agent-tail-core 0.3.9 → 0.4.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/README.md CHANGED
@@ -14,6 +14,8 @@ Each service gets its own log file (`fe.log`, `api.log`) plus a `combined.log` w
14
14
 
15
15
  ```bash
16
16
  tail -f tmp/logs/latest/combined.log
17
+ # or
18
+ agent-tail tail combined -f
17
19
  ```
18
20
 
19
21
  ## CLI commands
@@ -21,6 +23,7 @@ tail -f tmp/logs/latest/combined.log
21
23
  - **`agent-tail run`** — spawn services with unified logging (recommended)
22
24
  - **`agent-tail init`** — create a session directory
23
25
  - **`agent-tail wrap`** — pipe a single command into an existing session
26
+ - **`agent-tail tail`** — resolve the latest session, then forward to system `tail`
24
27
 
25
28
  ## Options
26
29
 
package/dist/cli.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { n as cmd_run, r as cmd_wrap, t as cmd_init } from "./commands-Bw4eTFz0.mjs";
2
+ import { i as cmd_wrap, n as cmd_run, r as cmd_tail, t as cmd_init } from "./commands-Bv44Rf77.mjs";
3
3
  import { parseArgs } from "node:util";
4
4
 
5
5
  //#region src/cli.ts
@@ -10,8 +10,9 @@ const HELP = `
10
10
  agent-tail init Create a new log session
11
11
  agent-tail wrap <name> -- <command...> Wrap a command, pipe output to <name>.log
12
12
  agent-tail run <config...> Run multiple services concurrently
13
+ agent-tail tail [query] [--] [tail args...] Tail the latest session's logs via system tail
13
14
 
14
- \x1b[1mOptions:\x1b[0m
15
+ \x1b[1mShared Options:\x1b[0m
15
16
  --log-dir <dir> Log directory relative to cwd (default: tmp/logs)
16
17
  --max-sessions <n> Max sessions to keep (default: 10)
17
18
  --no-combined Don't write to combined.log
@@ -19,11 +20,18 @@ const HELP = `
19
20
  --mute <name> Mute a service from terminal and combined.log (repeatable, still logs to <name>.log)
20
21
  -h, --help Show this help
21
22
 
