@kairyou/agent-tools 0.12.0 → 0.13.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.
@@ -0,0 +1,137 @@
1
+ // OpenCode adapter for the log capability: translates plugin events into the
2
+ // hook payloads dist/log/hook.mjs consumes, so both agents share one recorder.
3
+ //
4
+ // Verified against a real OpenCode run: chat.message carries the user prompt,
5
+ // tool.execute.after carries absolute file paths in output.metadata.files, and
6
+ // the assistant's final text arrives as message.part.updated parts belonging
7
+ // to messages that message.updated announced as role=assistant.
8
+
9
+ import { spawn } from "node:child_process";
10
+ import path from "node:path";
11
+ import { fileURLToPath } from "node:url";
12
+
13
+ const HOOK = path.join(path.dirname(fileURLToPath(import.meta.url)), "hook.mjs");
14
+
15
+ // Sends run strictly one after another: the hook does an unlocked
16
+ // read-modify-write on the day state, so parallel hook processes from the
17
+ // same adapter would drop each other's events.
18
+ let sendQueue = Promise.resolve();
19
+
20
+ function send(payload) {
21
+ sendQueue = sendQueue.then(
22
+ () =>
23
+ new Promise((resolve) => {
24
+ try {
25
+ // OpenCode is a Bun single binary, so process.execPath points at
26
+ // opencode.exe; the hook needs the node on PATH (required >= 22 anyway).
27
+ const child = spawn("node", [HOOK], {
28
+ stdio: ["pipe", "ignore", "ignore"],
29
+ windowsHide: true,
30
+ });
31
+ child.on("error", () => resolve());
32
+ child.on("exit", () => resolve());
33
+ child.stdin.write(JSON.stringify(payload));
34
+ child.stdin.end();
35
+ } catch {
36
+ // Logging must never break the session.
37
+ resolve();
38
+ }
39
+ })
40
+ );
41
+ }
42
+
43
+ export const AgentToolsLog = async ({ directory, project } = {}) => {
44
+ const cwd = directory || project?.worktree || process.cwd();
45
+ const assistantMessageIds = new Set();
46
+ const lastAssistantText = new Map();
47
+
48
+ return {
49
+ "chat.message": async (_input, output) => {
50
+ const message = output?.message;
51
+ if (message?.role !== "user") return;
52
+ const text = (output?.parts || [])
53
+ .filter((part) => part?.type === "text" && typeof part.text === "string")
54
+ .map((part) => part.text)
55
+ .join("\n");
56
+ if (!text.trim()) return;
57
+ send({
58
+ hook_event_name: "UserPromptSubmit",
59
+ session_id: message.sessionID,
60
+ cwd,
61
+ prompt: text,
62
+ });
63
+ },
64
+ "tool.execute.before": async (input, output) => {
65
+ const tool = String(input?.tool || "");
66
+ if (tool === "bash") return;
67
+ const args = output?.args;
68
+ if (!args || typeof args !== "object") return;
69
+ // Forwarded so the hook can snapshot a baseline for the detailed diff;
70
+ // tools whose args carry no file path (such as apply_patch) are simply
71
+ // skipped there and their diff stays "unknown".
72
+ send({
73
+ hook_event_name: "PreToolUse",
74
+ session_id: input?.sessionID,
75
+ cwd,
76
+ tool_name: tool === "write" ? "Write" : "Edit",
77
+ tool_input: args,
78
+ });
79
+ },
80
+ "tool.execute.after": async (input, output) => {
81
+ const sessionId = input?.sessionID;
82
+ const tool = String(input?.tool || "");
83
+ if (tool === "bash") {
84
+ send({
85
+ hook_event_name: "PostToolUse",
86
+ session_id: sessionId,
87
+ cwd,
88
+ tool_name: "Bash",
89
+ tool_input: { command: String(input?.args?.command || "") },
90
+ });
91
+ return;
92
+ }
93
+ const files = output?.metadata?.files;
94
+ if (!Array.isArray(files)) return;
95
+ for (const file of files) {
96
+ if (!file?.filePath) continue;
97
+ send({
98
+ hook_event_name: "PostToolUse",
99
+ session_id: sessionId,
100
+ cwd,
101
+ tool_name: tool === "write" ? "Write" : "Edit",
102
+ tool_input: { file_path: file.filePath },
103
+ });
104
+ }
105
+ },
106
+ event: async ({ event }) => {
107
+ const type = event?.type;
108
+ const properties = event?.properties || {};
109
+ if (type === "message.updated") {
110
+ if (properties.info?.role === "assistant") assistantMessageIds.add(properties.info.id);
111
+ return;
112
+ }
113
+ if (type === "message.part.updated") {
114
+ const part = properties.part;
115
+ if (part?.type === "text" && assistantMessageIds.has(part.messageID)) {
116
+ lastAssistantText.set(part.sessionID, String(part.text || ""));
117
+ }
118
+ return;
119
+ }
120
+ if (type === "session.idle") {
121
+ send({
122
+ hook_event_name: "Stop",
123
+ session_id: properties.sessionID,
124
+ cwd,
125
+ last_assistant_message: lastAssistantText.get(properties.sessionID) || "",
126
+ });
127
+ // Cleared per turn: a cancelled or text-less next turn must not
128
+ // inherit this turn's summary as its own result.
129
+ lastAssistantText.delete(properties.sessionID);
130
+ return;
131
+ }
132
+ if (type === "session.deleted") {
133
+ lastAssistantText.delete(properties.sessionID);
134
+ }
135
+ },
136
+ };
137
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kairyou/agent-tools",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
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": {
package/scripts/build.mjs CHANGED
@@ -35,6 +35,12 @@ const TARGETS = {
35
35
  cli: path.join(ROOT, "integrations", "vision", "lib", "cli.mjs"),
36
36
  },
37
37
  },
