@skrr-ai/cli 0.1.41 → 0.1.43
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/dist/commands/balance/overage.js +4 -3
- package/dist/commands/balance/plan.js +10 -6
- package/dist/commands/balance/usage/events.js +3 -2
- package/dist/commands/balance/usage.js +2 -1
- package/dist/commands/browser/install.js +3 -2
- package/dist/commands/browser/uninstall.js +3 -2
- package/dist/commands/code/install.js +6 -6
- package/dist/commands/followups/remind.js +4 -0
- package/dist/commands/harnesses/install.d.ts +29 -0
- package/dist/commands/harnesses/install.js +153 -25
- package/dist/commands/harnesses/installers.d.ts +10 -0
- package/dist/commands/harnesses/installers.js +31 -0
- package/dist/commands/harnesses/list.js +3 -2
- package/dist/commands/skills/import-as-actions.js +6 -15
- package/dist/commands/tasks/attachments/set-role.d.ts +24 -0
- package/dist/commands/tasks/attachments/set-role.js +60 -0
- package/dist/commands/tasks/attachments/upload.d.ts +1 -0
- package/dist/commands/tasks/attachments/upload.js +18 -52
- package/dist/commands/tasks/comments/add.d.ts +1 -0
- package/dist/commands/tasks/comments/add.js +67 -2
- package/dist/commands/tasks/complete.d.ts +1 -0
- package/dist/commands/tasks/complete.js +69 -1
- package/dist/commands/tasks/result/submit.d.ts +3 -0
- package/dist/commands/tasks/result/submit.js +78 -5
- package/dist/commands/tasks/update.js +4 -2
- package/dist/commands/tasks/updates/add.d.ts +1 -0
- package/dist/commands/tasks/updates/add.js +63 -0
- package/dist/lib/api-fetch.js +7 -2
- package/dist/lib/cli-installers.d.ts +63 -4
- package/dist/lib/cli-installers.js +98 -8
- package/dist/lib/dedicated-machines.js +4 -2
- package/dist/lib/file-mime.js +1 -1
- package/dist/lib/first-party-harness-agent.js +6 -5
- package/dist/lib/first-party-harness-doctor.js +15 -32
- package/dist/lib/first-party-harness-managed.d.ts +9 -4
- package/dist/lib/first-party-harness-managed.js +13 -10
- package/dist/lib/first-party-harness.d.ts +25 -21
- package/dist/lib/first-party-harness.js +40 -27
- package/dist/lib/followups.d.ts +2 -0
- package/dist/lib/harness-provider-input.d.ts +18 -12
- package/dist/lib/harness-provider-input.js +18 -12
- package/dist/lib/harness-tiers.d.ts +4 -3
- package/dist/lib/harness-tiers.js +4 -3
- package/dist/lib/html-text.js +25 -0
- package/dist/lib/local-skills.d.ts +27 -0
- package/dist/lib/local-skills.js +38 -0
- package/dist/lib/session-task-endpoints.d.ts +1 -1
- package/dist/lib/session-task-endpoints.js +3 -0
- package/dist/lib/task-asset-upload.d.ts +84 -0
- package/dist/lib/task-asset-upload.js +374 -0
- package/dist/lib/task-closure.d.ts +28 -0
- package/dist/lib/task-closure.js +37 -0
- package/dist/lib/tasks.d.ts +8 -0
- package/dist/lib/tasks.js +16 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarness.d.ts +113 -39
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarness.js +148 -71
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarnessChannels.d.ts +21 -33
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarnessChannels.js +24 -50
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarnessHome.d.ts +19 -7
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarnessHome.js +26 -10
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/harnessTrust.d.ts +9 -5
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/harnessTrust.js +9 -5
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +1 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +1 -13
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarness.d.ts +113 -39
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarness.js +146 -70
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarnessChannels.d.ts +21 -33
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarnessChannels.js +24 -49
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarnessHome.d.ts +19 -7
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarnessHome.js +27 -11
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/harnessTrust.d.ts +9 -5
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/harnessTrust.js +9 -5
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +1 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +1 -4
- package/dist/node_modules/@skrr-ai/auth-core/package.json +1 -1
- package/dist/node_modules/@skrr-ai/data-provider/index.js +4025 -3938
- package/dist/node_modules/@skrr-ai/data-provider/package.json +1 -1
- package/oclif.manifest.json +12853 -12721
- package/package.json +2 -2
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.readTaskForUpload = readTaskForUpload;
|
|
37
|
+
exports.uploadTaskAssetFile = uploadTaskAssetFile;
|
|
38
|
+
exports.attachTaskAssetFiles = attachTaskAssetFiles;
|
|
39
|
+
exports.taskAssetMarkdownRef = taskAssetMarkdownRef;
|
|
40
|
+
exports.sha256OfFile = sha256OfFile;
|
|
41
|
+
exports.postFileToStorage = postFileToStorage;
|
|
42
|
+
/**
|
|
43
|
+
* One Task-asset upload path for every CLI surface — `tasks attachments
|
|
44
|
+
* upload` and the `--attach` flag on updates/result/complete/comments all
|
|
45
|
+
* funnel here so the routing rules live in exactly one place (OSK-9863).
|
|
46
|
+
*
|
|
47
|
+
* ≤25MB or image → buffered multipart (canonicalization needs the bytes)
|
|
48
|
+
* larger non-image → intent → presigned POST to S3 → complete (OSK-9867)
|
|
49
|
+
*
|
|
50
|
+
* Session routing follows the CREDENTIAL, resolved here once per file so a
|
|
51
|
+
* narrowed agent session lands on `task-attachments*` and a human (or a
|
|
52
|
+
* delegated session holding a wide credential) lands on `/api/tasks/:id/*`.
|
|
53
|
+
*
|
|
54
|
+
* Retry safety is server-side: the sha256 IS the asset identity, so a second
|
|
55
|
+
* upload of the same bytes returns the existing ready row instead of
|
|
56
|
+
* duplicating it.
|
|
57
|
+
*
|
|
58
|
+
* The digest this module REPORTS is always the server's. The server
|
|
59
|
+
* canonicalizes images (EXIF strip, orientation, re-encode) before storing
|
|
60
|
+
* them, so the bytes on disk and the bytes stored are routinely different; an
|
|
61
|
+
* evidence reference that named the local hash would pin a version of the
|
|
62
|
+
* file that was never stored (OSK-10120). The local hash is computed only
|
|
63
|
+
* where the protocol needs it — declaring a direct upload, which storage then
|
|
64
|
+
* enforces — and never leaves this module as a claim about the asset.
|
|
65
|
+
*/
|
|
66
|
+
const fs = __importStar(require("node:fs"));
|
|
67
|
+
const http = __importStar(require("node:http"));
|
|
68
|
+
const https = __importStar(require("node:https"));
|
|
69
|
+
const path = __importStar(require("node:path"));
|
|
70
|
+
const node_buffer_1 = require("node:buffer");
|
|
71
|
+
const node_crypto_1 = require("node:crypto");
|
|
72
|
+
const data_provider_1 = require("@skrr-ai/data-provider");
|
|
73
|
+
const base_command_1 = require("../base-command");
|
|
74
|
+
const file_mime_1 = require("./file-mime");
|
|
75
|
+
const api_fetch_1 = require("./api-fetch");
|
|
76
|
+
const session_task_endpoints_1 = require("./session-task-endpoints");
|
|
77
|
+
function mb(bytes) {
|
|
78
|
+
return `${Math.floor(bytes / (1024 * 1024))} MB`;
|
|
79
|
+
}
|
|
80
|
+
const SHA256_HEX = /^[0-9a-f]{64}$/i;
|
|
81
|
+
function storedDigest(asset) {
|
|
82
|
+
const value = asset.contentSha256;
|
|
83
|
+
return typeof value === 'string' && SHA256_HEX.test(value) ? value.toLowerCase() : undefined;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Read the Task an upload targets, through the same credential the upload
|
|
87
|
+
* will use. In a session the task comes from the claim, so the read is the
|
|
88
|
+
* bound task and `taskId` is only reconciled against it — a scoped credential
|
|
89
|
+
* refuses to act on any other task before a byte leaves the machine.
|
|
90
|
+
*/
|
|
91
|
+
async function readTaskForUpload(taskId) {
|
|
92
|
+
const readPath = await (0, session_task_endpoints_1.sessionScopedTaskPath)('read');
|
|
93
|
+
if (readPath) {
|
|
94
|
+
const bound = await (0, api_fetch_1.apiFetch)(readPath);
|
|
95
|
+
if (!(0, session_task_endpoints_1.requestMatchesBoundTask)(bound, taskId)) {
|
|
96
|
+
throw new Error(`This session is bound to task ${String(bound.identifier ?? bound.id ?? '?')} — a scoped credential cannot attach files to ${taskId}.`);
|
|
97
|
+
}
|
|
98
|
+
return bound;
|
|
99
|
+
}
|
|
100
|
+
return (await data_provider_1.dataService.getTask(taskId, { enrich: false }));
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Upload `filePath` as a Task asset. In a session the task comes from the
|
|
104
|
+
* claim — `taskId` is only reconciled against the binding, never sent.
|
|
105
|
+
* Throws a plain Error; callers decide how to surface it.
|
|
106
|
+
*/
|
|
107
|
+
async function uploadTaskAssetFile(opts) {
|
|
108
|
+
const { taskId, filePath, role } = opts;
|
|
109
|
+
if (!fs.existsSync(filePath)) {
|
|
110
|
+
throw new Error(`File not found: ${filePath}`);
|
|
111
|
+
}
|
|
112
|
+
const stat = fs.statSync(filePath);
|
|
113
|
+
if (!stat.isFile()) {
|
|
114
|
+
throw new Error(`Not a regular file: ${filePath}`);
|
|
115
|
+
}
|
|
116
|
+
const size = stat.size;
|
|
117
|
+
const filename = path.basename(filePath);
|
|
118
|
+
const mimeType = (0, file_mime_1.mimeTypeForFilename)(filename);
|
|
119
|
+
const isImage = mimeType.startsWith('image/');
|
|
120
|
+
if (isImage && size > data_provider_1.TASK_ATTACHMENT_MAX_BYTES) {
|
|
121
|
+
throw new Error(`${filename} is too large for an image (max ${mb(data_provider_1.TASK_ATTACHMENT_MAX_BYTES)})`);
|
|
122
|
+
}
|
|
123
|
+
if (size > data_provider_1.TASK_DIRECT_UPLOAD_MAX_BYTES) {
|
|
124
|
+
throw new Error(`${filename} exceeds the direct-upload limit (${mb(data_provider_1.TASK_DIRECT_UPLOAD_MAX_BYTES)})`);
|
|
125
|
+
}
|
|
126
|
+
const direct = !isImage && size > data_provider_1.TASK_ATTACHMENT_MAX_BYTES;
|
|
127
|
+
const sessionPath = await (0, session_task_endpoints_1.sessionScopedTaskPath)('attachment');
|
|
128
|
+
let result;
|
|
129
|
+
try {
|
|
130
|
+
if (sessionPath) {
|
|
131
|
+
await readTaskForUpload(taskId);
|
|
132
|
+
result = direct
|
|
133
|
+
? await sessionDirectUpload({ filePath, size, filename, mimeType, role })
|
|
134
|
+
: await sessionBufferedUpload({ filePath, filename, mimeType, role });
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
result = direct
|
|
138
|
+
? await genericDirectUpload({ taskId, filePath, size, filename, mimeType, role })
|
|
139
|
+
: await genericBufferedUpload({ taskId, filePath, filename, mimeType, role });
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
catch (err) {
|
|
143
|
+
throw withServerReason(err, filename);
|
|
144
|
+
}
|
|
145
|
+
return {
|
|
146
|
+
id: String(result.id ?? result._id),
|
|
147
|
+
digest: storedDigest(result),
|
|
148
|
+
filename,
|
|
149
|
+
mimeType,
|
|
150
|
+
deduplicated: result.deduplicated === true,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Put the server's own reason on a refused upload.
|
|
155
|
+
*
|
|
156
|
+
* The transports keep response bodies out of `message` (a body can carry a
|
|
157
|
+
* stack trace), so a refusal read `HTTP 403 Forbidden — POST …` — and `--attach`
|
|
158
|
+
* prints only the message. Task media refusals are written to be acted on: 422
|
|
159
|
+
* an image whose bytes cannot be decoded, 415 a type the bytes contradict, 413
|
|
160
|
+
* the storage quota, 403 an agent asking for a Run input. The reason and its
|
|
161
|
+
* code replace the transport line; status and body stay on the error, so
|
|
162
|
+
* `handleApiError` still reads them. An error with no stated reason is left
|
|
163
|
+
* exactly as it was.
|
|
164
|
+
*/
|
|
165
|
+
function withServerReason(err, filename) {
|
|
166
|
+
if (!(err instanceof Error))
|
|
167
|
+
return err;
|
|
168
|
+
const { code, message } = (0, base_command_1.apiRefusal)(err);
|
|
169
|
+
if (!message)
|
|
170
|
+
return err;
|
|
171
|
+
err.message = `${filename}: ${message}${code ? ` (${code})` : ''}`;
|
|
172
|
+
return err;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Upload every `--attach` file and return the reference objects an Update or
|
|
176
|
+
* Result appends to `evidenceRefs`. Serial: half a dozen files is the
|
|
177
|
+
* realistic case and parallel uploads would race the session rate limiter.
|
|
178
|
+
*/
|
|
179
|
+
async function attachTaskAssetFiles(opts) {
|
|
180
|
+
const refs = [];
|
|
181
|
+
for (const filePath of opts.files) {
|
|
182
|
+
const asset = await uploadTaskAssetFile({
|
|
183
|
+
taskId: opts.taskId,
|
|
184
|
+
filePath,
|
|
185
|
+
role: opts.role,
|
|
186
|
+
});
|
|
187
|
+
refs.push({
|
|
188
|
+
kind: 'task_attachment',
|
|
189
|
+
id: asset.id,
|
|
190
|
+
...(asset.digest ? { digest: `sha256:${asset.digest}` } : {}),
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
return refs;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* The markdown image a comment body carries for an asset. The alt text is the
|
|
197
|
+
* filename, so its brackets are escaped: `shot [final].png` would otherwise
|
|
198
|
+
* close the alt early and break the reference.
|
|
199
|
+
*/
|
|
200
|
+
function taskAssetMarkdownRef(asset) {
|
|
201
|
+
const alt = asset.filename.replace(/[\\[\]]/g, (ch) => `\\${ch}`);
|
|
202
|
+
return ``;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* The buffered path holds the file in memory by design: it is capped at the
|
|
206
|
+
* buffered limit, and the server needs the whole body to canonicalize it.
|
|
207
|
+
*/
|
|
208
|
+
function fileFormData(filePath, filename, mimeType, role) {
|
|
209
|
+
const fd = new FormData();
|
|
210
|
+
fd.append('file', new node_buffer_1.Blob([fs.readFileSync(filePath)], { type: mimeType }), filename);
|
|
211
|
+
if (role)
|
|
212
|
+
fd.append('role', role);
|
|
213
|
+
return fd;
|
|
214
|
+
}
|
|
215
|
+
async function genericBufferedUpload(p) {
|
|
216
|
+
return data_provider_1.dataService.uploadTaskAttachment({
|
|
217
|
+
taskId: p.taskId,
|
|
218
|
+
formData: fileFormData(p.filePath, p.filename, p.mimeType, p.role),
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
async function sessionBufferedUpload(p) {
|
|
222
|
+
return (0, api_fetch_1.apiFetch)((await (0, session_task_endpoints_1.sessionScopedTaskPath)('attachment')), {
|
|
223
|
+
method: 'POST',
|
|
224
|
+
body: fileFormData(p.filePath, p.filename, p.mimeType, p.role),
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
async function genericDirectUpload(p) {
|
|
228
|
+
const intent = await data_provider_1.dataService.createTaskAttachmentUploadIntent({
|
|
229
|
+
taskId: p.taskId,
|
|
230
|
+
filename: p.filename,
|
|
231
|
+
mimeType: p.mimeType,
|
|
232
|
+
sizeBytes: p.size,
|
|
233
|
+
sha256: await sha256OfFile(p.filePath),
|
|
234
|
+
role: p.role,
|
|
235
|
+
});
|
|
236
|
+
if (!intent.uploadUrl)
|
|
237
|
+
return intent.attachment;
|
|
238
|
+
await postFileToStorage({
|
|
239
|
+
uploadUrl: intent.uploadUrl,
|
|
240
|
+
uploadFields: intent.uploadFields ?? {},
|
|
241
|
+
filePath: p.filePath,
|
|
242
|
+
size: p.size,
|
|
243
|
+
filename: p.filename,
|
|
244
|
+
mimeType: p.mimeType,
|
|
245
|
+
});
|
|
246
|
+
return data_provider_1.dataService.completeTaskAttachmentUpload({
|
|
247
|
+
taskId: p.taskId,
|
|
248
|
+
attachmentId: intent.attachment.id,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
async function sessionDirectUpload(p) {
|
|
252
|
+
const intent = await (0, api_fetch_1.apiFetch)((await (0, session_task_endpoints_1.sessionScopedTaskPath)('attachmentIntent')), {
|
|
253
|
+
method: 'POST',
|
|
254
|
+
body: {
|
|
255
|
+
filename: p.filename,
|
|
256
|
+
mimeType: p.mimeType,
|
|
257
|
+
sizeBytes: p.size,
|
|
258
|
+
sha256: await sha256OfFile(p.filePath),
|
|
259
|
+
...(p.role ? { role: p.role } : {}),
|
|
260
|
+
},
|
|
261
|
+
});
|
|
262
|
+
if (!intent.uploadUrl)
|
|
263
|
+
return intent.attachment;
|
|
264
|
+
await postFileToStorage({
|
|
265
|
+
uploadUrl: intent.uploadUrl,
|
|
266
|
+
uploadFields: intent.uploadFields ?? {},
|
|
267
|
+
filePath: p.filePath,
|
|
268
|
+
size: p.size,
|
|
269
|
+
filename: p.filename,
|
|
270
|
+
mimeType: p.mimeType,
|
|
271
|
+
});
|
|
272
|
+
return (0, api_fetch_1.apiFetch)((await (0, session_task_endpoints_1.sessionScopedTaskPath)('attachmentComplete')), {
|
|
273
|
+
method: 'POST',
|
|
274
|
+
body: { attachmentId: intent.attachment.id },
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
/** sha256 of a file, read as a stream: a 250MB recording never sits in memory. */
|
|
278
|
+
async function sha256OfFile(filePath) {
|
|
279
|
+
const hash = (0, node_crypto_1.createHash)('sha256');
|
|
280
|
+
for await (const chunk of fs.createReadStream(filePath)) {
|
|
281
|
+
hash.update(chunk);
|
|
282
|
+
}
|
|
283
|
+
return hash.digest('hex');
|
|
284
|
+
}
|
|
285
|
+
/** A form-data header value: no CR/LF, and quotes percent-encoded. */
|
|
286
|
+
function quotedHeaderValue(value) {
|
|
287
|
+
return value.replace(/[\r\n]/g, ' ').replace(/"/g, '%22');
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* POST the file to the presigned URL as multipart/form-data, streamed from
|
|
291
|
+
* disk.
|
|
292
|
+
*
|
|
293
|
+
* Built by hand rather than with `FormData` + `fetch` for two reasons, both
|
|
294
|
+
* about large files. `FormData` needs a `Blob`, which here means the whole
|
|
295
|
+
* file in memory — the double buffering this replaced read up to 250MB and
|
|
296
|
+
* then copied it again. And storage refuses a POST without a Content-Length
|
|
297
|
+
* (it does not take chunked uploads), which a streamed `fetch` body cannot
|
|
298
|
+
* promise; the exact length is known here, because every part except the file
|
|
299
|
+
* is a small in-memory string and the file's size came from `stat`.
|
|
300
|
+
*
|
|
301
|
+
* `file` goes LAST — S3 presigned POST evaluates fields in order and ignores
|
|
302
|
+
* anything after it. The fields carry `x-amz-checksum-sha256`, so storage
|
|
303
|
+
* itself rejects bytes that do not match the digest the intent declared,
|
|
304
|
+
* including a file that changed between hashing and upload.
|
|
305
|
+
*/
|
|
306
|
+
async function postFileToStorage(p) {
|
|
307
|
+
const boundary = `----skrr-upload-${(0, node_crypto_1.randomBytes)(16).toString('hex')}`;
|
|
308
|
+
const fieldParts = Object.entries(p.uploadFields).map(([name, value]) => `--${boundary}\r\nContent-Disposition: form-data; name="${quotedHeaderValue(name)}"\r\n\r\n${value}\r\n`);
|
|
309
|
+
const head = Buffer.from(fieldParts.join('') +
|
|
310
|
+
`--${boundary}\r\nContent-Disposition: form-data; name="file"; filename="${quotedHeaderValue(p.filename)}"\r\nContent-Type: ${quotedHeaderValue(p.mimeType)}\r\n\r\n`, 'utf8');
|
|
311
|
+
const tail = Buffer.from(`\r\n--${boundary}--\r\n`, 'utf8');
|
|
312
|
+
const url = new URL(p.uploadUrl);
|
|
313
|
+
const transport = url.protocol === 'http:' ? http : https;
|
|
314
|
+
await new Promise((resolve, reject) => {
|
|
315
|
+
let settled = false;
|
|
316
|
+
const fail = (err) => {
|
|
317
|
+
if (settled)
|
|
318
|
+
return;
|
|
319
|
+
settled = true;
|
|
320
|
+
reject(err);
|
|
321
|
+
};
|
|
322
|
+
const req = transport.request(url, {
|
|
323
|
+
method: 'POST',
|
|
324
|
+
headers: {
|
|
325
|
+
'Content-Type': `multipart/form-data; boundary=${boundary}`,
|
|
326
|
+
'Content-Length': String(head.length + p.size + tail.length),
|
|
327
|
+
},
|
|
328
|
+
}, (res) => {
|
|
329
|
+
let body = '';
|
|
330
|
+
res.setEncoding('utf8');
|
|
331
|
+
res.on('data', (chunk) => {
|
|
332
|
+
if (body.length < 4096)
|
|
333
|
+
body += chunk;
|
|
334
|
+
});
|
|
335
|
+
res.on('error', fail);
|
|
336
|
+
res.on('end', () => {
|
|
337
|
+
const status = res.statusCode ?? 0;
|
|
338
|
+
if (status >= 200 && status < 300) {
|
|
339
|
+
if (!settled) {
|
|
340
|
+
settled = true;
|
|
341
|
+
resolve();
|
|
342
|
+
}
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
// S3 names the refusal in XML (`<Code>BadDigest</Code>`,
|
|
346
|
+
// `EntityTooLarge`, `AccessDenied` once the POST policy expired).
|
|
347
|
+
// The code is the only part a person can act on.
|
|
348
|
+
const code = /<Code>([^<]+)<\/Code>/.exec(body)?.[1];
|
|
349
|
+
fail(new Error(`Storage rejected the upload (${status}${code ? ` ${code}` : ''})`));
|
|
350
|
+
});
|
|
351
|
+
});
|
|
352
|
+
req.on('error', fail);
|
|
353
|
+
req.write(head);
|
|
354
|
+
let sent = 0;
|
|
355
|
+
const stream = fs.createReadStream(p.filePath);
|
|
356
|
+
stream.on('data', (chunk) => {
|
|
357
|
+
sent += chunk.length;
|
|
358
|
+
});
|
|
359
|
+
stream.on('error', (err) => {
|
|
360
|
+
req.destroy(err);
|
|
361
|
+
fail(err);
|
|
362
|
+
});
|
|
363
|
+
stream.on('end', () => {
|
|
364
|
+
if (sent !== p.size) {
|
|
365
|
+
const err = new Error(`${p.filename} changed while it was being uploaded`);
|
|
366
|
+
req.destroy(err);
|
|
367
|
+
fail(err);
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
req.end(tail);
|
|
371
|
+
});
|
|
372
|
+
stream.pipe(req, { end: false });
|
|
373
|
+
});
|
|
374
|
+
}
|
|
@@ -1,4 +1,19 @@
|
|
|
1
1
|
import type { TaskClosureEvaluation } from '@skrr-ai/data-provider';
|
|
2
|
+
/**
|
|
3
|
+
* Is this Task SETTLED — closed in a way that refuses a new Result until it is
|
|
4
|
+
* reopened (`TASK_REOPEN_REQUIRED`)?
|
|
5
|
+
*
|
|
6
|
+
* Read the same way the server reads it: the immutable `statusType` when the row
|
|
7
|
+
* carries a known one, the legacy status id otherwise, and the `settled` band
|
|
8
|
+
* from the shared vocabulary rather than a column name. A command asks this
|
|
9
|
+
* BEFORE doing something the refusal would strand — uploading `--attach` files
|
|
10
|
+
* for a Result the server is about to refuse — and never instead of the
|
|
11
|
+
* server's own check, which stays the authority.
|
|
12
|
+
*/
|
|
13
|
+
export declare function taskIsSettled(task: {
|
|
14
|
+
statusType?: unknown;
|
|
15
|
+
status?: unknown;
|
|
16
|
+
} | null | undefined): boolean;
|
|
2
17
|
/**
|
|
3
18
|
* Render "where does this Task actually stand" for every surface that should
|
|
4
19
|
* say it.
|
|
@@ -28,6 +43,19 @@ export type ClosureLike = TaskClosureEvaluation | {
|
|
|
28
43
|
* the whole point is to be able to act without a second lookup.
|
|
29
44
|
*/
|
|
30
45
|
export declare function closureReport(closure: ClosureLike): string[];
|
|
46
|
+
/**
|
|
47
|
+
* The ways forward when a closed Task refuses a Result (`TASK_REOPEN_REQUIRED`).
|
|
48
|
+
*
|
|
49
|
+
* Both writers reach this refusal, `tasks result submit` and `tasks complete`,
|
|
50
|
+
* so both print from here. The first version sat in `result submit` alone and
|
|
51
|
+
* offered `tasks complete` as a way around it, which the same refusal stops:
|
|
52
|
+
* every step named here has to work on a closed Task (OSK-10031).
|
|
53
|
+
*/
|
|
54
|
+
export declare function reopenRequiredLines({ bin, task, command, }: {
|
|
55
|
+
bin: string;
|
|
56
|
+
task: string;
|
|
57
|
+
command: 'result submit' | 'complete';
|
|
58
|
+
}): string[];
|
|
31
59
|
/** `met (agent)` vs `met (person)` — shared with the web through data-provider. */
|
|
32
60
|
export { describeAssessorKind as describeAssessor } from '@skrr-ai/data-provider';
|
|
33
61
|
export { describeClosure as closureSnapshotLines, type TaskClosureSnapshot as ClosureSnapshot, } from '@skrr-ai/data-provider';
|
package/dist/lib/task-closure.js
CHANGED
|
@@ -1,8 +1,29 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.closureSnapshotLines = exports.describeAssessor = void 0;
|
|
4
|
+
exports.taskIsSettled = taskIsSettled;
|
|
4
5
|
exports.closureReport = closureReport;
|
|
6
|
+
exports.reopenRequiredLines = reopenRequiredLines;
|
|
5
7
|
const data_provider_1 = require("@skrr-ai/data-provider");
|
|
8
|
+
/**
|
|
9
|
+
* Is this Task SETTLED — closed in a way that refuses a new Result until it is
|
|
10
|
+
* reopened (`TASK_REOPEN_REQUIRED`)?
|
|
11
|
+
*
|
|
12
|
+
* Read the same way the server reads it: the immutable `statusType` when the row
|
|
13
|
+
* carries a known one, the legacy status id otherwise, and the `settled` band
|
|
14
|
+
* from the shared vocabulary rather than a column name. A command asks this
|
|
15
|
+
* BEFORE doing something the refusal would strand — uploading `--attach` files
|
|
16
|
+
* for a Result the server is about to refuse — and never instead of the
|
|
17
|
+
* server's own check, which stays the authority.
|
|
18
|
+
*/
|
|
19
|
+
function taskIsSettled(task) {
|
|
20
|
+
if (!task)
|
|
21
|
+
return false;
|
|
22
|
+
const type = (0, data_provider_1.isTaskWorkflowStateType)(task.statusType)
|
|
23
|
+
? task.statusType
|
|
24
|
+
: data_provider_1.LEGACY_STATUS_TYPE_BY_ID[String(task.status ?? '')];
|
|
25
|
+
return Boolean(type) && data_provider_1.TASK_WORKFLOW_TYPE_BANDS.settled.includes(type);
|
|
26
|
+
}
|
|
6
27
|
function isUnavailable(closure) {
|
|
7
28
|
return Boolean(closure && closure.unavailable);
|
|
8
29
|
}
|
|
@@ -82,6 +103,22 @@ function closureReport(closure) {
|
|
|
82
103
|
: 'Closing anyway is allowed — all of this is recorded on resolution.closure.');
|
|
83
104
|
return lines;
|
|
84
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* The ways forward when a closed Task refuses a Result (`TASK_REOPEN_REQUIRED`).
|
|
108
|
+
*
|
|
109
|
+
* Both writers reach this refusal, `tasks result submit` and `tasks complete`,
|
|
110
|
+
* so both print from here. The first version sat in `result submit` alone and
|
|
111
|
+
* offered `tasks complete` as a way around it, which the same refusal stops:
|
|
112
|
+
* every step named here has to work on a closed Task (OSK-10031).
|
|
113
|
+
*/
|
|
114
|
+
function reopenRequiredLines({ bin, task, command, }) {
|
|
115
|
+
return [
|
|
116
|
+
`→ Reopen it: ${bin} tasks move ${task} --status in_progress`,
|
|
117
|
+
command === 'complete'
|
|
118
|
+
? '→ Then run this command again.'
|
|
119
|
+
: `→ Then submit again, or submit and close in one step: ${bin} tasks complete ${task} --summary "…"`,
|
|
120
|
+
];
|
|
121
|
+
}
|
|
85
122
|
/** `met (agent)` vs `met (person)` — shared with the web through data-provider. */
|
|
86
123
|
var data_provider_2 = require("@skrr-ai/data-provider");
|
|
87
124
|
Object.defineProperty(exports, "describeAssessor", { enumerable: true, get: function () { return data_provider_2.describeAssessorKind; } });
|
package/dist/lib/tasks.d.ts
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
import type { ParsedDeliverableCoordinate, ParsedExpectationSpec } from '@skrr-ai/data-provider';
|
|
2
|
+
/**
|
|
3
|
+
* `--goal` on `tasks update`. `none`, `null` and `clear` detach the goal: PATCH
|
|
4
|
+
* sends `goalId: null`. Anything else names a goal. `clear` is accepted because
|
|
5
|
+
* this same command documents it for `--expect`, so it is the spelling a user
|
|
6
|
+
* reaches for — and it used to be stored as a goal id (OSK-10207).
|
|
7
|
+
* Undefined means the flag was not passed.
|
|
8
|
+
*/
|
|
9
|
+
export declare function resolveTaskGoalFlag(value: string | undefined): string | null | undefined;
|
|
2
10
|
export declare const TASK_PRIORITIES: readonly ["low", "medium", "high", "critical"];
|
|
3
11
|
/**
|
|
4
12
|
* The kinds `tasks events append --kind` will accept.
|
package/dist/lib/tasks.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.RECOGNIZED_TASK_VERDICTS = exports.DELIVERABLE_LIFECYCLE_VALUES = exports.EXPECT_FLAG_HELP = exports.TASK_WORK_EVENT_KINDS = exports.TASK_PRIORITIES = void 0;
|
|
4
|
+
exports.resolveTaskGoalFlag = resolveTaskGoalFlag;
|
|
4
5
|
exports.releasePinRepairGuard = releasePinRepairGuard;
|
|
5
6
|
exports.taskUpdateMutationGuard = taskUpdateMutationGuard;
|
|
6
7
|
exports.executionIsolationFromWorktreeFlag = executionIsolationFromWorktreeFlag;
|
|
@@ -36,6 +37,21 @@ exports.verdictIsRecognized = verdictIsRecognized;
|
|
|
36
37
|
const data_provider_1 = require("@skrr-ai/data-provider");
|
|
37
38
|
const prompt_1 = require("./prompt");
|
|
38
39
|
const projects_1 = require("./projects");
|
|
40
|
+
/**
|
|
41
|
+
* `--goal` on `tasks update`. `none`, `null` and `clear` detach the goal: PATCH
|
|
42
|
+
* sends `goalId: null`. Anything else names a goal. `clear` is accepted because
|
|
43
|
+
* this same command documents it for `--expect`, so it is the spelling a user
|
|
44
|
+
* reaches for — and it used to be stored as a goal id (OSK-10207).
|
|
45
|
+
* Undefined means the flag was not passed.
|
|
46
|
+
*/
|
|
47
|
+
function resolveTaskGoalFlag(value) {
|
|
48
|
+
if (value === undefined)
|
|
49
|
+
return undefined;
|
|
50
|
+
const v = value.trim();
|
|
51
|
+
if (!v || ['none', 'null', 'clear'].includes(v.toLowerCase()))
|
|
52
|
+
return null;
|
|
53
|
+
return v;
|
|
54
|
+
}
|
|
39
55
|
exports.TASK_PRIORITIES = ['low', 'medium', 'high', 'critical'];
|
|
40
56
|
const DESCRIPTION_FORMATS = ['html', 'markdown', 'plain'];
|
|
41
57
|
/**
|