@commonlyai/cli 0.1.213 → 0.1.214

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": "@commonlyai/cli",
3
- "version": "0.1.213",
3
+ "version": "0.1.214",
4
4
  "license": "Apache-2.0",
5
5
  "description": "The Commonly CLI — connect agents, manage pods, iterate fast",
6
6
  "type": "module",
@@ -1,6 +1,7 @@
1
1
  import { createOwnKeyResolver, createMachineReportClient } from '../lib/own-key-launch.js';
2
2
  import { foregroundDaemonHint } from '../lib/daemon-hint.js';
3
3
  import { loadDaemonRecord } from '../lib/daemon-store.js';
4
+ import { daemonSeatLogPath, logSize, readAppended, readLogTail } from '../lib/daemon-logs.js';
4
5
  import { WALKED_HARNESSES } from '../lib/runtime-inventory.js';
5
6
  import { createRuntimeReporter } from '../lib/runtime-observation.js';
6
7
  /**
@@ -4374,38 +4375,85 @@ Use --local to find the name you'd pass to 'agent run' or 'agent detach'.
4374
4375
  });
4375
4376
 
4376
4377
  // ── logs ──────────────────────────────────────────────────────────────────
4378
+ // Without --pod, the seat's local daemon log is read. With --pod, the owner
4379
+ // login reads recent events and runs from the member-gated seat-activity
4380
+ // route, whichever machine the seat runs on. The owner login is never sent to
4381
+ // the agent-runtime routes, which only accept a seat's own token.
4377
4382
  agent
4378
4383
  .command('logs <name>')
4379
- .description('Stream recent events for an agent')
4380
- .option('--instance-id <id>', 'Instance ID', 'default')
4381
- .option('--follow', 'Keep polling for new events', false)
4382
- .option('--instance <url>', 'Target Commonly instance')
4384
+ .description("Show a seat's recent log: local daemon log, or server activity with --pod")
4385
+ .option('--pod <podId>', 'Read server-side events and runs for a pod you belong to')
4386
+ .option('--instance-id <id>', 'Seat instance ID', 'default')
4387
+ .option('--follow', 'Continue printing new output', false)
4388
+ .option('--lines <n>', 'Number of trailing local lines to show', '50')
4389
+ .option('--instance <url>', 'Target Commonly instance (with --pod)')
4383
4390
  .action(async (name, opts) => {
4391
+ if (opts.follow && opts.pod) {
4392
+ console.error('--follow needs a local seat log; server activity is a snapshot. Re-run without --follow.');
4393
+ process.exit(1);
4394
+ }
4395
+ const lines = Number(opts.lines);
4396
+ if (!Number.isInteger(lines) || lines < 1) {
4397
+ console.error(`--lines must be a positive integer, got '${opts.lines}'`);
4398
+ process.exit(1);
4399
+ }
4400
+ if (!opts.pod) {
4401
+ const path = daemonSeatLogPath(name);
4402
+ let output;
4403
+ try {
4404
+ output = readLogTail(path, lines);
4405
+ } catch (err) {
4406
+ console.error(`Cannot read ${path}: ${err.message}`);
4407
+ process.exit(1);
4408
+ }
4409
+ if (output === null) {
4410
+ console.error(`No local log for '${name}' at ${path}. Pass --pod <podId> to read its server-side activity.`);
4411
+ process.exit(1);
4412
+ }
4413
+ if (output) console.log(output);
4414
+ if (!opts.follow) return;
4415
+ // Poll rather than fs.watch: a rename-rotated log would leave a watch
4416
+ // on the old inode silent. Only appended bytes are printed.
4417
+ let offset = logSize(path);
4418
+ const timer = setInterval(() => {
4419
+ try {
4420
+ const next = readAppended(path, offset);
4421
+ offset = next.offset;
4422
+ if (next.text) process.stdout.write(next.text);
4423
+ } catch (err) {
4424
+ // Rotation can leave a gap with no file; wait for it to come back.
4425
+ if (err?.code === 'ENOENT') return;
4426
+ console.error(`Cannot follow ${path}: ${err.message}`);
4427
+ process.exit(1);
4428
+ }
4429
+ }, 1000);
4430
+ const close = () => { clearInterval(timer); process.exit(0); };
4431
+ process.once('SIGINT', close);
4432
+ process.once('SIGTERM', close);
4433
+ return;
4434
+ }
4435
+
4384
4436
  const token = getToken(opts.instance);
4385
4437
  if (!token) { console.error('Not logged in. Run: commonly login'); process.exit(1); }
4386
-
4387
4438
  const client = createClient({ instance: resolveInstanceUrl(opts.instance), token });
4388
-
4389
- const fetchAndPrint = async () => {
4390
- const { events = [] } = await client.get('/api/agents/runtime/events', {
4439
+ const printActivity = async () => {
4440
+ const { events = [], runs = [] } = await client.get('/api/agents/seat-activity', {
4441
+ podId: opts.pod,
4391
4442
  agentName: name,
4392
4443
  instanceId: opts.instanceId,
4393
- limit: 20,
4394
4444
  });
4395
4445
  events.forEach((e) => {
4396
4446
  const ts = new Date(e.createdAt).toTimeString().slice(0, 8);
4397
- const outcome = e.delivery?.outcome || e.status;
4398
- console.log(`[${ts}] ${e.type.padEnd(18)} → ${outcome}`);
4447
+ console.log(`[${ts}] ${String(e.type).padEnd(18)} → ${e.outcome || e.status}`);
4399
4448
  });
4400
- return events;
4449
+ runs.forEach((r) => {
4450
+ const ts = new Date(r.startedAt).toTimeString().slice(0, 8);
4451
+ console.log(`[${ts}] run ${String(r.trigger).padEnd(14)} → ${r.status} (${r.totalTokens} tokens)`);
4452
+ });
4453
+ if (events.length === 0 && runs.length === 0) console.log(`No recent activity for '${name}' in this pod.`);
4401
4454
  };
4402
-
4403
4455
  try {
4404
- await fetchAndPrint();
4405
- if (opts.follow) {
4406
- console.log('Following... (Ctrl+C to stop)');
4407
- setInterval(fetchAndPrint, 5000);
4408
- }
4456
+ await printActivity();
4409
4457
  } catch (err) {
4410
4458
  console.error(`Failed: ${err.message}`);
4411
4459
  process.exit(1);
@@ -1,4 +1,4 @@
1
- import { readFileSync } from 'node:fs';
1
+ import { closeSync, openSync, readFileSync, readSync, statSync } from 'node:fs';
2
2
  import { homedir } from 'node:os';
3
3
  import { join } from 'node:path';
4
4
 
@@ -23,3 +23,21 @@ export const readLogTail = (path, count = 50) => {
23
23
  throw error;
24
24
  }
25
25
  };
26
+
27
+ export const logSize = (path) => statSync(path).size;
28
+
29
+ // Bytes appended since `offset`, for `agent logs --follow`. A file that shrank
30
+ // (rotated or truncated) is read from the start again.
31
+ export const readAppended = (path, offset) => {
32
+ const size = logSize(path);
33
+ const start = size < offset ? 0 : offset;
34
+ if (size === start) return { text: '', offset: size };
35
+ const fd = openSync(path, 'r');
36
+ try {
37
+ const buffer = Buffer.alloc(size - start);
38
+ readSync(fd, buffer, 0, buffer.length, start);
39
+ return { text: buffer.toString('utf8'), offset: size };
40
+ } finally {
41
+ closeSync(fd);
42
+ }
43
+ };