38
+ log: {
39
+ entryPoints: {
40
+ hook: path.join(ROOT, "integrations", "log", "hook.mjs"),
41
+ "opencode-plugin": path.join(ROOT, "integrations", "log", "opencode-plugin.mjs"),
42
+ },
43
+ },
38
44
  };
39
45
 
40
46
  // Repo-shipped usage routes (a fork can commit integrations/usage/routes/*.mjs
@@ -7,19 +7,20 @@
7
7
  // statusline Claude Code statusLine script (claude only).
8
8
  // usage Active API provider quota/balance (all agents).
9
9
  // vision inspect_image MCP server + at-vision skill (all agents).
10
+ // log AI session work log via agent hooks (all agents).
10
11
  //
11
12
  // Standalone commands (dispatched before capability parsing):
12
13
  // inspect-image <path|url> --question "..." Human diagnostic for vision.
13
14
  // mcp-vision Run the vision MCP stdio server.
14
15
  //
15
16
  // Targets:
16
- // claude -> ~/.claude/settings.json (statusLine key)
17
+ // claude -> ~/.claude/settings.json (statusLine key + log hooks)
17
18
  // + ~/.claude/skills/at-usage
18
19
  // ~/.claude.json (vision MCP) + ~/.claude/skills (at-vision)
19
- // codex -> ~/.codex/hooks.json (standalone hooks file)
20
+ // codex -> ~/.codex/hooks.json (usage + log hooks)
20
21
  // + ~/.agents/skills/at-usage
21
22
  // ~/.codex/config.toml (vision MCP) + ~/.agents/skills (at-vision)
22
- // opencode -> ~/.config/opencode/ (server + TUI plugins,
23
+ // opencode -> ~/.config/opencode/ (usage + log plugins,
23
24
  // vision MCP in opencode.json + skills/at-vision)
24
25
  // Runtime scripts are copied into ~/.agent-tools so this installer can be
25
26
  // run via npx from GitHub without requiring a persistent local clone.
