sentinelayer-cli 0.12.2 → 0.12.4

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.12.2",
3
+ "version": "0.12.4",
4
4
  "description": "Scaffold Sentinelayer spec/prompt/guide artifacts with secure browser auth and token bootstrap.",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -7,7 +7,10 @@ export const PRICED_ACTIONS = Object.freeze([
7
7
  "audit_run",
8
8
  "audit_security",
9
9
  "audit_frontend",
10
+ "chat_ask",
10
11
  "omargate_deep",
12
+ "scan_precheck",
13
+ "spec_generate_ai",
11
14
  ]);
12
15
 
13
16
  const OMIT_METADATA_KEYS = new Set([
@@ -0,0 +1,91 @@
1
+ import { buildBillingRunId, buildCallIdempotencyKey, stableHash } from "./ledger-entry.js";
2
+ import { recordSessionUsage } from "./session-usage.js";
3
+
4
+ function normalizeString(value) {
5
+ return String(value || "").trim();
6
+ }
7
+
8
+ function nonNegativeInteger(value) {
9
+ const parsed = Number(value || 0);
10
+ if (!Number.isFinite(parsed) || parsed < 0) return 0;
11
+ return Math.floor(parsed);
12
+ }
13
+
14
+ export function usageNumber(value, fallback = 0) {
15
+ const parsed = Number(value);
16
+ if (Number.isFinite(parsed) && parsed >= 0) return parsed;
17
+ const parsedFallback = Number(fallback || 0);
18
+ return Number.isFinite(parsedFallback) && parsedFallback >= 0 ? parsedFallback : 0;
19
+ }
20
+
21
+ export async function recordCliLlmSessionUsage({
22
+ sessionId,
23
+ agentId,
24
+ action,
25
+ model,
26
+ inputTokens = 0,
27
+ outputTokens = 0,
28
+ startedAtIso = "",
29
+ targetPath,
30
+ billingTier = "internal",
31
+ sourceCommand = "",
32
+ provider = "",
33
+ metadata = {},
34
+ } = {}) {
35
+ const normalizedSessionId = normalizeString(sessionId);
36
+ const normalizedAgentId = normalizeString(agentId);
37
+ const normalizedAction = normalizeString(action);
38
+ const normalizedModel = normalizeString(model);
39
+ const createdAt = normalizeString(startedAtIso) || new Date().toISOString();
40
+ const safeInputTokens = nonNegativeInteger(inputTokens);
41
+ const safeOutputTokens = nonNegativeInteger(outputTokens);
42
+
43
+ if (!normalizedSessionId || !normalizedAgentId || !normalizedAction || !normalizedModel) {
44
+ return { ok: false, reason: "missing_session_usage_fields" };
45
+ }
46
+ if (safeInputTokens + safeOutputTokens <= 0) {
47
+ return { ok: false, reason: "zero_tokens" };
48
+ }
49
+
50
+ try {
51
+ const configHash = stableHash(
52
+ JSON.stringify({
53
+ action: normalizedAction,
54
+ agentId: normalizedAgentId,
55
+ model: normalizedModel,
56
+ provider: normalizeString(provider),
57
+ sourceCommand: normalizeString(sourceCommand),
58
+ metadata,
59
+ }),
60
+ );
61
+ const billingRunId = buildBillingRunId({
62
+ sessionId: normalizedSessionId,
63
+ invocationTimestamp: createdAt,
64
+ configHash,
65
+ });
66
+ return await recordSessionUsage(
67
+ normalizedSessionId,
68
+ {
69
+ agentId: normalizedAgentId,
70
+ action: normalizedAction,
71
+ model: normalizedModel,
72
+ inputTokens: safeInputTokens,
73
+ outputTokens: safeOutputTokens,
74
+ idempotencyKey: buildCallIdempotencyKey({ runId: billingRunId, callIndex: 0 }),
75
+ billingTier,
76
+ createdAt,
77
+ metadata: {
78
+ sourceCommand,
79
+ provider,
80
+ ...metadata,
81
+ },
82
+ },
83
+ { targetPath },
84
+ );
85
+ } catch (error) {
86
+ return {
87
+ ok: false,
88
+ reason: error instanceof Error ? error.message : String(error || "session_usage_failed"),
89
+ };
90
+ }
91
+ }
@@ -9,7 +9,9 @@ import {
9
9
  resolveModel,
10
10
  resolveProvider,
11
11
  } from "../ai/client.js";
12
+ import { recordCliLlmSessionUsage, usageNumber } from "../billing/llm-session-usage.js";
12
13
  import { resolveOutputRoot } from "../config/service.js";
14
+ import { estimateModelCost } from "../cost/tracker.js";
13
15
  import { estimateTokens } from "../cost/tokenizer.js";
14
16
 
15
17
  function shouldEmitJson(options, command) {
@@ -92,7 +94,9 @@ export function registerChatCommand(program) {
92
94
  });
93
95
 
94
96
  const startedAt = Date.now();
97
+ const startedAtIso = new Date(startedAt).toISOString();
95
98
  let responseText = "";
99
+ let invocation = null;
96
100
 
97
101
  if (options.dryRun) {
98
102
  responseText = `DRY_RUN_RESPONSE: ${prompt.slice(0, 240)}`;
@@ -100,7 +104,7 @@ export function registerChatCommand(program) {
100
104
  const streamEnabled = Boolean(options.stream);
101
105
  let streamedText = "";
102
106
  const client = createMultiProviderApiClient();
103
- const invocation = await client.invoke({
107
+ invocation = await client.invoke({
104
108
  provider,
105
109
  model,
106
110
  prompt,
@@ -125,8 +129,31 @@ export function registerChatCommand(program) {
125
129
 
126
130
  const durationMs = Date.now() - startedAt;
127
131
  const generatedAt = new Date().toISOString();
128
- const inputTokens = estimateTokens(prompt, { model });
129
- const outputTokens = estimateTokens(responseText, { model });
132
+ const estimatedInputTokens = estimateTokens(prompt, { model });
133
+ const estimatedOutputTokens = estimateTokens(responseText, { model });
134
+ const inputTokens = usageNumber(invocation?.usage?.inputTokens, estimatedInputTokens);
135
+ const outputTokens = usageNumber(invocation?.usage?.outputTokens, estimatedOutputTokens);
136
+ const modelCost = estimateModelCost({ modelId: invocation?.model || model, inputTokens, outputTokens });
137
+ const costUsd = usageNumber(invocation?.usage?.costUsd, modelCost.costUsd);
138
+
139
+ const sessionUsageLedger = options.dryRun
140
+ ? { ok: false, reason: "dry_run" }
141
+ : await recordCliLlmSessionUsage({
142
+ sessionId,
143
+ agentId: "chat-cli",
144
+ action: "chat_ask",
145
+ model: invocation?.model || model,
146
+ inputTokens,
147
+ outputTokens,
148
+ startedAtIso,
149
+ targetPath,
150
+ sourceCommand: "chat ask",
151
+ provider: invocation?.provider || provider,
152
+ metadata: {
153
+ streamed: Boolean(options.stream),
154
+ pricingFound: modelCost.pricingFound,
155
+ },
156
+ });
130
157
 
131
158
  await appendTranscriptEntries({
132
159
  transcriptPath,
@@ -154,8 +181,8 @@ export function registerChatCommand(program) {
154
181
  const payload = {
155
182
  command: "chat ask",
156
183
  sessionId,
157
- provider,
158
- model,
184
+ provider: invocation?.provider || provider,
185
+ model: invocation?.model || model,
159
186
  dryRun: Boolean(options.dryRun),
160
187
  streamed: Boolean(options.stream),
161
188
  transcriptPath,
@@ -165,8 +192,10 @@ export function registerChatCommand(program) {
165
192
  inputTokens,
166
193
  outputTokens,
167
194
  totalTokens: inputTokens + outputTokens,
195
+ costUsd,
168
196
  durationMs,
169
197
  },
198
+ billing: sessionUsageLedger,
170
199
  };
171
200
 
172
201
  if (emitJson) {
@@ -179,6 +208,6 @@ export function registerChatCommand(program) {
179
208
  }
180
209
  console.log(pc.gray(`session: ${sessionId}`));
181
210
  console.log(pc.gray(`transcript: ${transcriptPath}`));
182
- console.log(pc.gray(`usage: input=${inputTokens} output=${outputTokens} duration_ms=${durationMs}`));
211
+ console.log(pc.gray(`usage: input=${inputTokens} output=${outputTokens} cost_usd=${costUsd.toFixed(6)} duration_ms=${durationMs}`));
183
212
  });
184
213
  }
@@ -11,6 +11,7 @@ import {
11
11
  resolveModel,
12
12
  resolveProvider,
13
13
  } from "../ai/client.js";
14
+ import { recordCliLlmSessionUsage, usageNumber } from "../billing/llm-session-usage.js";
14
15
  import { loadConfig, resolveOutputRoot } from "../config/service.js";
15
16
  import { evaluateBudget } from "../cost/budget.js";
16
17
  import { appendCostEntry, summarizeCostHistory } from "../cost/history.js";
@@ -585,6 +586,7 @@ export function registerScanCommand(program) {
585
586
  profile,
586
587
  });
587
588
 
589
+ const startedAtIso = new Date().toISOString();
588
590
  const startedAtMs = Date.now();
589
591
  const client = createMultiProviderApiClient();
590
592
  const response = await client.invoke({
@@ -615,13 +617,16 @@ export function registerScanCommand(program) {
615
617
  await fsp.mkdir(path.dirname(reportPath), { recursive: true });
616
618
  await fsp.writeFile(reportPath, reportMarkdown, "utf-8");
617
619
 
618
- const inputTokens = estimateTokens(prompt, { model: response.model });
619
- const outputTokens = estimateTokens(aiMarkdown, { model: response.model });
620
+ const estimatedInputTokens = estimateTokens(prompt, { model: response.model });
621
+ const estimatedOutputTokens = estimateTokens(aiMarkdown, { model: response.model });
622
+ const inputTokens = usageNumber(response.usage?.inputTokens, estimatedInputTokens);
623
+ const outputTokens = usageNumber(response.usage?.outputTokens, estimatedOutputTokens);
620
624
  const modelCost = maybeEstimateModelCost({
621
625
  modelId: response.model,
622
626
  inputTokens,
623
627
  outputTokens,
624
628
  });
629
+ const costUsd = usageNumber(response.usage?.costUsd, modelCost.costUsd);
625
630
  const sessionId = String(options.sessionId || "scan-ai-precheck").trim() || "scan-ai-precheck";
626
631
 
627
632
  const appendedCost = await appendCostEntry(
@@ -639,7 +644,7 @@ export function registerScanCommand(program) {
639
644
  cacheWriteTokens: 0,
640
645
  durationMs,
641
646
  toolCalls: 1,
642
- costUsd: modelCost.costUsd,
647
+ costUsd,
643
648
  progressScore: aiMarkdown ? 1 : 0,
644
649
  }
645
650
  );
@@ -682,7 +687,7 @@ export function registerScanCommand(program) {
682
687
  outputTokens,
683
688
  cacheReadTokens: 0,
684
689
  cacheWriteTokens: 0,
685
- costUsd: modelCost.costUsd,
690
+ costUsd,
686
691
  durationMs,
687
692
  toolCalls: 1,
688
693
  },
@@ -727,9 +732,28 @@ export function registerScanCommand(program) {
727
732
  invocationId: appendedCost.entry.invocationId,
728
733
  },
729
734
  }
730
- );
735
+ );
731
736
  }
732
737
 
738
+ const sessionUsageLedger = await recordCliLlmSessionUsage({
739
+ sessionId,
740
+ agentId: "scan-precheck",
741
+ action: "scan_precheck",
742
+ model: response.model,
743
+ inputTokens,
744
+ outputTokens,
745
+ startedAtIso,
746
+ targetPath,
747
+ sourceCommand: "scan precheck",
748
+ provider: response.provider,
749
+ metadata: {
750
+ specPath,
751
+ profile,
752
+ policyPackId: activePolicy.selected?.id || "",
753
+ pricingFound: modelCost.pricingFound,
754
+ },
755
+ });
756
+
733
757
  const payload = {
734
758
  command: "scan precheck",
735
759
  targetPath,
@@ -749,10 +773,11 @@ export function registerScanCommand(program) {
749
773
  usage: {
750
774
  inputTokens,
751
775
  outputTokens,
752
- costUsd: modelCost.costUsd,
776
+ costUsd,
753
777
  durationMs,
754
778
  toolCalls: 1,
755
779
  },
780
+ billing: sessionUsageLedger,
756
781
  budget,
757
782
  cost: {
758
783
  filePath: appendedCost.filePath,
@@ -9,6 +9,7 @@ import {
9
9
  resolveModel,
10
10
  resolveProvider,
11
11
  } from "../ai/client.js";
12
+ import { recordCliLlmSessionUsage, usageNumber } from "../billing/llm-session-usage.js";
12
13
  import { loadConfig } from "../config/service.js";
13
14
  import { evaluateBudget } from "../cost/budget.js";
14
15
  import { appendCostEntry, summarizeCostHistory } from "../cost/history.js";
@@ -273,6 +274,7 @@ async function maybeEnhanceSpecWithAi({
273
274
  ingest,
274
275
  });
275
276
 
277
+ const startedAtIso = new Date().toISOString();
276
278
  const startedAtMs = Date.now();
277
279
  const client = createMultiProviderApiClient();
278
280
  const result = await client.invoke({
@@ -288,13 +290,16 @@ async function maybeEnhanceSpecWithAi({
288
290
  const normalizedText = String(result.text || "").trim();
289
291
  const enhancedMarkdown = normalizedText || baseSpecMarkdown;
290
292
 
291
- const inputTokens = estimateTokens(prompt, { model: result.model });
292
- const outputTokens = estimateTokens(enhancedMarkdown, { model: result.model });
293
+ const estimatedInputTokens = estimateTokens(prompt, { model: result.model });
294
+ const estimatedOutputTokens = estimateTokens(enhancedMarkdown, { model: result.model });
295
+ const inputTokens = usageNumber(result.usage?.inputTokens, estimatedInputTokens);
296
+ const outputTokens = usageNumber(result.usage?.outputTokens, estimatedOutputTokens);
293
297
  const modelCost = maybeEstimateModelCost({
294
298
  modelId: result.model,
295
299
  inputTokens,
296
300
  outputTokens,
297
301
  });
302
+ const costUsd = usageNumber(result.usage?.costUsd, modelCost.costUsd);
298
303
 
299
304
  const sessionId = String(options.sessionId || "spec-generate-ai").trim() || "spec-generate-ai";
300
305
  const appendedCost = await appendCostEntry(
@@ -312,7 +317,7 @@ async function maybeEnhanceSpecWithAi({
312
317
  cacheWriteTokens: 0,
313
318
  durationMs,
314
319
  toolCalls: 1,
315
- costUsd: modelCost.costUsd,
320
+ costUsd,
316
321
  progressScore: normalizedText ? 1 : 0,
317
322
  }
318
323
  );
@@ -355,7 +360,7 @@ async function maybeEnhanceSpecWithAi({
355
360
  outputTokens,
356
361
  cacheReadTokens: 0,
357
362
  cacheWriteTokens: 0,
358
- costUsd: modelCost.costUsd,
363
+ costUsd,
359
364
  durationMs,
360
365
  toolCalls: 1,
361
366
  },
@@ -403,6 +408,24 @@ async function maybeEnhanceSpecWithAi({
403
408
  );
404
409
  }
405
410
 
411
+ const sessionUsageLedger = await recordCliLlmSessionUsage({
412
+ sessionId,
413
+ agentId: "spec-generator",
414
+ action: "spec_generate_ai",
415
+ model: result.model,
416
+ inputTokens,
417
+ outputTokens,
418
+ startedAtIso,
419
+ targetPath,
420
+ sourceCommand: "spec generate --ai",
421
+ provider: result.provider,
422
+ metadata: {
423
+ template: template?.id || template?.name || "",
424
+ projectType: template?.projectType || "",
425
+ pricingFound: modelCost.pricingFound,
426
+ },
427
+ });
428
+
406
429
  return {
407
430
  markdown: enhancedMarkdown,
408
431
  ai: {
@@ -413,10 +436,11 @@ async function maybeEnhanceSpecWithAi({
413
436
  usage: {
414
437
  inputTokens,
415
438
  outputTokens,
416
- costUsd: modelCost.costUsd,
439
+ costUsd,
417
440
  durationMs,
418
441
  toolCalls: 1,
419
442
  },
443
+ billing: sessionUsageLedger,
420
444
  budget,
421
445
  cost: {
422
446
  filePath: appendedCost.filePath,