@robbeverhelst/do 1.281.0 → 1.283.0
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/do.js +646 -13
- package/package.json +2 -1
package/dist/do.js
CHANGED
|
@@ -1038,7 +1038,7 @@ function flagBool(flags, name) {
|
|
|
1038
1038
|
|
|
1039
1039
|
// src/commands.ts
|
|
1040
1040
|
import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
1041
|
-
import { basename, dirname as dirname2, join as
|
|
1041
|
+
import { basename as basename2, dirname as dirname2, join as join4 } from "path";
|
|
1042
1042
|
|
|
1043
1043
|
// ../../node_modules/drizzle-orm/entity.js
|
|
1044
1044
|
var entityKind = Symbol.for("drizzle:entityKind");
|
|
@@ -4191,6 +4191,7 @@ var contacts = pgTable("contacts", {
|
|
|
4191
4191
|
tenantId: text("tenant_id").notNull().references(() => tenants.id),
|
|
4192
4192
|
kind: contactKind("kind").notNull().default("person"),
|
|
4193
4193
|
email: text("email"),
|
|
4194
|
+
emailInvalidAt: timestamp("email_invalid_at", { withTimezone: true }),
|
|
4194
4195
|
phone: text("phone"),
|
|
4195
4196
|
company: text("company"),
|
|
4196
4197
|
website: text("website"),
|
|
@@ -22235,7 +22236,14 @@ var setHealthLabAnalyteInputSchema = object({
|
|
|
22235
22236
|
|
|
22236
22237
|
// ../../packages/schema/src/contracts/files.ts
|
|
22237
22238
|
var FILE_TEXT_MAX = 1e6;
|
|
22238
|
-
var FILE_SOURCES = [
|
|
22239
|
+
var FILE_SOURCES = [
|
|
22240
|
+
"upload",
|
|
22241
|
+
"chat",
|
|
22242
|
+
"doccle",
|
|
22243
|
+
"doccle_zip",
|
|
22244
|
+
"scan",
|
|
22245
|
+
"game_save"
|
|
22246
|
+
];
|
|
22239
22247
|
var fileSourceSchema = union([
|
|
22240
22248
|
_enum2(FILE_SOURCES),
|
|
22241
22249
|
string2().regex(/^gmail:[A-Za-z0-9_-]{1,200}$/, "gmail:<message id>")
|
|
@@ -24144,7 +24152,8 @@ var contactSchema = createSelectSchema(contacts, {
|
|
|
24144
24152
|
lastContactedAt: exports_coerce.date().nullable(),
|
|
24145
24153
|
userId: string2().nullable().default(null),
|
|
24146
24154
|
rhythmDays: number2().int().nullable().default(null),
|
|
24147
|
-
rhythmSnoozedUntil: string2().nullable().default(null)
|
|
24155
|
+
rhythmSnoozedUntil: string2().nullable().default(null),
|
|
24156
|
+
emailInvalidAt: exports_coerce.date().nullable().default(null)
|
|
24148
24157
|
});
|
|
24149
24158
|
var contactImportantDateSchema = object({
|
|
24150
24159
|
id: ulidSchema,
|
|
@@ -24188,7 +24197,7 @@ var createContactInputSchema = createInsertSchema(contacts, {
|
|
|
24188
24197
|
lastContactedAt: exports_coerce.date().nullish(),
|
|
24189
24198
|
rhythmDays: contactPeopleInputShape.rhythmDays,
|
|
24190
24199
|
rhythmSnoozedUntil: contactPeopleInputShape.rhythmSnoozedUntil
|
|
24191
|
-
}).omit({ tenantId: true, keepInTouchRuleId: true, userId: true }).extend({
|
|
24200
|
+
}).omit({ tenantId: true, keepInTouchRuleId: true, userId: true, emailInvalidAt: true }).extend({
|
|
24192
24201
|
title: string2().min(1),
|
|
24193
24202
|
parentId: ulidSchema.nullish(),
|
|
24194
24203
|
importantDates: array(contactImportantDateInputSchema).optional(),
|
|
@@ -24205,7 +24214,7 @@ var updateContactInputSchema = createUpdateSchema(contacts, {
|
|
|
24205
24214
|
lastContactedAt: exports_coerce.date().nullish(),
|
|
24206
24215
|
rhythmDays: contactPeopleInputShape.rhythmDays,
|
|
24207
24216
|
rhythmSnoozedUntil: contactPeopleInputShape.rhythmSnoozedUntil
|
|
24208
|
-
}).omit({ id: true, tenantId: true, keepInTouchRuleId: true, userId: true }).extend({
|
|
24217
|
+
}).omit({ id: true, tenantId: true, keepInTouchRuleId: true, userId: true, emailInvalidAt: true }).extend({
|
|
24209
24218
|
title: string2().min(1).optional(),
|
|
24210
24219
|
parentId: ulidSchema.nullish(),
|
|
24211
24220
|
importantDates: array(contactImportantDateInputSchema).optional(),
|
|
@@ -25394,6 +25403,35 @@ var runInstallSchema = object({
|
|
|
25394
25403
|
lastBackup: isoDate.nullish(),
|
|
25395
25404
|
notes: text5.nullish()
|
|
25396
25405
|
}).strict();
|
|
25406
|
+
var SAVE_SCREENSHOT_MAX_BASE64 = 1500000;
|
|
25407
|
+
var runSaveFields = {
|
|
25408
|
+
game: _enum2(["skyrim"]).default("skyrim"),
|
|
25409
|
+
fileName: shortText.nullish(),
|
|
25410
|
+
saveNumber: number2().int().min(0).max(1e9),
|
|
25411
|
+
playerName: name,
|
|
25412
|
+
level: number2().int().min(0).max(1e4),
|
|
25413
|
+
location: shortText,
|
|
25414
|
+
race: shortText.nullish(),
|
|
25415
|
+
sex: _enum2(["male", "female"]).nullish(),
|
|
25416
|
+
playTimeMinutes: number2().int().min(0).max(1e4 * 60).nullish(),
|
|
25417
|
+
savedAt: string2().datetime({ offset: true }).nullish(),
|
|
25418
|
+
gameDate: object({
|
|
25419
|
+
text: shortText.pipe(string2().min(1)),
|
|
25420
|
+
hour: number2().min(0).max(24).nullish(),
|
|
25421
|
+
daysPassed: number2().min(0).max(1e6).nullish()
|
|
25422
|
+
}).strict().nullish(),
|
|
25423
|
+
plugins: array(shortText).max(600).optional(),
|
|
25424
|
+
stats: record(shortText, number2().int()).refine((r) => Object.keys(r).length <= 300, "at most 300 stats").optional()
|
|
25425
|
+
};
|
|
25426
|
+
var runSaveSchema = object({
|
|
25427
|
+
...runSaveFields,
|
|
25428
|
+
screenshotFileId: string2().min(1).max(64).nullish(),
|
|
25429
|
+
uploadedAt: string2().datetime({ offset: true })
|
|
25430
|
+
}).strict();
|
|
25431
|
+
var uploadRunSaveHeaderInputSchema = object({
|
|
25432
|
+
...runSaveFields,
|
|
25433
|
+
screenshot: string2().max(SAVE_SCREENSHOT_MAX_BASE64).regex(/^[A-Za-z0-9+/]+={0,2}$/, "must be base64").nullish()
|
|
25434
|
+
}).strict();
|
|
25397
25435
|
var runFacetSchema = object({
|
|
25398
25436
|
theme: runThemeSchema.default("generic"),
|
|
25399
25437
|
edition: shortText.nullish(),
|
|
@@ -25414,6 +25452,7 @@ var runFacetSchema = object({
|
|
|
25414
25452
|
achievements: runAchievementsSchema.nullish(),
|
|
25415
25453
|
rareFinds: runRareFindsSchema.nullish(),
|
|
25416
25454
|
install: runInstallSchema.nullish(),
|
|
25455
|
+
save: runSaveSchema.nullish(),
|
|
25417
25456
|
money: object({ goal: shortText.nullish(), purpose: shortText.nullish() }).strict().nullish(),
|
|
25418
25457
|
links: array(object({ label: name, url: string2().url().max(2000) }).strict()).max(50).optional()
|
|
25419
25458
|
}).strict();
|
|
@@ -25441,6 +25480,7 @@ var runValueSectionValues = [
|
|
|
25441
25480
|
"achievements",
|
|
25442
25481
|
"rareFinds",
|
|
25443
25482
|
"install",
|
|
25483
|
+
"save",
|
|
25444
25484
|
"money"
|
|
25445
25485
|
];
|
|
25446
25486
|
var runValueSectionSchema = _enum2(runValueSectionValues);
|
|
@@ -25544,6 +25584,12 @@ var runChangesResultSchema = object({
|
|
|
25544
25584
|
sessionId: string2(),
|
|
25545
25585
|
batch: runChangeBatchSchema
|
|
25546
25586
|
});
|
|
25587
|
+
var runSaveHeaderResultSchema = object({
|
|
25588
|
+
run: runDetailSchema,
|
|
25589
|
+
unchanged: boolean3(),
|
|
25590
|
+
sessionId: string2().nullable(),
|
|
25591
|
+
batch: runChangeBatchSchema.nullable()
|
|
25592
|
+
});
|
|
25547
25593
|
var createRunInputSchema = object({
|
|
25548
25594
|
id: ulidSchema,
|
|
25549
25595
|
gameId: ulidSchema,
|
|
@@ -28315,7 +28361,7 @@ function parseCapture2(input, now) {
|
|
|
28315
28361
|
|
|
28316
28362
|
// src/meta.ts
|
|
28317
28363
|
var CLI_NAME = "do";
|
|
28318
|
-
var VERSION = "1.
|
|
28364
|
+
var VERSION = "1.283.0";
|
|
28319
28365
|
var API_PREFIX = "/api/v1";
|
|
28320
28366
|
var DEFAULT_API_URL = "http://localhost:3001";
|
|
28321
28367
|
|
|
@@ -33305,6 +33351,34 @@ var COMMAND_SPECS = [
|
|
|
33305
33351
|
returns: "RunChangesResult",
|
|
33306
33352
|
notes: "POST /api/v1/runs/{id}/changes. Ops: set|merge on a one-value section (character, arc, nextSteps, achievements, install, \u2026), upsert|remove on a list section matched by name (quests, choices, homes, people, mounts, uniques, orderOfPlay, team, checklists, links), tick on a checklist. The batch joins the last session if it was active in the last 6 hours, else opens a new one. A change that leaves the run invalid writes nothing."
|
|
33307
33353
|
},
|
|
33354
|
+
{
|
|
33355
|
+
path: ["runs", "save-header"],
|
|
33356
|
+
summary: "Read a Skyrim save (read-only) and send its header and screenshot to a run.",
|
|
33357
|
+
args: [
|
|
33358
|
+
{ name: "id", required: true, description: "Run ULID." },
|
|
33359
|
+
{
|
|
33360
|
+
name: "save",
|
|
33361
|
+
required: true,
|
|
33362
|
+
description: "A .ess save, or the Saves folder (its newest save)."
|
|
33363
|
+
}
|
|
33364
|
+
],
|
|
33365
|
+
flags: [
|
|
33366
|
+
{
|
|
33367
|
+
name: "watch",
|
|
33368
|
+
type: "boolean",
|
|
33369
|
+
description: "Keep watching the Saves folder; send each new save once the game has written it."
|
|
33370
|
+
},
|
|
33371
|
+
{
|
|
33372
|
+
name: "interval",
|
|
33373
|
+
type: "string",
|
|
33374
|
+
description: "With --watch: seconds between looks (default 10)."
|
|
33375
|
+
},
|
|
33376
|
+
{ name: "dry-run", type: "boolean", description: "Print what the save says; send nothing." }
|
|
33377
|
+
],
|
|
33378
|
+
scopes: ["collections:write"],
|
|
33379
|
+
returns: "RunSaveHeaderResult",
|
|
33380
|
+
notes: "POST /api/v1/runs/{id}/save-header. The save sync (run tracker phase 4): name, level, location, race, play time, the in-game date, plugins and General Stats become the run's `save` section and its character's level, as one update with a receipt and Undo; the screenshot is a file on the run (the last one is dropped). The same save twice changes nothing. Needs a member's key (the screenshot is stored as theirs). The save itself is only read. See docs/agents/skyrim-save-sync.md."
|
|
33381
|
+
},
|
|
33308
33382
|
{
|
|
33309
33383
|
path: ["runs", "undo"],
|
|
33310
33384
|
summary: "Undo a run's latest chat update (or the one given).",
|
|
@@ -35288,6 +35362,534 @@ function renderEvents(data, mode) {
|
|
|
35288
35362
|
`);
|
|
35289
35363
|
}
|
|
35290
35364
|
|
|
35365
|
+
// src/save-sync.ts
|
|
35366
|
+
import { readFile, stat as stat2 } from "fs/promises";
|
|
35367
|
+
import { basename } from "path";
|
|
35368
|
+
|
|
35369
|
+
// ../../packages/skyrim-save/src/body.ts
|
|
35370
|
+
import { inflateSync } from "zlib";
|
|
35371
|
+
|
|
35372
|
+
// ../../packages/skyrim-save/src/reader.ts
|
|
35373
|
+
class SaveFormatError extends Error {
|
|
35374
|
+
name = "SaveFormatError";
|
|
35375
|
+
}
|
|
35376
|
+
|
|
35377
|
+
class Reader {
|
|
35378
|
+
bytes;
|
|
35379
|
+
view;
|
|
35380
|
+
offset;
|
|
35381
|
+
constructor(bytes, offset = 0) {
|
|
35382
|
+
this.bytes = bytes;
|
|
35383
|
+
this.view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
35384
|
+
this.offset = offset;
|
|
35385
|
+
}
|
|
35386
|
+
get remaining() {
|
|
35387
|
+
return this.bytes.byteLength - this.offset;
|
|
35388
|
+
}
|
|
35389
|
+
need(n, what) {
|
|
35390
|
+
if (n < 0 || this.offset + n > this.bytes.byteLength) {
|
|
35391
|
+
throw new SaveFormatError(`the save ends before its ${what} (at byte ${this.offset})`);
|
|
35392
|
+
}
|
|
35393
|
+
const at = this.offset;
|
|
35394
|
+
this.offset += n;
|
|
35395
|
+
return at;
|
|
35396
|
+
}
|
|
35397
|
+
u8(what) {
|
|
35398
|
+
return this.view.getUint8(this.need(1, what));
|
|
35399
|
+
}
|
|
35400
|
+
u16(what) {
|
|
35401
|
+
return this.view.getUint16(this.need(2, what), true);
|
|
35402
|
+
}
|
|
35403
|
+
u32(what) {
|
|
35404
|
+
return this.view.getUint32(this.need(4, what), true);
|
|
35405
|
+
}
|
|
35406
|
+
f32(what) {
|
|
35407
|
+
return this.view.getFloat32(this.need(4, what), true);
|
|
35408
|
+
}
|
|
35409
|
+
u64(what) {
|
|
35410
|
+
return this.view.getBigUint64(this.need(8, what), true);
|
|
35411
|
+
}
|
|
35412
|
+
take(n, what) {
|
|
35413
|
+
const at = this.need(n, what);
|
|
35414
|
+
return this.bytes.subarray(at, at + n);
|
|
35415
|
+
}
|
|
35416
|
+
wstring(what) {
|
|
35417
|
+
const len = this.u16(what);
|
|
35418
|
+
return decodeLatin1(this.take(len, what));
|
|
35419
|
+
}
|
|
35420
|
+
}
|
|
35421
|
+
function decodeLatin1(bytes) {
|
|
35422
|
+
let out = "";
|
|
35423
|
+
for (const b of bytes)
|
|
35424
|
+
out += String.fromCharCode(b);
|
|
35425
|
+
return out;
|
|
35426
|
+
}
|
|
35427
|
+
|
|
35428
|
+
// ../../packages/skyrim-save/src/lz4.ts
|
|
35429
|
+
function lz4DecompressBlock(src, size) {
|
|
35430
|
+
const out = new Uint8Array(size);
|
|
35431
|
+
let i = 0;
|
|
35432
|
+
let o = 0;
|
|
35433
|
+
while (i < src.length) {
|
|
35434
|
+
const token = src[i++];
|
|
35435
|
+
let literals = token >> 4;
|
|
35436
|
+
if (literals === 15) {
|
|
35437
|
+
let b;
|
|
35438
|
+
do {
|
|
35439
|
+
b = byteAt(src, i++);
|
|
35440
|
+
literals += b;
|
|
35441
|
+
} while (b === 255);
|
|
35442
|
+
}
|
|
35443
|
+
if (i + literals > src.length || o + literals > size) {
|
|
35444
|
+
throw new SaveFormatError("the save's body is cut short (LZ4 literals)");
|
|
35445
|
+
}
|
|
35446
|
+
out.set(src.subarray(i, i + literals), o);
|
|
35447
|
+
i += literals;
|
|
35448
|
+
o += literals;
|
|
35449
|
+
if (i >= src.length)
|
|
35450
|
+
break;
|
|
35451
|
+
const offset = byteAt(src, i) | byteAt(src, i + 1) << 8;
|
|
35452
|
+
i += 2;
|
|
35453
|
+
if (offset === 0 || offset > o)
|
|
35454
|
+
throw new SaveFormatError("a bad LZ4 match in the save's body");
|
|
35455
|
+
let length = (token & 15) + 4;
|
|
35456
|
+
if ((token & 15) === 15) {
|
|
35457
|
+
let b;
|
|
35458
|
+
do {
|
|
35459
|
+
b = byteAt(src, i++);
|
|
35460
|
+
length += b;
|
|
35461
|
+
} while (b === 255);
|
|
35462
|
+
}
|
|
35463
|
+
if (o + length > size)
|
|
35464
|
+
throw new SaveFormatError("the save's body is longer than it says");
|
|
35465
|
+
for (let k = 0;k < length; k++, o++)
|
|
35466
|
+
out[o] = out[o - offset];
|
|
35467
|
+
}
|
|
35468
|
+
if (o !== size)
|
|
35469
|
+
throw new SaveFormatError("the save's body is shorter than it says");
|
|
35470
|
+
return out;
|
|
35471
|
+
}
|
|
35472
|
+
function byteAt(src, i) {
|
|
35473
|
+
const b = src[i];
|
|
35474
|
+
if (b === undefined)
|
|
35475
|
+
throw new SaveFormatError("the save's body is cut short (LZ4)");
|
|
35476
|
+
return b;
|
|
35477
|
+
}
|
|
35478
|
+
|
|
35479
|
+
// ../../packages/skyrim-save/src/body.ts
|
|
35480
|
+
var SAVE_STAT_CATEGORIES = [
|
|
35481
|
+
"general",
|
|
35482
|
+
"quest",
|
|
35483
|
+
"combat",
|
|
35484
|
+
"magic",
|
|
35485
|
+
"crafting",
|
|
35486
|
+
"crime",
|
|
35487
|
+
"dlc"
|
|
35488
|
+
];
|
|
35489
|
+
var TAMRIEL_MONTHS = [
|
|
35490
|
+
"Morning Star",
|
|
35491
|
+
"Sun's Dawn",
|
|
35492
|
+
"First Seed",
|
|
35493
|
+
"Rain's Hand",
|
|
35494
|
+
"Second Seed",
|
|
35495
|
+
"Midyear",
|
|
35496
|
+
"Sun's Height",
|
|
35497
|
+
"Last Seed",
|
|
35498
|
+
"Hearthfire",
|
|
35499
|
+
"Frostfall",
|
|
35500
|
+
"Sun's Dusk",
|
|
35501
|
+
"Evening Star"
|
|
35502
|
+
];
|
|
35503
|
+
var CLOCK_GLOBALS = {
|
|
35504
|
+
year: 53,
|
|
35505
|
+
month: 54,
|
|
35506
|
+
day: 55,
|
|
35507
|
+
hour: 56,
|
|
35508
|
+
daysPassed: 57
|
|
35509
|
+
};
|
|
35510
|
+
var MISC_STATS = 0;
|
|
35511
|
+
var GLOBAL_VARIABLES = 3;
|
|
35512
|
+
var LIGHT_PLUGINS_FROM = 78;
|
|
35513
|
+
function decompressBody(bytes, header) {
|
|
35514
|
+
if (header.compression === "none")
|
|
35515
|
+
return bytes.subarray(header.bodyOffset);
|
|
35516
|
+
const r = new Reader(bytes, header.bodyOffset);
|
|
35517
|
+
const uncompressed = r.u32("uncompressed size");
|
|
35518
|
+
const compressed = r.u32("compressed size");
|
|
35519
|
+
if (uncompressed > 512 * 1024 * 1024)
|
|
35520
|
+
throw new SaveFormatError("the save's body is too big");
|
|
35521
|
+
const block = r.take(compressed, "compressed body");
|
|
35522
|
+
if (header.compression === "lz4")
|
|
35523
|
+
return lz4DecompressBlock(block, uncompressed);
|
|
35524
|
+
const out = new Uint8Array(inflateSync(block));
|
|
35525
|
+
if (out.byteLength !== uncompressed) {
|
|
35526
|
+
throw new SaveFormatError("the save's body is not the size it says");
|
|
35527
|
+
}
|
|
35528
|
+
return out;
|
|
35529
|
+
}
|
|
35530
|
+
function parseSaveBody(bytes, header) {
|
|
35531
|
+
const body = decompressBody(bytes, header);
|
|
35532
|
+
const r = new Reader(body);
|
|
35533
|
+
const formVersion = r.u8("form version");
|
|
35534
|
+
const pluginInfoSize = r.u32("plugin info size");
|
|
35535
|
+
const pluginsEnd = r.offset + pluginInfoSize;
|
|
35536
|
+
const plugins = [];
|
|
35537
|
+
const pluginCount = r.u8("plugin count");
|
|
35538
|
+
for (let i = 0;i < pluginCount; i++)
|
|
35539
|
+
plugins.push(r.wstring("plugin name"));
|
|
35540
|
+
const lightPlugins = [];
|
|
35541
|
+
if (header.version >= 12 && formVersion >= LIGHT_PLUGINS_FROM) {
|
|
35542
|
+
const lightCount = r.u16("light plugin count");
|
|
35543
|
+
for (let i = 0;i < lightCount; i++)
|
|
35544
|
+
lightPlugins.push(r.wstring("light plugin name"));
|
|
35545
|
+
}
|
|
35546
|
+
if (r.offset !== pluginsEnd) {
|
|
35547
|
+
throw new SaveFormatError("the save's plugin list is not the size it says");
|
|
35548
|
+
}
|
|
35549
|
+
const formIdArrayAt = r.u32("form id array offset");
|
|
35550
|
+
r.u32("unknown table offset");
|
|
35551
|
+
const globals1At = r.u32("global data table 1 offset");
|
|
35552
|
+
r.u32("global data table 2 offset");
|
|
35553
|
+
r.u32("change forms offset");
|
|
35554
|
+
r.u32("global data table 3 offset");
|
|
35555
|
+
const globals1Count = r.u32("global data table 1 count");
|
|
35556
|
+
const base = header.bodyOffset;
|
|
35557
|
+
const formIds = readFormIdArray(body, formIdArrayAt - base);
|
|
35558
|
+
const { globals, stats } = readGlobalData(body, globals1At - base, globals1Count, formIds);
|
|
35559
|
+
return { formVersion, plugins, lightPlugins, gameTime: clockFrom(globals), stats };
|
|
35560
|
+
}
|
|
35561
|
+
function readFormIdArray(body, at) {
|
|
35562
|
+
const r = new Reader(body, at);
|
|
35563
|
+
const count = r.u32("form id count");
|
|
35564
|
+
if (count * 4 > r.remaining)
|
|
35565
|
+
throw new SaveFormatError("the save's form id array is cut short");
|
|
35566
|
+
const out = new Uint32Array(count);
|
|
35567
|
+
for (let i = 0;i < count; i++)
|
|
35568
|
+
out[i] = r.u32("form id");
|
|
35569
|
+
return out;
|
|
35570
|
+
}
|
|
35571
|
+
function readGlobalData(body, at, count, formIds) {
|
|
35572
|
+
const r = new Reader(body, at);
|
|
35573
|
+
const globals = new Map;
|
|
35574
|
+
const stats = [];
|
|
35575
|
+
for (let i = 0;i < count; i++) {
|
|
35576
|
+
const type = r.u32("global data type");
|
|
35577
|
+
const length = r.u32("global data length");
|
|
35578
|
+
const g = new Reader(r.take(length, "global data"));
|
|
35579
|
+
if (type === MISC_STATS) {
|
|
35580
|
+
const n = g.u32("stat count");
|
|
35581
|
+
for (let k = 0;k < n; k++) {
|
|
35582
|
+
const name = g.wstring("stat name");
|
|
35583
|
+
const category = SAVE_STAT_CATEGORIES[g.u8("stat category")] ?? "general";
|
|
35584
|
+
const value = g.u32("stat value") | 0;
|
|
35585
|
+
if (value !== 0)
|
|
35586
|
+
stats.push({ name, category, value });
|
|
35587
|
+
}
|
|
35588
|
+
} else if (type === GLOBAL_VARIABLES) {
|
|
35589
|
+
const n = vsval(g);
|
|
35590
|
+
for (let k = 0;k < n; k++) {
|
|
35591
|
+
const id = refId(g, formIds);
|
|
35592
|
+
globals.set(id, g.f32("global value"));
|
|
35593
|
+
}
|
|
35594
|
+
}
|
|
35595
|
+
}
|
|
35596
|
+
return { globals, stats };
|
|
35597
|
+
}
|
|
35598
|
+
function clockFrom(globals) {
|
|
35599
|
+
const get = (id) => globals.get(id);
|
|
35600
|
+
const year = get(CLOCK_GLOBALS.year);
|
|
35601
|
+
const month = get(CLOCK_GLOBALS.month);
|
|
35602
|
+
const day = get(CLOCK_GLOBALS.day);
|
|
35603
|
+
const hour = get(CLOCK_GLOBALS.hour);
|
|
35604
|
+
const daysPassed = get(CLOCK_GLOBALS.daysPassed);
|
|
35605
|
+
if (year === undefined || month === undefined || day === undefined || hour === undefined) {
|
|
35606
|
+
return null;
|
|
35607
|
+
}
|
|
35608
|
+
return {
|
|
35609
|
+
year: Math.round(year),
|
|
35610
|
+
month: Math.round(month),
|
|
35611
|
+
day: Math.round(day),
|
|
35612
|
+
hour,
|
|
35613
|
+
daysPassed: daysPassed ?? 0
|
|
35614
|
+
};
|
|
35615
|
+
}
|
|
35616
|
+
function vsval(r) {
|
|
35617
|
+
const first = r.u8("vsval");
|
|
35618
|
+
const size = first & 3;
|
|
35619
|
+
if (size === 0)
|
|
35620
|
+
return first >> 2;
|
|
35621
|
+
if (size === 1)
|
|
35622
|
+
return (first | r.u8("vsval") << 8) >> 2;
|
|
35623
|
+
const rest = r.u16("vsval");
|
|
35624
|
+
return (first | rest << 8) >>> 2;
|
|
35625
|
+
}
|
|
35626
|
+
function refId(r, formIds) {
|
|
35627
|
+
const b0 = r.u8("ref id");
|
|
35628
|
+
const b1 = r.u8("ref id");
|
|
35629
|
+
const b2 = r.u8("ref id");
|
|
35630
|
+
const value = (b0 & 63) << 16 | b1 << 8 | b2;
|
|
35631
|
+
const type = b0 >> 6;
|
|
35632
|
+
if (type === 1)
|
|
35633
|
+
return value;
|
|
35634
|
+
if (type === 2)
|
|
35635
|
+
return (4278190080 | value) >>> 0;
|
|
35636
|
+
if (type === 0)
|
|
35637
|
+
return value === 0 ? 0 : formIds[value - 1] ?? 0;
|
|
35638
|
+
return 0;
|
|
35639
|
+
}
|
|
35640
|
+
function formatGameDate(time) {
|
|
35641
|
+
const month = TAMRIEL_MONTHS[time.month] ?? `month ${time.month + 1}`;
|
|
35642
|
+
return `${ordinal(time.day)} of ${month}, 4E ${time.year}`;
|
|
35643
|
+
}
|
|
35644
|
+
function ordinal(n) {
|
|
35645
|
+
const tens = n % 100;
|
|
35646
|
+
if (tens >= 11 && tens <= 13)
|
|
35647
|
+
return `${n}th`;
|
|
35648
|
+
const suffix = n % 10 === 1 ? "st" : n % 10 === 2 ? "nd" : n % 10 === 3 ? "rd" : "th";
|
|
35649
|
+
return `${n}${suffix}`;
|
|
35650
|
+
}
|
|
35651
|
+
// ../../packages/skyrim-save/src/header.ts
|
|
35652
|
+
var SAVE_MAGIC = "TESV_SAVEGAME";
|
|
35653
|
+
var SE_VERSION = 12;
|
|
35654
|
+
var FILETIME_UNIX_EPOCH = 116444736000000000n;
|
|
35655
|
+
function parseSaveHeader(bytes) {
|
|
35656
|
+
const r = new Reader(bytes);
|
|
35657
|
+
const magic = String.fromCharCode(...r.take(SAVE_MAGIC.length, "magic"));
|
|
35658
|
+
if (magic !== SAVE_MAGIC)
|
|
35659
|
+
throw new SaveFormatError("not a Skyrim save (no TESV_SAVEGAME magic)");
|
|
35660
|
+
const headerSize = r.u32("header size");
|
|
35661
|
+
const headerStart = r.offset;
|
|
35662
|
+
const version = r.u32("version");
|
|
35663
|
+
if (version < 7 || version > 64)
|
|
35664
|
+
throw new SaveFormatError(`unknown save version ${version}`);
|
|
35665
|
+
const saveNumber = r.u32("save number");
|
|
35666
|
+
const playerName = r.wstring("player name");
|
|
35667
|
+
const level = r.u32("level");
|
|
35668
|
+
const location = r.wstring("location");
|
|
35669
|
+
const gameDate = r.wstring("game date");
|
|
35670
|
+
const raceEditorId = r.wstring("race");
|
|
35671
|
+
const sexCode = r.u16("sex");
|
|
35672
|
+
const current = r.f32("experience");
|
|
35673
|
+
const toNextLevel = r.f32("experience to next level");
|
|
35674
|
+
const filetime = r.u64("save time");
|
|
35675
|
+
const width = r.u32("screenshot width");
|
|
35676
|
+
const height = r.u32("screenshot height");
|
|
35677
|
+
let compression = "none";
|
|
35678
|
+
if (version >= SE_VERSION) {
|
|
35679
|
+
const code = r.u16("compression");
|
|
35680
|
+
compression = code === 1 ? "zlib" : code === 2 ? "lz4" : "none";
|
|
35681
|
+
}
|
|
35682
|
+
if (r.offset - headerStart !== headerSize) {
|
|
35683
|
+
throw new SaveFormatError(`the header says ${headerSize} bytes but its fields take ${r.offset - headerStart}`);
|
|
35684
|
+
}
|
|
35685
|
+
if (width > 4096 || height > 4096) {
|
|
35686
|
+
throw new SaveFormatError(`a ${width}\xD7${height} screenshot is not a save's`);
|
|
35687
|
+
}
|
|
35688
|
+
const bytesPerPixel = version >= SE_VERSION ? 4 : 3;
|
|
35689
|
+
const pixels = r.take(width * height * bytesPerPixel, "screenshot");
|
|
35690
|
+
return {
|
|
35691
|
+
version,
|
|
35692
|
+
saveNumber,
|
|
35693
|
+
playerName,
|
|
35694
|
+
level,
|
|
35695
|
+
location,
|
|
35696
|
+
gameDate,
|
|
35697
|
+
playTimeMinutes: playTimeMinutes(gameDate),
|
|
35698
|
+
raceEditorId,
|
|
35699
|
+
race: raceName(raceEditorId),
|
|
35700
|
+
sex: sexCode === 1 ? "female" : "male",
|
|
35701
|
+
experience: { current, toNextLevel },
|
|
35702
|
+
savedAt: filetimeToIso(filetime),
|
|
35703
|
+
screenshot: { width, height, bytesPerPixel, pixels },
|
|
35704
|
+
compression,
|
|
35705
|
+
bodyOffset: r.offset
|
|
35706
|
+
};
|
|
35707
|
+
}
|
|
35708
|
+
function playTimeMinutes(gameDate) {
|
|
35709
|
+
const m = /^(\d+)\.(\d{1,2})\.(\d{1,2})$/.exec(gameDate.trim());
|
|
35710
|
+
if (!m)
|
|
35711
|
+
return null;
|
|
35712
|
+
return Number(m[1]) * 60 + Number(m[2]);
|
|
35713
|
+
}
|
|
35714
|
+
function raceName(editorId) {
|
|
35715
|
+
const base = editorId.replace(/Vampire$/, "").replace(/Race$/, "");
|
|
35716
|
+
return base.replace(/([a-z])([A-Z])/g, "$1 $2") || editorId;
|
|
35717
|
+
}
|
|
35718
|
+
function filetimeToIso(filetime) {
|
|
35719
|
+
if (filetime <= FILETIME_UNIX_EPOCH)
|
|
35720
|
+
return null;
|
|
35721
|
+
const ms = Number((filetime - FILETIME_UNIX_EPOCH) / 10000n);
|
|
35722
|
+
const date = new Date(ms);
|
|
35723
|
+
return Number.isNaN(date.getTime()) ? null : date.toISOString();
|
|
35724
|
+
}
|
|
35725
|
+
// ../../packages/skyrim-save/src/png.ts
|
|
35726
|
+
import { deflateSync } from "zlib";
|
|
35727
|
+
var SIGNATURE = Uint8Array.of(137, 80, 78, 71, 13, 10, 26, 10);
|
|
35728
|
+
function screenshotToPng(shot) {
|
|
35729
|
+
const { width, height, bytesPerPixel, pixels } = shot;
|
|
35730
|
+
if (width === 0 || height === 0)
|
|
35731
|
+
throw new RangeError("the save has no screenshot");
|
|
35732
|
+
const stride = width * bytesPerPixel;
|
|
35733
|
+
const raw = new Uint8Array((stride + 1) * height);
|
|
35734
|
+
for (let y = 0;y < height; y++) {
|
|
35735
|
+
raw.set(pixels.subarray(y * stride, (y + 1) * stride), y * (stride + 1) + 1);
|
|
35736
|
+
}
|
|
35737
|
+
const ihdr = new Uint8Array(13);
|
|
35738
|
+
const v = new DataView(ihdr.buffer);
|
|
35739
|
+
v.setUint32(0, width);
|
|
35740
|
+
v.setUint32(4, height);
|
|
35741
|
+
ihdr[8] = 8;
|
|
35742
|
+
ihdr[9] = bytesPerPixel === 4 ? 6 : 2;
|
|
35743
|
+
return concat([
|
|
35744
|
+
SIGNATURE,
|
|
35745
|
+
chunk("IHDR", ihdr),
|
|
35746
|
+
chunk("IDAT", new Uint8Array(deflateSync(raw))),
|
|
35747
|
+
chunk("IEND", new Uint8Array(0))
|
|
35748
|
+
]);
|
|
35749
|
+
}
|
|
35750
|
+
function chunk(type, data) {
|
|
35751
|
+
const out = new Uint8Array(12 + data.byteLength);
|
|
35752
|
+
const v = new DataView(out.buffer);
|
|
35753
|
+
v.setUint32(0, data.byteLength);
|
|
35754
|
+
for (let i = 0;i < 4; i++)
|
|
35755
|
+
out[4 + i] = type.charCodeAt(i);
|
|
35756
|
+
out.set(data, 8);
|
|
35757
|
+
v.setUint32(8 + data.byteLength, crc32(out.subarray(4, 8 + data.byteLength)));
|
|
35758
|
+
return out;
|
|
35759
|
+
}
|
|
35760
|
+
var table = null;
|
|
35761
|
+
function crc32(bytes) {
|
|
35762
|
+
if (!table) {
|
|
35763
|
+
table = new Uint32Array(256);
|
|
35764
|
+
for (let n = 0;n < 256; n++) {
|
|
35765
|
+
let c = n;
|
|
35766
|
+
for (let k = 0;k < 8; k++)
|
|
35767
|
+
c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
|
|
35768
|
+
table[n] = c >>> 0;
|
|
35769
|
+
}
|
|
35770
|
+
}
|
|
35771
|
+
let crc = 4294967295;
|
|
35772
|
+
for (const b of bytes)
|
|
35773
|
+
crc = table[(crc ^ b) & 255] ^ crc >>> 8;
|
|
35774
|
+
return (crc ^ 4294967295) >>> 0;
|
|
35775
|
+
}
|
|
35776
|
+
function concat(parts) {
|
|
35777
|
+
const out = new Uint8Array(parts.reduce((n, p) => n + p.byteLength, 0));
|
|
35778
|
+
let at = 0;
|
|
35779
|
+
for (const p of parts) {
|
|
35780
|
+
out.set(p, at);
|
|
35781
|
+
at += p.byteLength;
|
|
35782
|
+
}
|
|
35783
|
+
return out;
|
|
35784
|
+
}
|
|
35785
|
+
// ../../packages/skyrim-save/src/save.ts
|
|
35786
|
+
function readSave(bytes) {
|
|
35787
|
+
const header = parseSaveHeader(bytes);
|
|
35788
|
+
let body = null;
|
|
35789
|
+
let bodyError = null;
|
|
35790
|
+
try {
|
|
35791
|
+
body = parseSaveBody(bytes, header);
|
|
35792
|
+
} catch (err) {
|
|
35793
|
+
if (!(err instanceof SaveFormatError))
|
|
35794
|
+
throw err;
|
|
35795
|
+
bodyError = err.message;
|
|
35796
|
+
}
|
|
35797
|
+
const shot = header.screenshot;
|
|
35798
|
+
return {
|
|
35799
|
+
header,
|
|
35800
|
+
body,
|
|
35801
|
+
bodyError,
|
|
35802
|
+
summary: summarize(header, body),
|
|
35803
|
+
screenshotPng: shot.width > 0 && shot.height > 0 ? screenshotToPng(shot) : null
|
|
35804
|
+
};
|
|
35805
|
+
}
|
|
35806
|
+
function summarize(header, body) {
|
|
35807
|
+
const time = body?.gameTime ?? null;
|
|
35808
|
+
return {
|
|
35809
|
+
version: header.version,
|
|
35810
|
+
saveNumber: header.saveNumber,
|
|
35811
|
+
playerName: header.playerName,
|
|
35812
|
+
level: header.level,
|
|
35813
|
+
location: header.location,
|
|
35814
|
+
race: header.race,
|
|
35815
|
+
sex: header.sex,
|
|
35816
|
+
playTimeMinutes: header.playTimeMinutes,
|
|
35817
|
+
savedAt: header.savedAt,
|
|
35818
|
+
gameDate: time ? {
|
|
35819
|
+
text: formatGameDate(time),
|
|
35820
|
+
hour: Math.round(time.hour * 100) / 100,
|
|
35821
|
+
daysPassed: Math.round(time.daysPassed * 100) / 100
|
|
35822
|
+
} : null,
|
|
35823
|
+
plugins: body?.plugins ?? [],
|
|
35824
|
+
lightPlugins: body?.lightPlugins ?? [],
|
|
35825
|
+
stats: Object.fromEntries((body?.stats ?? []).map((s) => [s.name, s.value]))
|
|
35826
|
+
};
|
|
35827
|
+
}
|
|
35828
|
+
// ../../packages/skyrim-save/src/newest.ts
|
|
35829
|
+
import { readdir, stat } from "fs/promises";
|
|
35830
|
+
import { join as join3 } from "path";
|
|
35831
|
+
async function newestSave(dir) {
|
|
35832
|
+
let best = null;
|
|
35833
|
+
for (const name of await readdir(dir)) {
|
|
35834
|
+
if (!name.toLowerCase().endsWith(".ess"))
|
|
35835
|
+
continue;
|
|
35836
|
+
const path = join3(dir, name);
|
|
35837
|
+
const info = await stat(path);
|
|
35838
|
+
if (!info.isFile())
|
|
35839
|
+
continue;
|
|
35840
|
+
if (!best || info.mtimeMs > best.at)
|
|
35841
|
+
best = { path, at: info.mtimeMs };
|
|
35842
|
+
}
|
|
35843
|
+
return best?.path ?? null;
|
|
35844
|
+
}
|
|
35845
|
+
// src/save-sync.ts
|
|
35846
|
+
async function resolveSave(path) {
|
|
35847
|
+
const info = await stat2(path).catch(() => null);
|
|
35848
|
+
if (!info)
|
|
35849
|
+
throw usageError(`no such file or folder: ${path}`);
|
|
35850
|
+
if (!info.isDirectory())
|
|
35851
|
+
return path;
|
|
35852
|
+
const newest = await newestSave(path);
|
|
35853
|
+
if (!newest)
|
|
35854
|
+
throw usageError(`no .ess save in ${path}`);
|
|
35855
|
+
return newest;
|
|
35856
|
+
}
|
|
35857
|
+
async function saveUpload(path) {
|
|
35858
|
+
const save = readSave(new Uint8Array(await readFile(path)));
|
|
35859
|
+
const { version: _version, lightPlugins, plugins, ...summary } = save.summary;
|
|
35860
|
+
return uploadRunSaveHeaderInputSchema.parse({
|
|
35861
|
+
...summary,
|
|
35862
|
+
fileName: basename(path),
|
|
35863
|
+
plugins: [...plugins, ...lightPlugins].slice(0, 600),
|
|
35864
|
+
screenshot: save.screenshotPng ? Buffer.from(save.screenshotPng).toString("base64") : null
|
|
35865
|
+
});
|
|
35866
|
+
}
|
|
35867
|
+
async function watchSaves(opts) {
|
|
35868
|
+
const interval = opts.intervalMs ?? 1e4;
|
|
35869
|
+
const sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
35870
|
+
let seen = null;
|
|
35871
|
+
let sent = null;
|
|
35872
|
+
while (!opts.signal?.aborted) {
|
|
35873
|
+
try {
|
|
35874
|
+
const path = await newestSave(opts.dir);
|
|
35875
|
+
if (path) {
|
|
35876
|
+
const info = await stat2(path);
|
|
35877
|
+
const mark = `${path}|${info.mtimeMs}|${info.size}`;
|
|
35878
|
+
if (mark === seen && mark !== sent) {
|
|
35879
|
+
await opts.onSave(path);
|
|
35880
|
+
sent = mark;
|
|
35881
|
+
}
|
|
35882
|
+
seen = mark;
|
|
35883
|
+
}
|
|
35884
|
+
} catch (err) {
|
|
35885
|
+
opts.onError?.(err);
|
|
35886
|
+
}
|
|
35887
|
+
if (opts.signal?.aborted)
|
|
35888
|
+
break;
|
|
35889
|
+
await sleep(interval);
|
|
35890
|
+
}
|
|
35891
|
+
}
|
|
35892
|
+
|
|
35291
35893
|
// src/today.ts
|
|
35292
35894
|
var OPEN_STATUSES = new Set(["todo", "in_progress"]);
|
|
35293
35895
|
function isTaskLike(value) {
|
|
@@ -35864,7 +36466,7 @@ var HANDLERS = {
|
|
|
35864
36466
|
});
|
|
35865
36467
|
if (out !== undefined) {
|
|
35866
36468
|
for (const file of data.files) {
|
|
35867
|
-
const target =
|
|
36469
|
+
const target = join4(out, file.path);
|
|
35868
36470
|
mkdirSync2(dirname2(target), { recursive: true });
|
|
35869
36471
|
writeFileSync2(target, file.markdown);
|
|
35870
36472
|
}
|
|
@@ -36050,7 +36652,7 @@ var HANDLERS = {
|
|
|
36050
36652
|
} catch (cause) {
|
|
36051
36653
|
throw usageError(`can't read ${path}: ${cause instanceof Error ? cause.message : cause}`);
|
|
36052
36654
|
}
|
|
36053
|
-
const name = flagString(flags, "title") ?? flagString(flags, "name") ??
|
|
36655
|
+
const name = flagString(flags, "title") ?? flagString(flags, "name") ?? basename2(path);
|
|
36054
36656
|
const form = new FormData;
|
|
36055
36657
|
form.append("nodeId", node);
|
|
36056
36658
|
const fields = {
|
|
@@ -36077,7 +36679,7 @@ var HANDLERS = {
|
|
|
36077
36679
|
"files download": async ({ args, flags, requireClient }) => {
|
|
36078
36680
|
const id = requireArg(args, 0, "file id");
|
|
36079
36681
|
const file = await requireClient().getBytes(`/files/${id}/download`);
|
|
36080
|
-
const out = flagString(flags, "out") ??
|
|
36682
|
+
const out = flagString(flags, "out") ?? basename2(file.name ?? id);
|
|
36081
36683
|
writeFileSync2(out, file.bytes);
|
|
36082
36684
|
return {
|
|
36083
36685
|
data: { id, saved: out, size: file.bytes.byteLength, type: file.contentType },
|
|
@@ -36144,7 +36746,7 @@ var HANDLERS = {
|
|
|
36144
36746
|
if (value !== undefined)
|
|
36145
36747
|
form.append(key, value);
|
|
36146
36748
|
}
|
|
36147
|
-
const name =
|
|
36749
|
+
const name = basename2(path);
|
|
36148
36750
|
const type = flagString(flags, "type") ?? Bun.file(name).type;
|
|
36149
36751
|
form.append("file", new File([Uint8Array.from(bytes)], name, { type }), name);
|
|
36150
36752
|
}
|
|
@@ -37501,7 +38103,7 @@ var HANDLERS = {
|
|
|
37501
38103
|
const since = flagString(flags, "since");
|
|
37502
38104
|
if (since !== undefined)
|
|
37503
38105
|
form.append("since", since);
|
|
37504
|
-
const name =
|
|
38106
|
+
const name = basename2(path);
|
|
37505
38107
|
form.append("file", new File([Uint8Array.from(bytes)], name, { type: "text/csv" }), name);
|
|
37506
38108
|
const route = flagBool(flags, "commit") ? "/money/imports" : "/money/imports/preview";
|
|
37507
38109
|
return { data: await requireClient().upload(route, form), render: renderRecord };
|
|
@@ -37705,6 +38307,37 @@ var HANDLERS = {
|
|
|
37705
38307
|
const input = parseInput(applyRunChangesInputSchema, body);
|
|
37706
38308
|
return { data: await requireClient().post(`/runs/${id}/changes`, input), render: renderRecord };
|
|
37707
38309
|
},
|
|
38310
|
+
"runs save-header": async ({ args, flags, io, requireClient }) => {
|
|
38311
|
+
const id = requireArg(args, 0, "run id");
|
|
38312
|
+
const target = requireArg(args, 1, "save file or Saves folder");
|
|
38313
|
+
const dryRun = flagBool(flags, "dry-run");
|
|
38314
|
+
const send = async (path) => {
|
|
38315
|
+
const input = await saveUpload(path);
|
|
38316
|
+
if (dryRun)
|
|
38317
|
+
return { ...input, screenshot: input.screenshot ? "(png)" : null };
|
|
38318
|
+
return requireClient().post(`/runs/${id}/save-header`, input);
|
|
38319
|
+
};
|
|
38320
|
+
if (!flagBool(flags, "watch")) {
|
|
38321
|
+
return { data: await send(await resolveSave(target)), render: renderRecord };
|
|
38322
|
+
}
|
|
38323
|
+
const seconds = Number(flagString(flags, "interval") ?? "10");
|
|
38324
|
+
if (!Number.isFinite(seconds) || seconds < 1)
|
|
38325
|
+
throw usageError("--interval is seconds, 1 or more");
|
|
38326
|
+
io.stderr(`watching ${target} for new saves (every ${seconds}s); Ctrl-C stops
|
|
38327
|
+
`);
|
|
38328
|
+
await watchSaves({
|
|
38329
|
+
dir: target,
|
|
38330
|
+
intervalMs: seconds * 1000,
|
|
38331
|
+
onSave: async (path) => {
|
|
38332
|
+
const result = await send(path);
|
|
38333
|
+
io.stderr(`${result.unchanged ? "already had" : dryRun ? "read" : "sent"} ${basename2(path)}
|
|
38334
|
+
`);
|
|
38335
|
+
},
|
|
38336
|
+
onError: (err) => io.stderr(`save sync: ${err.message}
|
|
38337
|
+
`)
|
|
38338
|
+
});
|
|
38339
|
+
return { data: null, render: () => "" };
|
|
38340
|
+
},
|
|
37708
38341
|
"runs undo": async ({ args, flags, requireClient }) => {
|
|
37709
38342
|
const id = requireArg(args, 0, "run id");
|
|
37710
38343
|
const batch = flagString(flags, "batch");
|
|
@@ -37894,7 +38527,7 @@ var HANDLERS = {
|
|
|
37894
38527
|
} catch (cause) {
|
|
37895
38528
|
throw usageError(`can't read ${path}: ${cause instanceof Error ? cause.message : cause}`);
|
|
37896
38529
|
}
|
|
37897
|
-
const name =
|
|
38530
|
+
const name = basename2(path);
|
|
37898
38531
|
const form = new FormData;
|
|
37899
38532
|
const textFile = flagString(flags, "text-file");
|
|
37900
38533
|
if (textFile)
|
|
@@ -38101,7 +38734,7 @@ var HANDLERS = {
|
|
|
38101
38734
|
form.append("sourceName", fields.sourceName);
|
|
38102
38735
|
if (fields.sourceEmail)
|
|
38103
38736
|
form.append("sourceEmail", fields.sourceEmail);
|
|
38104
|
-
const name =
|
|
38737
|
+
const name = basename2(path);
|
|
38105
38738
|
form.append("file", new File([Uint8Array.from(bytes)], name, { type: "text/plain" }), name);
|
|
38106
38739
|
return { data: await requireClient().upload("/family-tree", form), render: renderRecord };
|
|
38107
38740
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@robbeverhelst/do",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.283.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "do — the agent-drivable CLI over the public /api/v1 surface",
|
|
6
6
|
"homepage": "https://github.com/robbeverhelst/do/tree/main/apps/cli",
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"@do/schema": "workspace:*",
|
|
36
|
+
"@do/skyrim-save": "workspace:*",
|
|
36
37
|
"@types/bun": "^1.4.3",
|
|
37
38
|
"@types/node": "^25.9.3",
|
|
38
39
|
"ulidx": "^2.4.1",
|