@ccmsg/cli 0.11.2 → 0.11.3
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/package.json +1 -1
- package/src/cli.ts +68 -13
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -438,8 +438,11 @@ const ROOT: Command = {
|
|
|
438
438
|
options: [
|
|
439
439
|
["--preset <名前>", "instance が持つ選択 (ccmsg dump presets で一覧)"],
|
|
440
440
|
["--types <選択>", "型をカンマ区切りで。prefix 可、-で除外、@名前で preset 展開"],
|
|
441
|
-
[
|
|
442
|
-
|
|
441
|
+
[
|
|
442
|
+
"--since <at|ago|uuid>",
|
|
443
|
+
"下限。時刻 (ISO か epoch ミリ秒)、今からの差 (-10m / -2h / -1d / -30s)、record の uuid",
|
|
444
|
+
],
|
|
445
|
+
["--until <at|ago|uuid>", "上限。同上"],
|
|
443
446
|
["--max-chars <n>", "1 アイテムの本文をこの文字数で切る (既定は切らない)"],
|
|
444
447
|
["--json", "markdown ではなく dump file の中身をそのまま出す"],
|
|
445
448
|
["--out <path>", "標準出力ではなくこの path に書く"],
|
|
@@ -1242,8 +1245,10 @@ async function dump(args: readonly string[]): Promise<unknown> {
|
|
|
1242
1245
|
...dumpArgs(subject, parsed.named),
|
|
1243
1246
|
})) as unknown as SessionDumpWriteResult;
|
|
1244
1247
|
const body = readFileSync(written.path, "utf8");
|
|
1245
|
-
|
|
1246
|
-
|
|
1248
|
+
// What the heading states is where the cut fell, not the words it was asked
|
|
1249
|
+
// for in: a dump read next week cannot work out what "10 minutes ago" was.
|
|
1250
|
+
const since = spelled(resolved("since", parsed.named.get("since")));
|
|
1251
|
+
const until = spelled(resolved("until", parsed.named.get("until")));
|
|
1247
1252
|
const limit = parsed.named.get("max-chars");
|
|
1248
1253
|
const text = parsed.flags.has("json")
|
|
1249
1254
|
? body
|
|
@@ -1301,23 +1306,73 @@ export function dumpArgs(
|
|
|
1301
1306
|
};
|
|
1302
1307
|
}
|
|
1303
1308
|
|
|
1309
|
+
/** One bound as the heading writes it: a moment in the spelling everything
|
|
1310
|
+
* else states an instant in, and a record id as it was given. */
|
|
1311
|
+
function spelled(bound: number | string | undefined): string | undefined {
|
|
1312
|
+
if (bound === undefined) return undefined;
|
|
1313
|
+
return typeof bound === "number" ? new Date(bound).toISOString() : bound;
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1304
1316
|
const AGENT_MARK = "/agent-";
|
|
1305
1317
|
|
|
1306
|
-
/** One bound, as whichever of the
|
|
1318
|
+
/** One bound, as whichever of the kinds it was written in.
|
|
1307
1319
|
*
|
|
1308
|
-
* A
|
|
1309
|
-
*
|
|
1310
|
-
*
|
|
1320
|
+
* A moment and a record id cannot be confused for one another — one reads as a
|
|
1321
|
+
* time and the other does not — so the caller writes what they have rather than
|
|
1322
|
+
* saying which it is. What reads as neither ends the command: a bound nobody
|
|
1323
|
+
* could act on would otherwise be dropped, and a dump asked for "the last ten
|
|
1324
|
+
* minutes" would come back empty with nothing saying why. */
|
|
1311
1325
|
function bound(kind: "since" | "until", value: string | undefined): Record<string, unknown> {
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
return at ===
|
|
1326
|
+
const at = resolved(kind, value);
|
|
1327
|
+
if (at === undefined) return {};
|
|
1328
|
+
return typeof at === "number" ? { [`${kind}_at`]: at } : { [`${kind}_uuid`]: at };
|
|
1315
1329
|
}
|
|
1316
1330
|
|
|
1317
|
-
|
|
1331
|
+
/** A record id, as the harness writes one: the whole uuid.
|
|
1332
|
+
*
|
|
1333
|
+
* Checked rather than assumed, because "not a time" is what a typo looks like
|
|
1334
|
+
* too — and because the id a heading shows is shortened to its first bytes,
|
|
1335
|
+
* which is what a person copies. The instance matches a bound against the
|
|
1336
|
+
* record's own uuid exactly, so a shortened one would cut nothing and answer
|
|
1337
|
+
* with an empty dump. */
|
|
1338
|
+
const RECORD_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
1339
|
+
|
|
1340
|
+
/** How far back from now a bound may be written: a count and a unit, with the
|
|
1341
|
+
* sign that says it is in the past.
|
|
1342
|
+
*
|
|
1343
|
+
* Only backwards, because what a person asks a transcript for is a stretch
|
|
1344
|
+
* that has already happened. A bound in the future would name a moment nothing
|
|
1345
|
+
* has reached, which is an empty dump asked for in a roundabout way. */
|
|
1346
|
+
const AGO = /^-(\d+)(s|m|h|d)$/;
|
|
1347
|
+
|
|
1348
|
+
const SPANS: Readonly<Record<string, number>> = {
|
|
1349
|
+
s: 1_000,
|
|
1350
|
+
m: 60_000,
|
|
1351
|
+
h: 3_600_000,
|
|
1352
|
+
d: 86_400_000,
|
|
1353
|
+
};
|
|
1354
|
+
|
|
1355
|
+
/** What one bound turns out to be: a moment, a record id, or nothing written.
|
|
1356
|
+
*
|
|
1357
|
+
* The clock is read here, at the moment the command is given, so `-10m` is ten
|
|
1358
|
+
* minutes before a person typed it rather than before anything the instance
|
|
1359
|
+
* later does. */
|
|
1360
|
+
function resolved(
|
|
1361
|
+
kind: "since" | "until",
|
|
1362
|
+
value: string | undefined,
|
|
1363
|
+
now: number = Date.now(),
|
|
1364
|
+
): number | string | undefined {
|
|
1365
|
+
if (value === undefined || value === "") return undefined;
|
|
1366
|
+
const ago = AGO.exec(value);
|
|
1367
|
+
if (ago !== null) return now - Number(ago[1]) * (SPANS[ago[2] as string] as number);
|
|
1318
1368
|
if (/^\d+$/.test(value)) return Number(value);
|
|
1319
1369
|
const parsed = Date.parse(value);
|
|
1320
|
-
|
|
1370
|
+
if (!Number.isNaN(parsed)) return parsed;
|
|
1371
|
+
if (RECORD_ID.test(value)) return value;
|
|
1372
|
+
throw new CommandError(
|
|
1373
|
+
"invalid_args",
|
|
1374
|
+
`--${kind} は時刻 (ISO か epoch ミリ秒)、今からの差 (-10m / -2h / -1d / -30s)、または record の uuid (短縮形でなく全体) です: ${value}`,
|
|
1375
|
+
);
|
|
1321
1376
|
}
|
|
1322
1377
|
|
|
1323
1378
|
function chars(value: string): number {
|