@@ -69,6 +70,8 @@ const PACKAGE_VERSION = (() => {
69
70
  // Everything copied into ~/.agent-tools is built output from dist/ (see
70
71
  // scripts/build.mjs); integrations/ holds the sources.
71
72
  const SOURCE = {
73
+ logHook: path.join(REPO_ROOT, "dist", "log", "hook.mjs"),
74
+ logOpencodePlugin: path.join(REPO_ROOT, "dist", "log", "opencode-plugin.mjs"),
72
75
  codexUsageHook: path.join(REPO_ROOT, "dist", "usage", "codex-hook.mjs"),
73
76
  usageScript: path.join(REPO_ROOT, "dist", "usage", "core.mjs"),
74
77
  usageCli: path.join(REPO_ROOT, "dist", "usage", "cli.mjs"),
@@ -78,6 +81,8 @@ const SOURCE = {
78
81
  opencodeUsageTui: path.join(REPO_ROOT, "dist", "usage", "opencode-tui.mjs"),
79
82
  };
80
83
  const RUNTIME = {
84
+ logHook: path.join(INSTALL_ROOT, "dist", "log", "hook.mjs"),
85
+ logOpencodePlugin: path.join(INSTALL_ROOT, "dist", "log", "opencode-plugin.mjs"),
81
86
  codexUsageHook: path.join(INSTALL_ROOT, "dist", "usage", "codex-hook.mjs"),
82
87
  usageScript: path.join(INSTALL_ROOT, "dist", "usage", "core.mjs"),
83
88
  usageCli: path.join(INSTALL_ROOT, "dist", "usage", "cli.mjs"),
@@ -86,12 +91,12 @@ const RUNTIME = {
86
91
  opencodeUsagePlugin: path.join(INSTALL_ROOT, "dist", "usage", "opencode-plugin.mjs"),
87
92
  opencodeUsageTui: path.join(INSTALL_ROOT, "dist", "usage", "opencode-tui.mjs"),
88
93
  };
89
- const ALL_CAPS = ["statusline", "usage", "vision"];
94
+ const ALL_CAPS = ["statusline", "usage", "vision", "log"];
90
95
  const ALL_AGENTS = ["claude", "codex", "opencode"];
91
96
  const AGENT_CAPS = {
92
- claude: ["statusline", "usage", "vision"],
93
- codex: ["usage", "vision"],
94
- opencode: ["usage", "vision"],
97
+ claude: ["statusline", "usage", "vision", "log"],
98
+ codex: ["usage", "vision", "log"],
99
+ opencode: ["usage", "vision", "log"],
95
100
  };
96
101
  const VISION_MCP_NAME = "agent-tools-vision";
97
102
  const VISION_SKILL_NAME = "at-vision";
@@ -233,6 +238,13 @@ function installRuntimeAssets(opts) {
233
238
  addFile(SOURCE.usageScript, RUNTIME.usageScript);
234
239
  addFile(SOURCE.config, RUNTIME.config, { mergeJsonc: true });
235
240
  }
241
+ if (wants(opts, "log")) {
242
+ addFile(SOURCE.logHook, RUNTIME.logHook);
243
+ if (opts.agents.includes("opencode")) {
244
+ addFile(SOURCE.logOpencodePlugin, RUNTIME.logOpencodePlugin);
245
+ }
246
+ addFile(SOURCE.config, RUNTIME.config, { mergeJsonc: true });
247
+ }
236
248
  if (wants(opts, "usage")) {
237
249
  if (opts.agents.includes("codex")) {
238
250
  addFile(SOURCE.codexUsageHook, RUNTIME.codexUsageHook);
@@ -446,6 +458,74 @@ function applyProviderUsage(cfg, { remove }) {
446
458
  if (Object.keys(cfg.hooks).length === 0) delete cfg.hooks;
447
459
  }
448
460
 
461
+ // ---- Log capability: agent hooks running dist/log/hook.mjs, identified by
462
+ // command signature (no meta key, mirroring the codex usage hooks). ----
463
+
464
+ const LOG_EVENTS = [
465
+ ["UserPromptSubmit", ""],
466
+ ["PreToolUse", "Write|Edit|MultiEdit"],
467
+ ["PostToolUse", "Write|Edit|MultiEdit|Bash"],
468
+ ["Stop", ""],
469
+ ];
470
+
471
+ // Exact match against the command this installer writes: anything else, even a
472
+ // user's own .../dist/log/hook.mjs, is not ours and must survive uninstall.
473
+ function isOurLogEntry(entry) {
474
+ const expected = nodeCmd(RUNTIME.logHook);
475
+ return (
476
+ entry &&
477
+ Array.isArray(entry.hooks) &&
478
+ entry.hooks.some((h) => h?.command === expected)
479
+ );
480
+ }
481
+
482
+ // Claude filters tool events via matchers; Codex hooks.json has no matcher
483
+ // concept, so there the hook filters by tool name itself.
484
+ function applyLogHooks(cfg, { remove, matchers }) {
485
+ cfg.hooks = cfg.hooks || {};
486
+ for (const [event, matcher] of LOG_EVENTS) {
487
+ const entries = (cfg.hooks[event] || []).filter((entry) => !isOurLogEntry(entry));
488
+ if (!remove) {
489
+ const entry = {
490
+ hooks: [{ type: "command", command: nodeCmd(RUNTIME.logHook), timeout: 30 }],
491
+ };
492
+ if (matchers && matcher) entry.matcher = matcher;
493
+ entries.push(entry);
494
+ }
495
+ if (entries.length > 0) cfg.hooks[event] = entries;
496
+ else delete cfg.hooks[event];
497
+ }
498
+ if (Object.keys(cfg.hooks).length === 0) delete cfg.hooks;
499
+ }
500
+
501
+ function runClaudeLog(opts) {
502
+ const settings = opts.settings || path.join(os.homedir(), ".claude", "settings.json");
503
+ console.log(`claude log: ${settings}`);
504
+ const cfg = readJson(settings);
505
+ applyLogHooks(cfg, { remove: opts.uninstall, matchers: true });
506
+ writeJson(settings, cfg, opts.dryRun);
507
+ console.log(opts.uninstall ? " - log" : " + log (session work log hooks)");
508
+ }
509
+
510
+ function runCodexLog(opts) {
511
+ const file = opts.codexHooks || path.join(os.homedir(), ".codex", "hooks.json");
512
+ console.log(`codex log: ${file}`);
513
+ const cfg = readJson(file);
514
+ applyLogHooks(cfg, { remove: opts.uninstall, matchers: false });
515
+ if (opts.uninstall && Object.keys(cfg).length === 0 && fs.existsSync(file)) {
516
+ removeFile(file, opts.dryRun);
517
+ } else {
518
+ writeJson(file, cfg, opts.dryRun);
519
+ }
520
+ console.log(opts.uninstall ? " - log" : " + log (session work log hooks)");
521
+ if (!opts.uninstall && !opts.dryRun) {
522
+ console.log(
523
+ " NOTE: Codex will not run this hook until you trust it — run `/hooks` " +
524
+ "inside Codex and approve the agent-tools hooks."
525
+ );
526
+ }
527
+ }
528
+
449
529
  // ---- Vision capability helpers. ----
450
530
 
451
531
  // The installed tree mirrors the package tree, so the bundled runtime lands in
@@ -828,6 +908,7 @@ function runClaudeVision(opts) {
828
908
 
829
909
  function runClaude(opts) {
830
910
  if (wants(opts, "vision")) runClaudeVision(opts);
911
+ if (wants(opts, "log")) runClaudeLog(opts);
831
912
  if (wants(opts, "usage")) {
832
913
  const skillsDir = opts.claudeSkillsDir || path.join(os.homedir(), ".claude", "skills");
833
914
  console.log(`claude usage: ${skillsDir}`);
@@ -891,6 +972,7 @@ function runCodexVision(opts) {
891
972
 
892
973
  function runCodex(opts) {
893
974
  if (wants(opts, "vision")) runCodexVision(opts);
975
+ if (wants(opts, "log")) runCodexLog(opts);
894
976
  if (!wants(opts, "usage")) {
895
977
  return;
896
978
  }
@@ -933,6 +1015,26 @@ function runCodex(opts) {
933
1015
  // usage after a session goes idle. A TUI plugin displays the shared snapshot. ----
934
1016
 
935
1017
  const OPENCODE_STUB_NAME = "agent-tools-usage.js";
1018
+ const OPENCODE_LOG_STUB_NAME = "agent-tools-log.js";
1019
+
1020
+ function runOpencodeLog(opts) {
1021
+ const configDir = opencodeConfigDir(opts);
1022
+ const stub = path.join(configDir, "plugins", OPENCODE_LOG_STUB_NAME);
1023
+ console.log(`opencode log: ${stub}`);
1024
+ if (opts.uninstall) {
1025
+ if (fs.existsSync(stub)) removeFile(stub, opts.dryRun);
1026
+ else console.log(" no agent-tools log plugin found; nothing to remove.");
1027
+ console.log(" - log");
1028
+ return;
1029
+ }
1030
+ const target = pathToFileURL(RUNTIME.logOpencodePlugin).href;
1031
+ const contents =
1032
+ "// Generated by agent-tools installer; do not edit.\n" +
1033
+ `export { AgentToolsLog } from ${JSON.stringify(target)};\n`;
1034
+ writeText(stub, contents, opts.dryRun);
1035
+ console.log(" + log (session work log plugin)");
1036
+ if (!opts.dryRun) console.log(" NOTE: restart opencode to load the agent-tools log plugin.");
1037
+ }
936
1038
 
937
1039
  function opencodeConfigDir(opts) {
938
1040
  return (
@@ -993,6 +1095,7 @@ function runOpencodeVision(opts) {
993
1095
 
994
1096
  function runOpencode(opts) {
995
1097
  if (wants(opts, "vision")) runOpencodeVision(opts);
1098
+ if (wants(opts, "log")) runOpencodeLog(opts);
996
1099
  if (!wants(opts, "usage")) {
997
1100
  return;
998
1101
  }
@@ -54,6 +54,15 @@ their dates are unverifiable, and a project left dirty for weeks would reappear
54
54
  progress every day. User-provided non-code work must remain clearly identified as user
55
55
  context.
56
56
 
57
+ `~/.agent-tools/config.jsonc` may also carry `log.output`, an automatically recorded
58
+ AI session log, separate from `dailyLog.output`. It is a markdown file with dated
59
+ entries, or a directory holding one `<date>.md` report per day; entries under
60
+ `log.projects` may route their sessions to their own `output`, so check those paths
61
+ too. Read the day's content as supplementary evidence, as it captures work that
62
+ produced no commits, such as troubleshooting or research sessions. Merge, do not
63
+ duplicate, work already backed by commits; when the key or the day's content is
64
+ absent, skip this entirely.
65
+
57
66
  ## Output
58
67
 
59
68
  Match the user's language:
@@ -85,7 +94,7 @@ write request, writes after previewing the entry. Resolve the destination in ord
85
94
  3. no destination: return the draft without writing.
86
95
 
87
96
  Read the destination before editing. Wrap each date's generated content in that date's
88
- own markers, `<!-- log:2026-07-31:start 5,a1b2c3d -->` / `<!-- log:2026-07-31:end -->`,
97
+ own markers, `<!-- daily-log:2026-07-31:start 5,a1b2c3d -->` / `<!-- daily-log:2026-07-31:end -->`,
89
98
  where the start marker stores the day's commit count and newest commit hash across the
90
99
  scanned projects; the date line and every line outside the markers belong to the user.
91
100
  Refresh an existing block when either value changed or the user explicitly asks;
@@ -96,9 +105,9 @@ user's lines instead of editing them. Do not duplicate the date. Insert a new da
96
105
  among the existing dated entries at its date-order position, inferring ascending or
97
106
  descending from the dates already present (ascending when that is ambiguous); content
98
107
  above or below the dated entries, such as notes or todo lists, stays where it is. On duplicate or
99
- unpaired markers, stop and propose the edit instead of writing. When Git shows no
100
- activity, leave the file unchanged and say so; the user can add a manual entry
101
- themselves. Recording a range applies these rules to each day's block independently.
108
+ unpaired markers, stop and propose the edit instead of writing. When neither Git nor
109
+ the session log shows activity for the day, leave the file unchanged and say so; the
110
+ user can add a manual entry themselves. Recording a range applies these rules to each day's block independently.
102
111
 
103
112
  Scheduling is separate; set it up only when the user asks. Prefer the OS scheduler
104
113
  (Task Scheduler, cron, launchd) over agent-internal timers, which stop with the agent:
@@ -57,8 +57,13 @@ Git jargon. Do not show commit counts or a generic source line.
57
57
  ## Optional logs
58
58
 
59
59
  A pasted daily/weekly log or explicit file path provides business context. If neither is
60
- supplied, optionally read `dailyLog.output` from config. Explicit conversation input
61
- always wins; a missing log is non-fatal.
60
+ supplied, optionally read `dailyLog.output` and `log.output` (an automatically recorded
61
+ AI session log; a dated markdown file, or a directory of per-day `<date>.md` reports,
62
+ of which read the dates inside the window; entries under `log.projects` may add their
63
+ own `output` paths, check those too) from `~/.agent-tools/config.jsonc`; the
64
+ latter also captures work that never produced commits,
65
+ such as troubleshooting or research sessions. Explicit conversation input always wins;
66
+ a missing key or file is non-fatal.
62
67
 
63
68
  Commit-backed items may be summarized directly. Keep log-only work separate for user
64
69
  confirmation; include it only after confirmation, using only dates stated in the log or