sentinelayer-cli 0.32.2 → 0.32.3
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
package/src/commands/omargate.js
CHANGED
|
@@ -20,7 +20,7 @@ export function registerOmarGateCommand(program, invokeLegacy) {
|
|
|
20
20
|
.option("--model <id>", "LLM model override (default: gpt-5.3-codex)")
|
|
21
21
|
.option("--provider <name>", "LLM provider: sentinelayer, openai, anthropic")
|
|
22
22
|
.option("--max-cost <usd>", "Maximum AI layer cost in USD (default: 5.0)")
|
|
23
|
-
.option("--notify-session <session-id>", "Write Omar Deep billing-grade usage entries into this Senti session")
|
|
23
|
+
.option("--notify-session <session-id>", "Write Omar Deep billing-grade usage entries into this Senti session (default: auto-detect active Senti session)")
|
|
24
24
|
.option("--require-usage-ledger", "Fail if Omar Deep AI calls cannot write billing-grade session_usage")
|
|
25
25
|
.option("--persona <csv>", "Only run these personas (comma-separated IDs); unknown IDs are dropped + warned")
|
|
26
26
|
.option("--skip-persona <csv>", "Skip these personas (comma-separated IDs)")
|
|
@@ -54,7 +54,7 @@ export function registerOmarGateCommand(program, invokeLegacy) {
|
|
|
54
54
|
.option("--stream", "Emit NDJSON events to stdout as personas work file-by-file")
|
|
55
55
|
.option("--notify-email <addr>", "Send final report to this email (default: account email)")
|
|
56
56
|
.option("--email-on-complete <addr>", "Trigger the API-side DD report email after the run completes")
|
|
57
|
-
.option("--notify-session <session-id>", "Stream progress into this Senti session (default: auto-
|
|
57
|
+
.option("--notify-session <session-id>", "Stream progress + billing-grade usage into this Senti session (default: auto-detect active Senti session)")
|
|
58
58
|
.option("--require-usage-ledger", "Fail if Investor-DD LLM planner calls cannot write session_usage")
|
|
59
59
|
.option("--no-email", "Skip email dispatch")
|
|
60
60
|
.option("--no-dashboard", "Skip dashboard card persistence")
|
package/src/legacy-cli.js
CHANGED
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
getCoordinationEtiquetteItems,
|
|
30
30
|
renderCoordinationNumberedList,
|
|
31
31
|
} from "./session/coordination-guidance.js";
|
|
32
|
+
import { resolveUsageSessionId } from "./session/usage-session-resolver.js";
|
|
32
33
|
|
|
33
34
|
let DEFAULT_API_URL = process.env.SENTINELAYER_API_URL || "https://api.sentinelayer.com";
|
|
34
35
|
let DEFAULT_WEB_URL = process.env.SENTINELAYER_WEB_URL || "https://sentinelayer.com";
|
|
@@ -1200,7 +1201,7 @@ async function runLocalOmarGateCommand(args) {
|
|
|
1200
1201
|
const devTestBotBaseUrl = getCommandOptionValue(args, "--devtestbot-base-url") || "";
|
|
1201
1202
|
const devTestBotScope = getCommandOptionValue(args, "--devtestbot-scope") || "";
|
|
1202
1203
|
const emailOnComplete = getCommandOptionValue(args, "--email-on-complete") || "";
|
|
1203
|
-
const
|
|
1204
|
+
const notifySessionFlag = getCommandOptionValue(args, "--notify-session") || "";
|
|
1204
1205
|
const requireUsageLedger = hasCommandOption(args, "--require-usage-ledger");
|
|
1205
1206
|
const model = getCommandOptionValue(args, "--model") || "gpt-5.3-codex";
|
|
1206
1207
|
const provider = getCommandOptionValue(args, "--provider") || "sentinelayer";
|
|
@@ -1211,6 +1212,12 @@ async function runLocalOmarGateCommand(args) {
|
|
|
1211
1212
|
if (!fs.existsSync(targetPath) || !fs.statSync(targetPath).isDirectory()) {
|
|
1212
1213
|
throw new Error(`Invalid --path target: ${targetPath}`);
|
|
1213
1214
|
}
|
|
1215
|
+
const usageSession = await resolveUsageSessionId({
|
|
1216
|
+
explicitSessionId: notifySessionFlag,
|
|
1217
|
+
targetPath,
|
|
1218
|
+
env: process.env,
|
|
1219
|
+
});
|
|
1220
|
+
const notifySession = usageSession.sessionId || "";
|
|
1214
1221
|
|
|
1215
1222
|
const { runInvestorDd } = await import("./review/investor-dd-orchestrator.js");
|
|
1216
1223
|
const { parsePersonaCsv, resolveFilteredPersonas } = await import("./review/scan-modes.js");
|
|
@@ -1235,6 +1242,15 @@ async function runLocalOmarGateCommand(args) {
|
|
|
1235
1242
|
printInfo(`Skipped personas: ${personaFilter.dropped.join(", ")}`);
|
|
1236
1243
|
}
|
|
1237
1244
|
if (dryRun) printInfo("Mode: dry-run (plan + stub report only)");
|
|
1245
|
+
if (notifySession) {
|
|
1246
|
+
printInfo(
|
|
1247
|
+
`Usage ledger: ${usageSession.source === "explicit" ? "using" : "auto-detected"} Senti session ${notifySession}`,
|
|
1248
|
+
);
|
|
1249
|
+
} else if (requireUsageLedger) {
|
|
1250
|
+
printInfo(
|
|
1251
|
+
`Usage ledger: required but no active Senti session was auto-detected (${usageSession.reason || "missing_session"}).`,
|
|
1252
|
+
);
|
|
1253
|
+
}
|
|
1238
1254
|
}
|
|
1239
1255
|
const result = await runInvestorDd({
|
|
1240
1256
|
rootPath: targetPath,
|
|
@@ -1298,7 +1314,7 @@ async function runLocalOmarGateCommand(args) {
|
|
|
1298
1314
|
const maxCostUsd = parseFloat(getCommandOptionValue(args, "--max-cost") || "5.0") || 5.0;
|
|
1299
1315
|
const modelOverride = getCommandOptionValue(args, "--model") || "";
|
|
1300
1316
|
const providerOverride = getCommandOptionValue(args, "--provider") || "";
|
|
1301
|
-
const
|
|
1317
|
+
const notifySessionFlag = getCommandOptionValue(args, "--notify-session") || "";
|
|
1302
1318
|
const requireUsageLedger = hasCommandOption(args, "--require-usage-ledger");
|
|
1303
1319
|
const scanMode = getCommandOptionValue(args, "--scan-mode") || "deep";
|
|
1304
1320
|
const maxParallel = parseInt(getCommandOptionValue(args, "--max-parallel") || "4", 10) || 4;
|
|
@@ -1315,6 +1331,12 @@ async function runLocalOmarGateCommand(args) {
|
|
|
1315
1331
|
if (!fs.existsSync(targetPath) || !fs.statSync(targetPath).isDirectory()) {
|
|
1316
1332
|
throw new Error(`Invalid --path target: ${targetPath}`);
|
|
1317
1333
|
}
|
|
1334
|
+
const usageSession = await resolveUsageSessionId({
|
|
1335
|
+
explicitSessionId: notifySessionFlag,
|
|
1336
|
+
targetPath,
|
|
1337
|
+
env: process.env,
|
|
1338
|
+
});
|
|
1339
|
+
const notifySession = usageSession.sessionId || "";
|
|
1318
1340
|
// Default scope to changed-files when the target has a git delta (uncommitted
|
|
1319
1341
|
// or staged), matching gh's PR-scoped Omar; fall back to full repo otherwise.
|
|
1320
1342
|
// Explicit --scope-mode/--diff/--staged always override (see resolveScopeModeFromOptions).
|
|
@@ -1327,6 +1349,15 @@ async function runLocalOmarGateCommand(args) {
|
|
|
1327
1349
|
printInfo(`Target: ${targetPath}`);
|
|
1328
1350
|
printInfo(`Scope mode: ${scopeMode}`);
|
|
1329
1351
|
printInfo(`Scan mode: ${scanMode} | AI: ${aiEnabled ? "enabled" : "disabled"}`);
|
|
1352
|
+
if (notifySession) {
|
|
1353
|
+
printInfo(
|
|
1354
|
+
`Usage ledger: ${usageSession.source === "explicit" ? "using" : "auto-detected"} Senti session ${notifySession}`,
|
|
1355
|
+
);
|
|
1356
|
+
} else if (requireUsageLedger) {
|
|
1357
|
+
printInfo(
|
|
1358
|
+
`Usage ledger: required but no active Senti session was auto-detected (${usageSession.reason || "missing_session"}).`,
|
|
1359
|
+
);
|
|
1360
|
+
}
|
|
1330
1361
|
console.error("");
|
|
1331
1362
|
console.error(pc.gray(` [${formatElapsed(Date.now() - commandStartedAt)}] Phase 1: Deterministic analysis (22 rules)...`));
|
|
1332
1363
|
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import process from "node:process";
|
|
2
|
+
|
|
3
|
+
import { listActiveSessions } from "./store.js";
|
|
4
|
+
|
|
5
|
+
const USAGE_SESSION_ENV_KEYS = [
|
|
6
|
+
"SENTINELAYER_NOTIFY_SESSION",
|
|
7
|
+
"SENTINELAYER_SESSION_ID",
|
|
8
|
+
"SL_SESSION_ID",
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
function normalizeString(value) {
|
|
12
|
+
return String(value || "").trim();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function isSafeUsageSessionId(value) {
|
|
16
|
+
const normalized = normalizeString(value);
|
|
17
|
+
if (!/^[A-Za-z0-9._-]{1,200}$/.test(normalized)) return false;
|
|
18
|
+
if (normalized === "." || normalized === "..") return false;
|
|
19
|
+
if (normalized.includes("..")) return false;
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function normalizeTrustedUsageSessionId(value, source) {
|
|
24
|
+
const normalized = normalizeString(value);
|
|
25
|
+
if (!normalized) return "";
|
|
26
|
+
if (!isSafeUsageSessionId(normalized)) {
|
|
27
|
+
throw new Error(
|
|
28
|
+
`Invalid ${source} usage session id: must be a single session identifier without path traversal, separators, or control characters.`,
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
return normalized;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function parseEpoch(value) {
|
|
35
|
+
const epoch = Date.parse(normalizeString(value));
|
|
36
|
+
return Number.isFinite(epoch) ? epoch : 0;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function sessionRecencyEpoch(session = {}) {
|
|
40
|
+
return Math.max(
|
|
41
|
+
parseEpoch(session.lastInteractionAt),
|
|
42
|
+
parseEpoch(session.updatedAt),
|
|
43
|
+
parseEpoch(session.createdAt),
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function resolveEnvUsageSession(env = process.env) {
|
|
48
|
+
for (const key of USAGE_SESSION_ENV_KEYS) {
|
|
49
|
+
const sessionId = normalizeTrustedUsageSessionId(env?.[key], key);
|
|
50
|
+
if (sessionId) {
|
|
51
|
+
return { sessionId, source: "env", envKey: key };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function selectMostRecentUsageSession(sessions = []) {
|
|
58
|
+
const candidatesBySessionId = new Map();
|
|
59
|
+
for (const session of Array.isArray(sessions) ? sessions : []) {
|
|
60
|
+
const sessionId = normalizeString(session?.sessionId);
|
|
61
|
+
if (!isSafeUsageSessionId(sessionId)) continue;
|
|
62
|
+
|
|
63
|
+
const candidate = {
|
|
64
|
+
sessionId,
|
|
65
|
+
title: normalizeString(session?.title),
|
|
66
|
+
recencyEpoch: sessionRecencyEpoch(session),
|
|
67
|
+
};
|
|
68
|
+
const existing = candidatesBySessionId.get(sessionId);
|
|
69
|
+
if (!existing || candidate.recencyEpoch > existing.recencyEpoch) {
|
|
70
|
+
candidatesBySessionId.set(sessionId, candidate);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const candidates = Array.from(candidatesBySessionId.values());
|
|
75
|
+
|
|
76
|
+
if (candidates.length === 0) {
|
|
77
|
+
return { sessionId: "", source: "none", reason: "no_active_session" };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
candidates.sort((left, right) => {
|
|
81
|
+
if (right.recencyEpoch !== left.recencyEpoch) {
|
|
82
|
+
return right.recencyEpoch - left.recencyEpoch;
|
|
83
|
+
}
|
|
84
|
+
return left.sessionId.localeCompare(right.sessionId);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const [winner, runnerUp] = candidates;
|
|
88
|
+
if (runnerUp && runnerUp.recencyEpoch === winner.recencyEpoch) {
|
|
89
|
+
return {
|
|
90
|
+
sessionId: "",
|
|
91
|
+
source: "ambiguous",
|
|
92
|
+
reason: "multiple_active_sessions_same_recency",
|
|
93
|
+
candidates: candidates.map((candidate) => candidate.sessionId),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
sessionId: winner.sessionId,
|
|
99
|
+
source: "local_active_session",
|
|
100
|
+
title: winner.title || null,
|
|
101
|
+
candidates: candidates.map((candidate) => candidate.sessionId),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export async function resolveUsageSessionId({
|
|
106
|
+
explicitSessionId = "",
|
|
107
|
+
targetPath = process.cwd(),
|
|
108
|
+
env = process.env,
|
|
109
|
+
listActiveSessionsFn = listActiveSessions,
|
|
110
|
+
} = {}) {
|
|
111
|
+
const explicit = normalizeTrustedUsageSessionId(explicitSessionId, "--notify-session");
|
|
112
|
+
if (explicit) {
|
|
113
|
+
return { sessionId: explicit, source: "explicit" };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const envSession = resolveEnvUsageSession(env);
|
|
117
|
+
if (envSession) {
|
|
118
|
+
return envSession;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
const sessions = await listActiveSessionsFn({ targetPath });
|
|
123
|
+
return selectMostRecentUsageSession(sessions);
|
|
124
|
+
} catch (error) {
|
|
125
|
+
return {
|
|
126
|
+
sessionId: "",
|
|
127
|
+
source: "error",
|
|
128
|
+
reason: "list_active_sessions_failed",
|
|
129
|
+
errorCode: error && typeof error === "object" ? normalizeString(error.code) || null : null,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
}
|