@kairyou/agent-tools 0.13.3 → 0.13.5

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/log/hook.mjs CHANGED
@@ -1251,30 +1251,46 @@ async function updateDailyFile(outputFile, day, items) {
1251
1251
  (item, index) => ` ${index + 1}. ${item.project ? `${item.project}: ` : ""}${item.text}`
1252
1252
  );
1253
1253
  const block = [`<!-- log:${day}:start -->`, ...lines, `<!-- log:${day}:end -->`];
1254
- let current = "";
1254
+ let raw = "";
1255
1255
  try {
1256
- current = await fs.readFile(outputFile, "utf8");
1256
+ raw = await fs.readFile(outputFile, "utf8");
1257
1257
  } catch {
1258
1258
  }
1259
+ const hadBom = raw.charCodeAt(0) === 65279;
1260
+ const current = hadBom ? raw.slice(1) : raw;
1259
1261
  const eol = current.includes("\r\n") ? "\r\n" : "\n";
1260
1262
  const fileLines = current ? current.split(/\r?\n/) : [];
1261
1263
  const startMarker = `<!-- log:${day}:start -->`;
1262
1264
  const endMarker = `<!-- log:${day}:end -->`;
1263
- const startIndex = fileLines.findIndex((line) => line.trim() === startMarker);
1264
- const endIndex = fileLines.findIndex((line) => line.trim() === endMarker);
1265
+ const startIndex = fileLines.findLastIndex((line) => line.trim() === startMarker);
1266
+ const endIndex = fileLines.findIndex(
1267
+ (line, index) => index > startIndex && line.trim() === endMarker
1268
+ );
1265
1269
  let nextLines;
1266
1270
  if (startIndex !== -1 && endIndex > startIndex) {
1267
1271
  nextLines = [...fileLines.slice(0, startIndex), ...block, ...fileLines.slice(endIndex + 1)];
1268
- } else if (startIndex !== -1 || endIndex !== -1) {
1269
- console.error(`[agent-tools log] Unpaired markers for ${day} in ${outputFile}; skipped.`);
1270
- return;
1271
1272
  } else {
1272
1273
  nextLines = insertDatedBlock(fileLines, day, block);
1273
1274
  }
1274
- const text = nextLines.join(eol).replace(/(\r?\n)*$/, eol);
1275
+ const body = nextLines.join(eol).replace(/(\r?\n)*$/, eol);
1276
+ const text = hadBom ? "\uFEFF" + body : body;
1275
1277
  await fs.mkdir(path.dirname(outputFile), { recursive: true });
1276
1278
  await writeFileAtomic(outputFile, text);
1277
1279
  }
