@ccmsg/cli 0.11.1 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ccmsg/cli",
3
- "version": "0.11.1",
3
+ "version": "0.11.3",
4
4
  "description": "The ccmsg daemon, CLI and agent plugins for one instance (= one config home)",
5
5
  "license": "MIT",
6
6
  "author": "kawaz",
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
- ["--since <at|uuid>", "下限。時刻 (ISO か epoch ミリ秒) か record の uuid"],
442
- ["--until <at|uuid>", "上限。同上"],
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
- const since = parsed.named.get("since");
1246
- const until = parsed.named.get("until");
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 two kinds it was written in.
1318
+ /** One bound, as whichever of the kinds it was written in.
1307
1319
  *
1308
- * A time and a record id cannot be confused for one another — one parses as a
1309
- * moment and the other does not — so the caller writes what they have rather
1310
- * than saying which it is. */
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
- if (value === undefined || value === "") return {};
1313
- const at = moment(value);
1314
- return at === undefined ? { [`${kind}_uuid`]: value } : { [`${kind}_at`]: 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
- function moment(value: string): number | undefined {
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
- return Number.isNaN(parsed) ? undefined : parsed;
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 {
@@ -201,9 +201,22 @@ const STARTING_PRESETS = [
201
201
  },
202
202
  },
203
203
  {
204
- name: "handoff",
205
- description: "後継セッションへの引き継ぎ。直近の会話と、走っているものの台帳",
206
- opts: { types: ["message", "system.task", "ids"] },
204
+ name: "recover",
205
+ description: "文脈の全回復。since / until で区切った区間の会話・作業・思考・合図と台帳",
206
+ opts: {
207
+ types: [
208
+ "message",
209
+ "tool",
210
+ "thinking",
211
+ "notice",
212
+ "hook",
213
+ "system.compact",
214
+ "system.api.error",
215
+ "system.task",
216
+ "system.attachment.queued_command",
217
+ "ids",
218
+ ],
219
+ },
207
220
  },
208
221
  {
209
222
  name: "audit",