23
+ \x1b[1mTail:\x1b[0m
24
+ agent-tail tail Tail every .log file in the latest session
25
+ agent-tail tail <log> Tail a specific log by exact or partial name
26
+ tail args Forwarded directly to system tail (for example: -f, -n 50)
27
+
22
28
  \x1b[1mExamples:\x1b[0m
23
29
  agent-tail init
24
30
  agent-tail wrap api -- uv run fastapi-server
25
31
  agent-tail wrap worker -- python -m celery worker
26
32
  agent-tail run "fe: npm run dev" "api: uv run server" "worker: uv run worker"
33
+ agent-tail tail -f
34
+ agent-tail tail browser -n 50
27
35
  `;
28
36
  function parse_cli_options(args) {
29
37
  const dash_index = args.indexOf("--");
@@ -77,6 +85,45 @@ function parse_cli_options(args) {
77
85
  rest
78
86
  };
79
87
  }
88
+ function parse_tail_options(args) {
89
+ let log_dir = "tmp/logs";
90
+ let query;
91
+ let i = 0;
92
+ while (i < args.length) {
93
+ const arg = args[i];
94
+ if (arg === "--") {
95
+ i += 1;
96
+ break;
97
+ }
98
+ if (!arg.startsWith("-") || arg === "-") break;
99
+ switch (arg) {
100
+ case "--log-dir":
101
+ log_dir = args[i + 1] ?? "";
102
+ if (!log_dir) throw new Error(`Missing value for ${arg}`);
103
+ i += 2;
104
+ continue;
105
+ case "--help":
106
+ case "-h":
107
+ console.log(HELP);
108
+ process.exit(0);
109
+ default: return {
110
+ log_dir,
111
+ query,
112
+ tail_args: args.slice(i)
113
+ };
114
+ }
115
+ }
116
+ if (args[i] && !args[i].startsWith("-")) {
117
+ query = args[i];
118
+ i += 1;
119
+ }
120
+ if (args[i] === "--") i += 1;
121
+ return {
122
+ log_dir,
123
+ query,
124
+ tail_args: args.slice(i)
125
+ };
126
+ }
80
127
  async function main() {
81
128
  const args = process.argv.slice(2);
82
129
  if (args.length === 0 || args[0] === "-h" || args[0] === "--help") {
@@ -84,24 +131,33 @@ async function main() {
84
131
  process.exit(0);
85
132
  }
86
133
  const subcommand = args[0];
87
- const { options, positionals, rest } = parse_cli_options(args.slice(1));
134
+ const sub_args = args.slice(1);
88
135
  const project_root = process.cwd();
89
136
  try {
90
137
  switch (subcommand) {
91
138
  case "init": {
139
+ const { options } = parse_cli_options(sub_args);
92
140
  const session_dir = cmd_init(project_root, options);
93
141
  console.log(session_dir);
94
142
  break;
95
143
  }
96
144
  case "wrap": {
145
+ const { options, positionals, rest } = parse_cli_options(sub_args);
97
146
  const code = await cmd_wrap(project_root, positionals[0], rest, options);
98
147
  process.exit(code);
99
148
  break;
100
149
  }
101
- case "run":
150
+ case "run": {
151
+ const { options, positionals } = parse_cli_options(sub_args);
102
152
  await cmd_run(project_root, positionals, options);
103
153
  process.exit(0);
104
154
  break;
155
+ }
156
+ case "tail": {
157
+ const code = await cmd_tail(project_root, parse_tail_options(sub_args));
158
+ process.exit(code);
159
+ break;
160
+ }
105
161
  default:
106
162
  console.error(`\x1b[36m[agent-tail]\x1b[0m Unknown command: ${subcommand}`);
107
163
  console.log(HELP);
@@ -86,6 +86,15 @@ var LogManager = class {
86
86
  * points to a valid directory, return it. Otherwise create a new session.
87
87
  */
88
88
  resolve_session(project_root) {
89
+ const existing = this.find_session(project_root);
90
+ if (existing) return existing;
91
+ const log_path = this.initialize(project_root);
92
+ return path.dirname(log_path);
93
+ }
94
+ /**
95
+ * Find the current session directory if one already exists.
96
+ */
97
+ find_session(project_root) {
89
98
  const log_dir = path.resolve(project_root, this.options.logDir);
90
99
  const latest_link = path.join(log_dir, "latest");
91
100
  try {
@@ -93,11 +102,10 @@ var LogManager = class {
93
102
  let target;
94
103
  if (stat.isSymbolicLink()) target = fs.realpathSync(latest_link);
95
104
  else if (stat.isFile()) target = fs.readFileSync(latest_link, "utf-8").trim();
96
- else throw new Error("unexpected latest type");
105
+ else return null;
97
106
  if (fs.existsSync(target) && fs.statSync(target).isDirectory()) return target;
98
107
  } catch {}
99
- const log_path = this.initialize(project_root);
100
- return path.dirname(log_path);
108
+ return null;
101
109
  }
102
110
  check_gitignore(project_root) {
103
111
  const gitignore_path = path.join(project_root, ".gitignore");
@@ -178,6 +186,42 @@ function cmd_init(project_root, options = DEFAULT_CLI_OPTIONS) {
178
186
  function resolve_session_dir(project_root, options = DEFAULT_CLI_OPTIONS) {
179
187
  return create_manager(options).resolve_session(project_root);
180
188
  }
189
+ function find_session_dir(project_root, log_dir = DEFAULT_CLI_OPTIONS.log_dir) {
190
+ return create_manager({
191
+ ...DEFAULT_CLI_OPTIONS,
192
+ log_dir
193
+ }).find_session(project_root);
194
+ }
195
+ function forward_signals_to_children(children) {
196
+ const signal_handlers = ["SIGINT", "SIGTERM"].map((signal) => {
197
+ const handler = () => {
198
+ for (const child of children) child.kill(signal);
199
+ };
200
+ process.on(signal, handler);
201
+ return [signal, handler];
202
+ });
203
+ return () => {
204
+ for (const [signal, handler] of signal_handlers) process.off(signal, handler);
205
+ };
206
+ }
207
+ function resolve_tail_paths(project_root, options) {
208
+ const session_dir = find_session_dir(project_root, options.log_dir);
209
+ const resolved_log_dir = path.resolve(project_root, options.log_dir);
210
+ if (!session_dir) throw new Error(`No log session found in ${resolved_log_dir}. Run "agent-tail run", "agent-tail wrap", or start a framework plugin first.`);
211
+ const log_files = fs.readdirSync(session_dir, { withFileTypes: true }).filter((entry) => entry.isFile() && entry.name.endsWith(".log")).map((entry) => entry.name).sort((a, b) => a.localeCompare(b));
212
+ if (log_files.length === 0) throw new Error(`No .log files found in ${session_dir}.`);
213
+ if (!options.query) return log_files.map((file_name) => path.join(session_dir, file_name));
214
+ const normalized_query = options.query.toLowerCase();
215
+ const exact_names = new Set([normalized_query, normalized_query.endsWith(".log") ? normalized_query : `${normalized_query}.log`]);
216
+ const exact_matches = log_files.filter((file_name) => exact_names.has(file_name.toLowerCase()));
217
+ if (exact_matches.length > 0) return exact_matches.map((file_name) => path.join(session_dir, file_name));
218
+ const partial_matches = log_files.filter((file_name) => {
219
+ const normalized_name = file_name.toLowerCase();
220
+ return normalized_name.includes(normalized_query) || path.basename(normalized_name, ".log").includes(normalized_query);
221
+ });
222
+ if (partial_matches.length > 0) return partial_matches.map((file_name) => path.join(session_dir, file_name));
223
+ throw new Error(`No logs found for "${options.query}" in ${session_dir}. Available logs: ${log_files.join(", ")}`);
224
+ }
181
225
  function parse_service_configs(args) {
182
226
  return args.map((arg) => {
183
227
  const colon_index = arg.indexOf(":");
@@ -189,6 +233,28 @@ function parse_service_configs(args) {
189
233
  });
190
234
  }
191
235
  /**
236
+ * Tail the latest session's logs by forwarding to the system tail command.
237
+ */
238
+ function cmd_tail(project_root, options) {
239
+ const log_paths = resolve_tail_paths(project_root, options);
240
+ const child = spawn("tail", [...options.tail_args, ...log_paths], { stdio: "inherit" });
241
+ const cleanup_signal_handlers = forward_signals_to_children([child]);
242
+ return new Promise((resolve, reject) => {
243
+ child.on("close", (code) => {
244
+ cleanup_signal_handlers();
245
+ resolve(code ?? 0);
246
+ });
247
+ child.on("error", (err) => {
248
+ cleanup_signal_handlers();
249
+ if (err.code === "ENOENT") {
250
+ reject(/* @__PURE__ */ new Error("System \"tail\" command not found. \"agent-tail tail\" requires a POSIX-style tail binary."));
251
+ return;
252
+ }
253
+ reject(err);
254
+ });
255
+ });
256
+ }
257
+ /**
192
258
  * Write data to a log stream and optionally to combined.log with a prefix.
193
259
  */
194
260
  function write_to_logs(chunk, name, log_stream, combined_stream, excludes = []) {
@@ -238,20 +304,20 @@ function cmd_wrap(project_root, name, command, options = DEFAULT_CLI_OPTIONS) {
238
304
  process.stderr.write(chunk);
239
305
  write_to_logs(chunk, name, log_stream, combined_stream, options.excludes);
240
306
  });
307
+ const cleanup_signal_handlers = forward_signals_to_children([child]);
241
308
  return new Promise((resolve, reject) => {
242
309
  child.on("close", (code) => {
310
+ cleanup_signal_handlers();
243
311
  log_stream.end();
244
312
  combined_stream?.end();
245
313
  resolve(code ?? 0);
246
314
  });
247
315
  child.on("error", (err) => {
316
+ cleanup_signal_handlers();
248
317
  log_stream.end();
249
318
  combined_stream?.end();
250
319
  reject(err);
251
320
  });
252
- for (const signal of ["SIGINT", "SIGTERM"]) process.on(signal, () => {
253
- child.kill(signal);
254
- });
255
321
  });
256
322
  }
257
323
  const COLORS = [
@@ -332,13 +398,15 @@ function cmd_run(project_root, service_args, options = DEFAULT_CLI_OPTIONS) {
332
398
  });
333
399
  });
334
400
  });
335
- for (const signal of ["SIGINT", "SIGTERM"]) process.on(signal, () => {
336
- for (const child of children) child.kill(signal);
337
- });
401
+ const cleanup_signal_handlers = forward_signals_to_children(children);
338
402
  return Promise.all(promises).then(() => {
403
+ cleanup_signal_handlers();
339
404
  combined_stream?.end();
405
+ }, (error) => {
406
+ cleanup_signal_handlers();
407
+ throw error;
340
408
  });
341
409
  }
342
410
 
343
411
  //#endregion
344
- export { resolve_session_dir as a, LogManager as c, parse_service_configs as i, SESSION_ENV_VAR as l, cmd_run as n, DEFAULT_OPTIONS as o, cmd_wrap as r, resolve_options as s, cmd_init as t, should_exclude as u };
412
+ export { parse_service_configs as a, resolve_options as c, should_exclude as d, cmd_wrap as i, LogManager as l, cmd_run as n, resolve_session_dir as o, cmd_tail as r, DEFAULT_OPTIONS as s, cmd_init as t, SESSION_ENV_VAR as u };
package/dist/index.d.mts CHANGED
@@ -65,6 +65,10 @@ declare class LogManager {
65
65
  * points to a valid directory, return it. Otherwise create a new session.
66
66
  */
67
67
  resolve_session(project_root: string): string;
68
+ /**
69
+ * Find the current session directory if one already exists.
70
+ */
71
+ find_session(project_root: string): string | null;
68
72
  check_gitignore(project_root: string): void;
69
73
  }
70
74
  //#endregion
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { a as resolve_session_dir, c as LogManager, i as parse_service_configs, l as SESSION_ENV_VAR, n as cmd_run, o as DEFAULT_OPTIONS, r as cmd_wrap, s as resolve_options, t as cmd_init, u as should_exclude } from "./commands-Bw4eTFz0.mjs";
1
+ import { a as parse_service_configs, c as resolve_options, d as should_exclude, i as cmd_wrap, l as LogManager, n as cmd_run, o as resolve_session_dir, s as DEFAULT_OPTIONS, t as cmd_init, u as SESSION_ENV_VAR } from "./commands-Bv44Rf77.mjs";
2
2
 
3
3
  //#region src/formatter.ts
4
4
  function format_log_line(entry) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "agent-tail-core",
3
3
  "type": "module",
4
- "version": "0.3.9",
4
+ "version": "0.4.0",
5
5
  "description": "Core utilities for agent-tail log capture plugins.",
6
6
  "license": "MIT",
7
7
  "repository": {