sentinelayer-cli 0.30.9 → 0.30.11

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": "sentinelayer-cli",
3
- "version": "0.30.9",
3
+ "version": "0.30.11",
4
4
  "description": "Scaffold Sentinelayer spec/prompt/guide artifacts with secure browser auth and token bootstrap.",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -9,7 +9,7 @@
9
9
  "docs:build": "node scripts/docs-build.mjs",
10
10
  "test": "npm run test:unit && npm run test:e2e",
11
11
  "test:unit": "node --import ./tests/setup-env.mjs --test tests/unit*.test.mjs",
12
- "test:e2e": "node --import ./tests/setup-env.mjs --test tests/e2e.test.mjs",
12
+ "test:e2e": "node --import ./tests/setup-env.mjs --test tests/e2e*.test.mjs",
13
13
  "test:coverage": "c8 node --import ./tests/setup-env.mjs --test tests/unit*.test.mjs",
14
14
  "verify": "npm run check && npm run docs:build && npm run test:e2e && npm run test:coverage && npm pack --dry-run",
15
15
  "release:publish": "node scripts/release-publish.mjs"
@@ -138,6 +138,49 @@ function normalizeString(value) {
138
138
  return String(value || "").trim();
139
139
  }
140
140
 
141
+ function messagePartsHaveContent(messageParts) {
142
+ const parts = Array.isArray(messageParts) ? messageParts : [messageParts];
143
+ return parts.some((part) => normalizeString(part));
144
+ }
145
+
146
+ function joinMessageParts(messageParts) {
147
+ if (Array.isArray(messageParts)) {
148
+ return messageParts.length === 1 ? String(messageParts[0] || "") : messageParts.join(" ");
149
+ }
150
+ return String(messageParts || "");
151
+ }
152
+
153
+ async function readUtf8FromStdin(stdin = process.stdin) {
154
+ if (stdin.isTTY) {
155
+ throw new Error("--stdin requires piped input.");
156
+ }
157
+ stdin.setEncoding("utf8");
158
+ let text = "";
159
+ for await (const chunk of stdin) {
160
+ text += String(chunk);
161
+ }
162
+ return text;
163
+ }
164
+
165
+ async function resolveSessionSayMessageInput(messageParts, options = {}) {
166
+ const hasPositionalMessage = messagePartsHaveContent(messageParts);
167
+ const hasMessageFile = Boolean(normalizeString(options.messageFile));
168
+ const hasStdin = Boolean(options.stdin);
169
+ const sources = [hasPositionalMessage, hasMessageFile, hasStdin].filter(Boolean).length;
170
+ if (sources > 1) {
171
+ throw new Error("Use only one message source: positional message, --message-file, or --stdin.");
172
+ }
173
+ if (hasMessageFile) {
174
+ return normalizeString(
175
+ await fsp.readFile(path.resolve(process.cwd(), String(options.messageFile)), "utf-8"),
176
+ );
177
+ }
178
+ if (hasStdin) {
179
+ return normalizeString(await readUtf8FromStdin());
180
+ }
181
+ return normalizeString(joinMessageParts(messageParts));
182
+ }
183
+
141
184
  function optionWasSetByCli(command, optionName) {
142
185
  if (!command || typeof command.getOptionValueSource !== "function") {
143
186
  return false;
@@ -3343,7 +3386,7 @@ export function registerSessionCommand(program) {
3343
3386
  });
3344
3387
 
3345
3388
  session
3346
- .command("say <sessionId> <message>")
3389
+ .command("say <sessionId> [message...]")
3347
3390
  .description("Send a message to the session")
3348
3391
  .option(
3349
3392
  "--agent <id>",
@@ -3367,16 +3410,18 @@ export function registerSessionCommand(program) {
3367
3410
  .option("--to <agent>", "Direct the message to a specific agent id")
3368
3411
  .option("--reply-to <sequence>", "Mark this message as a reply to a target sequence id")
3369
3412
  .option("--reply-cursor <cursor>", "Mark this message as a reply to a target event cursor")
3413
+ .option("--message-file <path>", "Read the message body from a UTF-8 file")
3414
+ .option("--stdin", "Read the message body from stdin")
3370
3415
  .option("--force-cli-user", "Allow fallback sends as cli-user when no agent identity can be resolved")
3371
3416
  .option("--local-only", "Append only to the local session cache without remote send confirmation")
3372
3417
  .option("--path <path>", "Workspace path for the session", ".")
3373
3418
  .option("--json", "Emit machine-readable output")
3374
- .action(async (sessionId, message, options, command) => {
3419
+ .action(async (sessionId, messageParts, options, command) => {
3375
3420
  const normalizedSessionId = normalizeString(sessionId);
3376
3421
  if (!normalizedSessionId) {
3377
3422
  throw new Error("session id is required.");
3378
3423
  }
3379
- const normalizedMessage = normalizeString(message);
3424
+ const normalizedMessage = await resolveSessionSayMessageInput(messageParts, options);
3380
3425
  if (!normalizedMessage) {
3381
3426
  throw new Error("message is required.");
3382
3427
  }
@@ -138,7 +138,10 @@ export function buildUsageLedgerEntry(
138
138
  { sessionId = "", priceBookVersion = DEFAULT_PRICE_BOOK_VERSION, billingTier = "unknown" } = {},
139
139
  ) {
140
140
  const kind = n(event?.event || event?.type);
141
- const payload = object(event?.payload);
141
+ const rawPayload = event?.payload;
142
+ const payload = object(rawPayload);
143
+ const payloadWasObject = rawPayload && typeof rawPayload === "object" && !Array.isArray(rawPayload);
144
+ if (kind === SESSION_USAGE_EVENT && !payloadWasObject) return null;
142
145
  const schema = n(payload.schema);
143
146
  const usage = object(payload.usage);
144
147
  const legacyUsageEvent = kind !== SESSION_USAGE_EVENT && hasUsageHints(usage);
@@ -237,7 +237,12 @@ function eventBody(event) {
237
237
  * - totals: { tokenTotal, costTotalUsd } summed through the pricing ledger
238
238
  * - sentiActions: count of orchestrator events
239
239
  */
240
- export function computeTranscriptStats({ sessionMeta = {}, events = [], speakerProfiles = new Map() } = {}) {
240
+ export function computeTranscriptStats({
241
+ sessionMeta = {},
242
+ events = [],
243
+ speakerProfiles = new Map(),
244
+ usageLedger = null,
245
+ } = {}) {
241
246
  const perAgent = new Map();
242
247
  let firstEventTs = null;
243
248
  let lastEventTs = null;
@@ -305,14 +310,14 @@ export function computeTranscriptStats({ sessionMeta = {}, events = [], speakerP
305
310
  if (epoch > record.lastSeenMs) record.lastSeenMs = epoch;
306
311
  }
307
312
 
308
- const usageLedger = buildSessionUsageLedger(events, {
313
+ const resolvedUsageLedger = usageLedger || buildSessionUsageLedger(events, {
309
314
  sessionId: normalize(sessionMeta.sessionId),
310
315
  });
311
316
  const fallbackUsageEpoch =
312
317
  lastEventTs ??
313
318
  firstEventTs ??
314
319
  (Number.isFinite(Date.parse(sessionMeta?.createdAt)) ? Date.parse(sessionMeta.createdAt) : 0);
315
- for (const entry of usageLedger.entries) {
320
+ for (const entry of resolvedUsageLedger.entries) {
316
321
  const entryEpoch = Number.isFinite(Date.parse(entry.timestamp))
317
322
  ? Date.parse(entry.timestamp)
318
323
  : fallbackUsageEpoch;
@@ -364,17 +369,17 @@ export function computeTranscriptStats({ sessionMeta = {}, events = [], speakerP
364
369
  sessionLiveSeconds,
365
370
  agents,
366
371
  totals: {
367
- tokenTotal: usageLedger.totals.totalTokens,
368
- inputTokens: usageLedger.totals.inputTokens,
369
- outputTokens: usageLedger.totals.outputTokens,
370
- costTotalUsd: usageLedger.totals.providerCostUsd,
371
- customerCostTotalUsd: usageLedger.totals.hasCustomerCost
372
- ? usageLedger.totals.customerCostUsd
372
+ tokenTotal: resolvedUsageLedger.totals.totalTokens,
373
+ inputTokens: resolvedUsageLedger.totals.inputTokens,
374
+ outputTokens: resolvedUsageLedger.totals.outputTokens,
375
+ costTotalUsd: resolvedUsageLedger.totals.providerCostUsd,
376
+ customerCostTotalUsd: resolvedUsageLedger.totals.hasCustomerCost
377
+ ? resolvedUsageLedger.totals.customerCostUsd
373
378
  : null,
374
- usageEntries: usageLedger.entries.length,
375
- duplicatesSkipped: usageLedger.duplicatesSkipped,
376
- unpriced: usageLedger.totals.unpriced,
377
- priceBookVersions: usageLedger.priceBookVersions,
379
+ usageEntries: resolvedUsageLedger.entries.length,
380
+ duplicatesSkipped: resolvedUsageLedger.duplicatesSkipped,
381
+ unpriced: resolvedUsageLedger.totals.unpriced,
382
+ priceBookVersions: resolvedUsageLedger.priceBookVersions,
378
383
  },
379
384
  sentiActions,
380
385
  };
@@ -408,6 +413,134 @@ function avatarMd(identity) {
408
413
  return identity.avatar || letterTile(identity.displayName || identity.agentId);
409
414
  }
410
415
 
416
+ function intCell(value) {
417
+ return Math.max(0, Math.floor(Number(value || 0))).toLocaleString("en-US");
418
+ }
419
+
420
+ function usdCell(value) {
421
+ return `$${Number(value || 0).toFixed(6)}`;
422
+ }
423
+
424
+ function tableText(value, { maxLength = 160 } = {}) {
425
+ const escaped = normalize(value)
426
+ .replace(/\r?\n/g, " ")
427
+ .replace(/\|/g, "\\|")
428
+ .replace(/`/g, "'");
429
+ if (!maxLength || escaped.length <= maxLength) return escaped;
430
+ const head = Math.max(24, Math.floor((maxLength - 3) * 0.7));
431
+ const tail = Math.max(12, maxLength - 3 - head);
432
+ return `${escaped.slice(0, head)}...${escaped.slice(-tail)}`;
433
+ }
434
+
435
+ function codeCell(value) {
436
+ const text = tableText(value);
437
+ return text ? `\`${text}\`` : "—";
438
+ }
439
+
440
+ function optionalUsdCell(value, hasValue) {
441
+ return hasValue ? usdCell(value) : "—";
442
+ }
443
+
444
+ function shortenIdempotencyKey(value, maxLength = 36) {
445
+ const text = tableText(value, { maxLength: 0 });
446
+ if (!text || text.length <= maxLength) return text;
447
+ const head = Math.max(8, Math.floor((maxLength - 3) * 0.58));
448
+ const tail = Math.max(6, maxLength - 3 - head);
449
+ return `${text.slice(0, head)}...${text.slice(-tail)}`;
450
+ }
451
+
452
+ function rollupsByCostAndTokens(map) {
453
+ return [...map.values()].sort((a, b) => (
454
+ b.providerCostUsd - a.providerCostUsd ||
455
+ b.totalTokens - a.totalTokens ||
456
+ b.entries - a.entries ||
457
+ tableText(a.label).localeCompare(tableText(b.label))
458
+ ));
459
+ }
460
+
461
+ function recentUsageEntries(entries, limit = 10) {
462
+ return entries
463
+ .map((entry, index) => ({ entry, index }))
464
+ .sort((a, b) => {
465
+ const aSequence = Number.isFinite(a.entry.sequenceId) ? a.entry.sequenceId : null;
466
+ const bSequence = Number.isFinite(b.entry.sequenceId) ? b.entry.sequenceId : null;
467
+ if (aSequence != null && bSequence != null && aSequence !== bSequence) {
468
+ return bSequence - aSequence;
469
+ }
470
+ const aTime = Date.parse(a.entry.timestamp);
471
+ const bTime = Date.parse(b.entry.timestamp);
472
+ if (Number.isFinite(aTime) && Number.isFinite(bTime) && aTime !== bTime) {
473
+ return bTime - aTime;
474
+ }
475
+ return b.index - a.index;
476
+ })
477
+ .slice(0, limit)
478
+ .map(({ entry }) => entry);
479
+ }
480
+
481
+ function appendUsageRollupTable(lines, { title, labelHeader, rollups }) {
482
+ lines.push(`### ${title}`);
483
+ lines.push("");
484
+ lines.push(`| ${labelHeader} | Entries | Input | Output | Total | Provider cost | Customer cost | Unpriced |`);
485
+ lines.push("|---|---:|---:|---:|---:|---:|---:|---:|");
486
+ if (rollups.length === 0) {
487
+ lines.push(`| — | 0 | 0 | 0 | 0 | ${usdCell(0)} | — | 0 |`);
488
+ } else {
489
+ for (const rollup of rollups) {
490
+ lines.push(
491
+ `| ${codeCell(rollup.label)} | ${intCell(rollup.entries)} | ${intCell(rollup.inputTokens)} | ${intCell(rollup.outputTokens)} | ${intCell(rollup.totalTokens)} | ${usdCell(rollup.providerCostUsd)} | ${optionalUsdCell(rollup.customerCostUsd, rollup.hasCustomerCost)} | ${intCell(rollup.unpriced)} |`,
492
+ );
493
+ }
494
+ }
495
+ lines.push("");
496
+ }
497
+
498
+ function appendUsageLedgerSection(lines, usageLedger) {
499
+ const totals = usageLedger.totals;
500
+ lines.push("## Usage Ledger");
501
+ lines.push("");
502
+
503
+ if (usageLedger.entries.length === 0 && usageLedger.duplicatesSkipped === 0) {
504
+ lines.push("_No usage telemetry recorded._");
505
+ lines.push("");
506
+ return;
507
+ }
508
+
509
+ const customerCostText = totals.hasCustomerCost
510
+ ? `Customer cost: ${usdCell(totals.customerCostUsd)}`
511
+ : "Customer cost: —";
512
+ lines.push(`Accepted entries: ${intCell(usageLedger.entries.length)}`);
513
+ lines.push(
514
+ `Tokens: ${intCell(totals.totalTokens)} (input ${intCell(totals.inputTokens)} / output ${intCell(totals.outputTokens)})`,
515
+ );
516
+ lines.push(`Provider cost: ${usdCell(totals.providerCostUsd)} · ${customerCostText}`);
517
+ lines.push(`Price books: ${usageLedger.priceBookVersions.map(tableText).filter(Boolean).join(", ") || "—"}`);
518
+ lines.push(`Duplicates skipped: ${intCell(usageLedger.duplicatesSkipped)} · Unpriced entries: ${intCell(totals.unpriced)}`);
519
+ lines.push("");
520
+
521
+ appendUsageRollupTable(lines, {
522
+ title: "Per Agent",
523
+ labelHeader: "Agent",
524
+ rollups: rollupsByCostAndTokens(usageLedger.perAgent),
525
+ });
526
+ appendUsageRollupTable(lines, {
527
+ title: "Per Action",
528
+ labelHeader: "Action",
529
+ rollups: rollupsByCostAndTokens(usageLedger.perAction),
530
+ });
531
+
532
+ lines.push("### Recent Entries");
533
+ lines.push("");
534
+ lines.push("| Time | Agent | Action | Model | Tokens | Provider cost | Customer cost | Idempotency key |");
535
+ lines.push("|---|---|---|---|---:|---:|---:|---|");
536
+ for (const entry of recentUsageEntries(usageLedger.entries)) {
537
+ lines.push(
538
+ `| ${tableText(timestampOnly(entry.timestamp)) || "—"} | ${codeCell(entry.agentId)} | ${codeCell(entry.action)} | ${codeCell(entry.model)} | ${intCell(entry.totalTokens)} | ${usdCell(entry.providerCostUsd)} | ${optionalUsdCell(entry.customerCostUsd, entry.customerCostUsd != null)} | ${codeCell(shortenIdempotencyKey(entry.idempotencyKey))} |`,
539
+ );
540
+ }
541
+ lines.push("");
542
+ }
543
+
411
544
  /**
412
545
  * Build the iMessage-style markdown transcript.
413
546
  *
@@ -429,8 +562,11 @@ export function buildTranscriptMarkdown({
429
562
  speakerProfiles = new Map(),
430
563
  options = {},
431
564
  } = {}) {
565
+ const usageLedger = buildSessionUsageLedger(events, {
566
+ sessionId: normalize(sessionMeta.sessionId),
567
+ });
432
568
  const includeSystemEvents = options.includeSystemEvents !== false;
433
- const stats = computeTranscriptStats({ sessionMeta, events, speakerProfiles });
569
+ const stats = computeTranscriptStats({ sessionMeta, events, speakerProfiles, usageLedger });
434
570
 
435
571
  const lines = [];
436
572
  const sessionId = normalize(sessionMeta.sessionId) || "unknown";
@@ -496,15 +632,17 @@ export function buildTranscriptMarkdown({
496
632
  stats.participantCount = stats.agents.length + silentRegisteredAgents.length;
497
633
  lines.push("");
498
634
 
635
+ appendUsageLedgerSection(lines, usageLedger);
636
+
499
637
  // Conversation
500
638
  lines.push("## Conversation");
501
639
  lines.push("");
502
640
  for (const event of events) {
503
- const kind = normalize(event.event || event.type);
641
+ const kind = normalize(event?.event || event?.type);
504
642
  if (!kind || !TRANSCRIPT_EVENT_KINDS.has(kind)) continue;
505
643
  if (!includeSystemEvents && SYSTEM_EVENT_KINDS.has(kind)) continue;
506
644
 
507
- const agentId = normalize(event.agent?.id || event.agentId);
645
+ const agentId = normalize(event?.agent?.id || event?.agentId);
508
646
  const profile = speakerProfiles.get(agentId) || null;
509
647
  const identity = resolveSpeakerIdentity({
510
648
  agentId,