1280
+ var ENTRY_MARKER_RE = /^<!--\s*(?:daily-)?log:\d{4}-\d{2}-\d{2}:(?:start|end)\b/;
1281
+ function datedSectionEnd(fileLines, dateIndex) {
1282
+ let end = dateIndex + 1;
1283
+ for (let i = dateIndex + 1; i < fileLines.length; i += 1) {
1284
+ const line = fileLines[i];
1285
+ if (!line.trim()) continue;
1286
+ if (ENTRY_MARKER_RE.test(line.trim()) || /^\s/.test(line)) {
1287
+ end = i + 1;
1288
+ continue;
1289
+ }
1290
+ break;
1291
+ }
1292
+ return end;
1293
+ }
1278
1294
  function insertDatedBlock(fileLines, day, block) {
1279
1295
  const dateLineRe = /^\+ (\d{4}-\d{2}-\d{2})\s*$/;
1280
1296
  const dates = [];
@@ -1284,22 +1300,14 @@ function insertDatedBlock(fileLines, day, block) {
1284
1300
  }
1285
1301
  const existing = dates.find((entry) => entry.date === day);
1286
1302
  if (existing) {
1287
- const boundaryRe = /^(\+ \d{4}-\d{2}-\d{2}\s*$|#{1,6}\s)/;
1288
- let insertAt2 = fileLines.length;
1289
- for (let i = existing.index + 1; i < fileLines.length; i += 1) {
1290
- if (boundaryRe.test(fileLines[i])) {
1291
- insertAt2 = i;
1292
- break;
1293
- }
1294
- }
1295
- while (insertAt2 - 1 > existing.index && fileLines[insertAt2 - 1].trim() === "") insertAt2 -= 1;
1303
+ const insertAt2 = datedSectionEnd(fileLines, existing.index);
1296
1304
  const trailing = fileLines.slice(insertAt2);
1297
1305
  const inserted = [...block];
1298
1306
  if (trailing.length > 0 && trailing[0].trim() !== "") inserted.push("");
1299
1307
  return [...fileLines.slice(0, insertAt2), ...inserted, ...trailing];
1300
1308
  }
1301
1309
  const ascending = dates.length < 2 || dates[0].date <= dates[dates.length - 1].date;
1302
- let insertAt = fileLines.length;
1310
+ let insertAt = dates.length > 0 ? datedSectionEnd(fileLines, dates[dates.length - 1].index) : fileLines.length;
1303
1311
  for (const entry of dates) {
1304
1312
  if (ascending ? entry.date > day : entry.date < day) {
1305
1313
  insertAt = entry.index;
@@ -520,36 +520,63 @@ async function updateDailyFile(outputFile, day, items) {
520
520
  );
521
521
  const block = [`<!-- log:${day}:start -->`, ...lines, `<!-- log:${day}:end -->`];
522
522
 
523
- let current = "";
523
+ let raw = "";
524
524
  try {
525
- current = await fs.readFile(outputFile, "utf8");
525
+ raw = await fs.readFile(outputFile, "utf8");
526
526
  } catch {
527
527
  // First write creates the file.
528
528
  }
529
+ // Windows editors and PowerShell write a BOM; strip it for parsing and put
530
+ // it back on write so the file keeps the encoding its author chose.
531
+ const hadBom = raw.charCodeAt(0) === 0xfeff;
532
+ const current = hadBom ? raw.slice(1) : raw;
529
533
  const eol = current.includes("\r\n") ? "\r\n" : "\n";
530
534
  const fileLines = current ? current.split(/\r?\n/) : [];
531
535
 
532
536
  const startMarker = `<!-- log:${day}:start -->`;
533
537
  const endMarker = `<!-- log:${day}:end -->`;
534
- const startIndex = fileLines.findIndex((line) => line.trim() === startMarker);
535
- const endIndex = fileLines.findIndex((line) => line.trim() === endMarker);
538
+ // Pair with the LAST start: an earlier damaged start must not swallow
539
+ // whatever the user wrote between it and our block.
540
+ const startIndex = fileLines.findLastIndex((line) => line.trim() === startMarker);
541
+ const endIndex = fileLines.findIndex(
542
+ (line, index) => index > startIndex && line.trim() === endMarker
543
+ );
536
544
 
537
545
  let nextLines;
538
546
  if (startIndex !== -1 && endIndex > startIndex) {
539
547
  nextLines = [...fileLines.slice(0, startIndex), ...block, ...fileLines.slice(endIndex + 1)];
540
- } else if (startIndex !== -1 || endIndex !== -1) {
541
- // Unpaired markers: refuse to guess an edit range in an unattended run.
542
- console.error(`[agent-tools log] Unpaired markers for ${day} in ${outputFile}; skipped.`);
543
- return;
544
548
  } else {
549
+ // Also covers an unpaired marker: never guess its range, just add an
550
+ // intact block. Recording must not stop because one block got damaged;
551
+ // the stray marker stays visible for the user to clean up.
545
552
  nextLines = insertDatedBlock(fileLines, day, block);
546
553
  }
547
554
 
548
- const text = nextLines.join(eol).replace(/(\r?\n)*$/, eol);
555
+ const body = nextLines.join(eol).replace(/(\r?\n)*$/, eol);
556
+ const text = hadBom ? "" + body : body;
549
557
  await fs.mkdir(path.dirname(outputFile), { recursive: true });
550
558
  await writeFileAtomic(outputFile, text);
551
559
  }
552
560
 
561
+ // Where a dated entry stops: markers and indented items belong to it, anything
562
+ // else below (headings, comment blocks, separators, todo lists) does not. Both
563
+ // marker namespaces count, since at-daily-log may share this file.
564
+ const ENTRY_MARKER_RE = /^<!--\s*(?:daily-)?log:\d{4}-\d{2}-\d{2}:(?:start|end)\b/;
565
+
566
+ function datedSectionEnd(fileLines, dateIndex) {
567
+ let end = dateIndex + 1;
568
+ for (let i = dateIndex + 1; i < fileLines.length; i += 1) {
569
+ const line = fileLines[i];
570
+ if (!line.trim()) continue;
571
+ if (ENTRY_MARKER_RE.test(line.trim()) || /^\s/.test(line)) {
572
+ end = i + 1;
573
+ continue;
574
+ }
575
+ break;
576
+ }
577
+ return end;
578
+ }
579
+
553
580
  function insertDatedBlock(fileLines, day, block) {
554
581
  const dateLineRe = /^\+ (\d{4}-\d{2}-\d{2})\s*$/;
555
582
  const dates = [];
@@ -560,18 +587,10 @@ function insertDatedBlock(fileLines, day, block) {
560
587
 
561
588
  const existing = dates.find((entry) => entry.date === day);
562
589
  if (existing) {
563
- // Date line exists without markers (user-written): append the block below
564
- // that date's lines. The dated section ends at the next date line or the
565
- // next heading (notes and todo lists after the entries stay untouched).
566
- const boundaryRe = /^(\+ \d{4}-\d{2}-\d{2}\s*$|#{1,6}\s)/;
567
- let insertAt = fileLines.length;
568
- for (let i = existing.index + 1; i < fileLines.length; i += 1) {
569
- if (boundaryRe.test(fileLines[i])) {
570
- insertAt = i;
571
- break;
572
- }
573
- }
574
- while (insertAt - 1 > existing.index && fileLines[insertAt - 1].trim() === "") insertAt -= 1;
590
+ // The date exists but carries no block of ours yet (user-written, or only
591
+ // at-daily-log's). Append below that date's own lines, never past the
592
+ // notes and todo lists that live further down the file.
593
+ const insertAt = datedSectionEnd(fileLines, existing.index);
575
594
  const trailing = fileLines.slice(insertAt);
576
595
  const inserted = [...block];
577
596
  if (trailing.length > 0 && trailing[0].trim() !== "") inserted.push("");
@@ -579,8 +598,12 @@ function insertDatedBlock(fileLines, day, block) {
579
598
  }
580
599
 
581
600
  // Insert a new date at its date-order position; ascending when ambiguous.
601
+ // Falling off the end means this date sorts last, so it goes at the end of
602
+ // the dated section rather than the end of the file: notes, comment blocks
603
+ // and todo lists kept below the log must stay below it.
582
604
  const ascending = dates.length < 2 || dates[0].date <= dates[dates.length - 1].date;
583
- let insertAt = fileLines.length;
605
+ let insertAt =
606
+ dates.length > 0 ? datedSectionEnd(fileLines, dates[dates.length - 1].index) : fileLines.length;
584
607
  for (const entry of dates) {
585
608
  if (ascending ? entry.date > day : entry.date < day) {
586
609
  insertAt = entry.index;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kairyou/agent-tools",
3
- "version": "0.13.3",
3
+ "version": "0.13.5",
4
4
  "description": "Reusable Agent Skills, plus integrations (statusline, provider usage, vision) that install into Codex, Claude Code, and opencode.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -26,13 +26,23 @@ Resolve paths to Git roots and deduplicate them. Report invalid paths; a non-Git
26
26
  directory does not block other valid projects. Do not clone remote URLs without consent.
27
27
 
28
28
  For each repository, resolve the author with `git -C <root> config user.name`; never
29
- infer aliases or use the remote login as the author. Filter commits by comparing `%an`
30
- literally, not via an `--author` regex. Inspect all local branches, current
31
- HEAD, user-named branches, and configured upstreams. Unless the user requests local-only
32
- data, refresh only the configured upstream branches, grouped into one best-effort
33
- `git fetch --no-tags <remote> <branch...>` per involved remote. Fetch failure is
34
- non-fatal. Do not change the working tree or local branch history. Deduplicate commits
35
- by hash.
29
+ infer aliases or use the remote login as the author.
30
+
31
+ Inspect all local branches, current HEAD, user-named branches, and configured
32
+ upstreams. Unless the user requests local-only data, refresh only the configured
33
+ upstream branches, grouped into one best-effort `git fetch --no-tags <remote>
34
+ <branch...>` per involved remote. Fetch failure is non-fatal. Do not change the
35
+ working tree or local branch history.
36
+
37
+ Then list candidates with `git -C <root> log --no-merges --since=<from>
38
+ --until=<to+1day> --format=%H%x09%an --branches HEAD <upstream refs>`, taking the
39
+ upstream refs from `git -C <root> for-each-ref --format=%(upstream) refs/heads/`.
40
+ Passing no ref walks only the current branch, so work on an unmerged branch reads as
41
+ no activity; `--all` reaches past the scope above into remote branches nobody tracks.
42
+ Keep the rows whose `%an` equals the resolved name exactly. Never pass `--author`: it
43
+ matches the whole `Name <email>` header, so anchored patterns silently match nothing.
44
+ When the window holds commits but none carry that name, report the names actually
45
+ found instead. Deduplicate commits by hash.
36
46
 
37
47
  ## Build work items
38
48
 
@@ -87,7 +97,9 @@ first, and skip empty days.
87
97
  ## Draft or record
88
98
 
89
99
  "Generate" / `生成日报` returns a draft. "Record" / `记录日报`, or an explicit
90
- write request, writes after previewing the entry. Resolve the destination in order:
100
+ write request, writes after previewing the entry. A draft may close with one short
101
+ line offering to record it; treat any affirmative reply to that offer as the go-ahead,
102
+ and keep the suggested word short (`记录` / `record`). Resolve the destination in order:
91
103
 
92
104
  1. a path supplied in conversation;
93
105
  2. optional `dailyLog.output` in `~/.agent-tools/config.jsonc`;
@@ -103,17 +115,19 @@ the current block and the regenerated one before writing, then replace only that
103
115
  date's block. If the date exists without markers, append a marked block below the
104
116
  user's lines instead of editing them. Do not duplicate the date. Insert a new date
105
117
  among the existing dated entries at its date-order position, inferring ascending or
106
- descending from the dates already present (ascending when that is ambiguous); content
107
- above or below the dated entries, such as notes or todo lists, stays where it is. On duplicate or
118
+ descending from the dates already present (ascending when that is ambiguous). A date
119
+ that sorts last goes right after the final dated entry, not at the end of the file:
120
+ notes, comment blocks and todo lists kept below the log stay below it. On duplicate or
108
121
  unpaired markers, stop and propose the edit instead of writing. When neither Git nor
109
122
  the session log shows activity for the day, leave the file unchanged and say so; the
110
123
  user can add a manual entry themselves. Recording a range applies these rules to each day's block independently.
111
124
 
112
125
  Scheduling is separate; set it up only when the user asks. Prefer the OS scheduler
113
126
  (Task Scheduler, cron, launchd) over agent-internal timers, which stop with the agent:
114
- schedule a headless run of the CLI this skill is executing in, invoking the skill with
115
- the recording request the user asked for (in Claude Code,
116
- `claude -p "/at-daily-log record the log"`; other CLIs have their own headless form).
127
+ schedule a headless run of the CLI this skill is executing in `claude -p <prompt>`,
128
+ `codex exec <prompt>`, `opencode run <prompt>` invoking the skill with the recording
129
+ request the user asked for, as in `claude -p "/at-daily-log record the log"`. How a
130
+ skill is addressed varies per CLI, so the test run below is what confirms it.
117
131
  Keep projects and output in config so the command stays stable. Before registering,
118
132
  confirm the schedule, make sure the output file is resolvable, and run the exact
119
133
  command once; register only after that test run records correctly, and show how to
@@ -27,8 +27,15 @@ Always exclude merge commits.
27
27
 
28
28
  Use an explicit author when supplied; otherwise resolve it independently per repository
29
29
  with `git -C <root> config user.name`. Never infer aliases or use the remote login as
30
- the author. Filter commits by comparing `%an` literally, not via an `--author` regex. If no commits match, report that and suggest listing known commit authors;
31
- do not try spelling or language variants. When results exist, mention once that the
30
+ the author. List candidates with `git -C <root> log --no-merges --since=<from>
31
+ --until=<to+1day> --format=%H%x09%an --branches HEAD <upstream refs>`, taking the
32
+ upstream refs from `git -C <root> for-each-ref --format=%(upstream) refs/heads/`;
33
+ passing no ref walks only the current branch, so work on an unmerged branch reads as
34
+ no activity, while `--all` reaches into remote branches nobody tracks. Keep the rows
35
+ whose `%an` equals that name exactly. Never pass `--author`: it matches the whole
36
+ `Name <email>` header, so anchored patterns silently match nothing. If no commits
37
+ match, report that together with the author names the window actually holds; do not
38
+ try spelling or language variants. When results exist, mention once that the
32
39
  user can provide other author names if needed.
33
40
 
34
41
  ## Project evidence