sentinelayer-cli 0.39.2 → 0.41.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 +4 -4
- package/package.json +2 -3
- package/src/agents/ai-governance/tools/base.js +19 -166
- package/src/agents/backend/tools/base.js +33 -186
- package/src/agents/code-quality/tools/base.js +27 -154
- package/src/agents/data-layer/tools/base.js +35 -180
- package/src/agents/documentation/tools/base.js +18 -146
- package/src/agents/infrastructure/tools/base.js +17 -164
- package/src/agents/observability/tools/base.js +35 -180
- package/src/agents/performance/tools/base.js +35 -205
- package/src/agents/release/tools/base.js +35 -180
- package/src/agents/reliability/tools/base.js +35 -180
- package/src/agents/security/tools/base.js +33 -187
- package/src/agents/shared-tools/domain-base.js +246 -0
- package/src/agents/supply-chain/tools/base.js +15 -144
- package/src/agents/testing/tools/base.js +33 -184
- package/src/cli.js +5 -0
- package/src/commands/mcp.js +1 -1
- package/src/commands/ring-owner.js +58 -0
- package/src/commands/session.js +703 -263
- package/src/engram/governance.js +67 -0
- package/src/engram/index.js +64 -0
- package/src/engram/namespace.js +89 -0
- package/src/engram/observations.js +34 -0
- package/src/engram/sla.js +104 -0
- package/src/engram/store.js +115 -0
- package/src/engram/tools.js +146 -0
- package/src/engram/trust.js +105 -0
- package/src/legacy-cli.js +6 -3
- package/src/mcp/registry.js +27 -10
- package/src/mcp/session-stdio-server.js +209 -18
- package/src/mcp/smoke.js +9 -8
- package/src/pocket/ring-owner.js +97 -0
- package/src/review/local-review.js +9 -5
- package/src/review/omargate-orchestrator.js +111 -14
- package/src/review/spec-binding.js +16 -1
- package/src/scan/generator.js +1 -1
- package/src/session/agent-registry.js +22 -63
- package/src/session/checkpoints.js +194 -0
- package/src/session/coordination-guidance.js +6 -6
- package/src/session/daemon.js +19 -124
- package/src/session/file-lease-integrations.js +1300 -0
- package/src/session/file-locks.js +967 -464
- package/src/session/listener.js +243 -20
- package/src/session/listeners.js +71 -80
- package/src/session/paths.js +2 -0
- package/src/session/recall/embedder.js +181 -0
- package/src/session/recall/entities.js +0 -0
- package/src/session/recall/index-build.js +38 -0
- package/src/session/recall/index-core.js +108 -0
- package/src/session/recall/index.js +131 -0
- package/src/session/recall/needle-eval.js +279 -0
- package/src/session/recall/observation-core.js +183 -0
- package/src/session/recall/observations.js +21 -0
- package/src/session/recall/provenance.js +153 -0
- package/src/session/recall/retrieve.js +374 -0
- package/src/session/recall/text.js +0 -0
- package/src/session/recall/token-stats.js +81 -0
- package/src/session/recap.js +29 -23
- package/src/session/remote-hydrate.js +5 -1
- package/src/session/sync.js +661 -11
- package/src/session/wake/pump.js +8 -3
|
@@ -1,151 +1,22 @@
|
|
|
1
|
-
// Shared helpers for
|
|
1
|
+
// Shared helpers for Dima's (supply-chain) domain tools (#A22).
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
import {
|
|
4
|
+
createFinding as createDomainFinding,
|
|
5
|
+
toPosix,
|
|
6
|
+
walkRepoFiles as walkDomainRepoFiles,
|
|
7
|
+
} from "../../shared-tools/domain-base.js";
|
|
6
8
|
|
|
7
|
-
|
|
9
|
+
const DEFAULT_PERSONA = "supply-chain";
|
|
8
10
|
|
|
9
|
-
|
|
10
|
-
".git",
|
|
11
|
-
"node_modules",
|
|
12
|
-
".venv",
|
|
13
|
-
".next",
|
|
14
|
-
"dist",
|
|
15
|
-
"build",
|
|
16
|
-
"coverage",
|
|
17
|
-
".sentinelayer",
|
|
18
|
-
".sentinel",
|
|
19
|
-
".turbo",
|
|
20
|
-
".idea",
|
|
21
|
-
".vscode",
|
|
22
|
-
"__pycache__",
|
|
23
|
-
".cache",
|
|
24
|
-
]);
|
|
25
|
-
const MAX_FILE_SIZE_BYTES = 1024 * 1024;
|
|
26
|
-
const SEVERITIES = Object.freeze(["P0", "P1", "P2", "P3"]);
|
|
11
|
+
export { toPosix };
|
|
27
12
|
|
|
28
|
-
export function
|
|
29
|
-
return
|
|
13
|
+
export function createFinding(options = {}) {
|
|
14
|
+
return createDomainFinding(options, {
|
|
15
|
+
defaultPersona: DEFAULT_PERSONA,
|
|
16
|
+
defaultKind: DEFAULT_PERSONA,
|
|
17
|
+
});
|
|
30
18
|
}
|
|
31
19
|
|
|
32
|
-
export function
|
|
33
|
-
|
|
34
|
-
kind,
|
|
35
|
-
file,
|
|
36
|
-
line = 0,
|
|
37
|
-
evidence = "",
|
|
38
|
-
rootCause = "",
|
|
39
|
-
recommendedFix = "",
|
|
40
|
-
confidence = null,
|
|
41
|
-
tool = "",
|
|
42
|
-
persona = "supply-chain",
|
|
43
|
-
} = {}) {
|
|
44
|
-
const normalizedSeverity = SEVERITIES.includes(String(severity || "").toUpperCase())
|
|
45
|
-
? String(severity).toUpperCase()
|
|
46
|
-
: "P2";
|
|
47
|
-
return {
|
|
48
|
-
persona,
|
|
49
|
-
tool: String(tool || "").trim(),
|
|
50
|
-
kind: String(kind || "").trim() || "supply-chain",
|
|
51
|
-
severity: normalizedSeverity,
|
|
52
|
-
file: toPosix(file || ""),
|
|
53
|
-
line: Number.isFinite(Number(line)) ? Math.max(0, Math.floor(Number(line))) : 0,
|
|
54
|
-
evidence: String(evidence || "").trim().slice(0, 400),
|
|
55
|
-
rootCause: String(rootCause || "").trim(),
|
|
56
|
-
recommendedFix: String(recommendedFix || "").trim(),
|
|
57
|
-
confidence:
|
|
58
|
-
confidence === null || confidence === undefined
|
|
59
|
-
? null
|
|
60
|
-
: Math.max(0, Math.min(1, Number(confidence) || 0)),
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
async function readIgnorePatterns(filePath) {
|
|
65
|
-
try {
|
|
66
|
-
const raw = await fsp.readFile(filePath, "utf-8");
|
|
67
|
-
return String(raw || "")
|
|
68
|
-
.split(/\r?\n/)
|
|
69
|
-
.map((line) => line.trim())
|
|
70
|
-
.filter((line) => line && !line.startsWith("#"));
|
|
71
|
-
} catch (err) {
|
|
72
|
-
if (err && typeof err === "object" && err.code === "ENOENT") {
|
|
73
|
-
return [];
|
|
74
|
-
}
|
|
75
|
-
throw err;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
async function createIgnoreMatcher(rootPath) {
|
|
80
|
-
const matcher = ignore();
|
|
81
|
-
const gitignore = await readIgnorePatterns(path.join(rootPath, ".gitignore"));
|
|
82
|
-
const sentinel = await readIgnorePatterns(
|
|
83
|
-
path.join(rootPath, ".sentinelayerignore")
|
|
84
|
-
);
|
|
85
|
-
matcher.add([...gitignore, ...sentinel]);
|
|
86
|
-
return (relativePath, isDirectory) => {
|
|
87
|
-
const normalized = toPosix(relativePath);
|
|
88
|
-
if (!normalized) {
|
|
89
|
-
return false;
|
|
90
|
-
}
|
|
91
|
-
const candidate = isDirectory ? `${normalized}/` : normalized;
|
|
92
|
-
return matcher.ignores(candidate);
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
export async function* walkRepoFiles({
|
|
97
|
-
rootPath = process.cwd(),
|
|
98
|
-
extensions = new Set(),
|
|
99
|
-
maxFileSize = MAX_FILE_SIZE_BYTES,
|
|
100
|
-
} = {}) {
|
|
101
|
-
const resolvedRoot = path.resolve(rootPath);
|
|
102
|
-
const ignoreMatcher = await createIgnoreMatcher(resolvedRoot);
|
|
103
|
-
const wantedExtensions =
|
|
104
|
-
extensions instanceof Set
|
|
105
|
-
? extensions
|
|
106
|
-
: new Set(Array.isArray(extensions) ? extensions : []);
|
|
107
|
-
const stack = [resolvedRoot];
|
|
108
|
-
while (stack.length > 0) {
|
|
109
|
-
const current = stack.pop();
|
|
110
|
-
let entries = [];
|
|
111
|
-
try {
|
|
112
|
-
entries = await fsp.readdir(current, { withFileTypes: true });
|
|
113
|
-
} catch {
|
|
114
|
-
continue;
|
|
115
|
-
}
|
|
116
|
-
for (const entry of entries) {
|
|
117
|
-
const fullPath = path.join(current, entry.name);
|
|
118
|
-
const relativePath = toPosix(path.relative(resolvedRoot, fullPath));
|
|
119
|
-
if (entry.isDirectory()) {
|
|
120
|
-
if (!relativePath || DEFAULT_IGNORED_DIRS.has(entry.name)) {
|
|
121
|
-
continue;
|
|
122
|
-
}
|
|
123
|
-
if (ignoreMatcher(relativePath, true)) {
|
|
124
|
-
continue;
|
|
125
|
-
}
|
|
126
|
-
stack.push(fullPath);
|
|
127
|
-
continue;
|
|
128
|
-
}
|
|
129
|
-
if (!entry.isFile()) {
|
|
130
|
-
continue;
|
|
131
|
-
}
|
|
132
|
-
if (ignoreMatcher(relativePath, false)) {
|
|
133
|
-
continue;
|
|
134
|
-
}
|
|
135
|
-
const ext = path.extname(entry.name).toLowerCase();
|
|
136
|
-
if (wantedExtensions.size > 0 && !wantedExtensions.has(ext) && !wantedExtensions.has("")) {
|
|
137
|
-
continue;
|
|
138
|
-
}
|
|
139
|
-
let stat = null;
|
|
140
|
-
try {
|
|
141
|
-
stat = await fsp.stat(fullPath);
|
|
142
|
-
} catch {
|
|
143
|
-
stat = null;
|
|
144
|
-
}
|
|
145
|
-
if (!stat || stat.size > maxFileSize) {
|
|
146
|
-
continue;
|
|
147
|
-
}
|
|
148
|
-
yield { fullPath, relativePath };
|
|
149
|
-
}
|
|
150
|
-
}
|
|
20
|
+
export async function* walkRepoFiles(options = {}) {
|
|
21
|
+
yield* walkDomainRepoFiles(options);
|
|
151
22
|
}
|
|
@@ -1,188 +1,18 @@
|
|
|
1
1
|
// Shared helpers for Priya's (testing) domain tools (#A15).
|
|
2
|
-
// Same pattern as security/tools/base.js and backend/tools/base.js — default
|
|
3
|
-
// persona is "testing". Later refactor (#A25+) can hoist to shared-tools/.
|
|
4
2
|
|
|
5
|
-
import
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
import {
|
|
4
|
+
DEFAULT_IGNORED_DIRS,
|
|
5
|
+
MAX_FILE_SIZE_BYTES,
|
|
6
|
+
SEVERITIES,
|
|
7
|
+
createFinding as createDomainFinding,
|
|
8
|
+
findLineMatches,
|
|
9
|
+
getLineContent,
|
|
10
|
+
normalizeSeverity,
|
|
11
|
+
toPosix,
|
|
12
|
+
walkRepoFiles as walkDomainRepoFiles,
|
|
13
|
+
} from "../../shared-tools/domain-base.js";
|
|
8
14
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const DEFAULT_IGNORED_DIRS = new Set([
|
|
12
|
-
".git",
|
|
13
|
-
"node_modules",
|
|
14
|
-
".venv",
|
|
15
|
-
".next",
|
|
16
|
-
"dist",
|
|
17
|
-
"build",
|
|
18
|
-
"coverage",
|
|
19
|
-
".sentinelayer",
|
|
20
|
-
".sentinel",
|
|
21
|
-
".turbo",
|
|
22
|
-
".idea",
|
|
23
|
-
".vscode",
|
|
24
|
-
"__pycache__",
|
|
25
|
-
".cache",
|
|
26
|
-
]);
|
|
27
|
-
const MAX_FILE_SIZE_BYTES = 1024 * 1024;
|
|
28
|
-
const SEVERITIES = Object.freeze(["P0", "P1", "P2", "P3"]);
|
|
29
|
-
|
|
30
|
-
export function toPosix(value) {
|
|
31
|
-
return String(value || "").replace(/\\/g, "/");
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export function normalizeSeverity(value) {
|
|
35
|
-
const normalized = String(value || "").trim().toUpperCase();
|
|
36
|
-
if (SEVERITIES.includes(normalized)) {
|
|
37
|
-
return normalized;
|
|
38
|
-
}
|
|
39
|
-
return "P2";
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export function createFinding({
|
|
43
|
-
severity,
|
|
44
|
-
kind,
|
|
45
|
-
file,
|
|
46
|
-
line = 0,
|
|
47
|
-
evidence = "",
|
|
48
|
-
rootCause = "",
|
|
49
|
-
recommendedFix = "",
|
|
50
|
-
confidence = null,
|
|
51
|
-
tool = "",
|
|
52
|
-
persona = "testing",
|
|
53
|
-
} = {}) {
|
|
54
|
-
return {
|
|
55
|
-
persona,
|
|
56
|
-
tool: String(tool || "").trim(),
|
|
57
|
-
kind: String(kind || "").trim() || "testing",
|
|
58
|
-
severity: normalizeSeverity(severity),
|
|
59
|
-
file: toPosix(file || ""),
|
|
60
|
-
line: Number.isFinite(Number(line)) ? Math.max(0, Math.floor(Number(line))) : 0,
|
|
61
|
-
evidence: String(evidence || "").trim().slice(0, 400),
|
|
62
|
-
rootCause: String(rootCause || "").trim(),
|
|
63
|
-
recommendedFix: String(recommendedFix || "").trim(),
|
|
64
|
-
confidence:
|
|
65
|
-
confidence === null || confidence === undefined
|
|
66
|
-
? null
|
|
67
|
-
: Math.max(0, Math.min(1, Number(confidence) || 0)),
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
async function readIgnorePatterns(filePath) {
|
|
72
|
-
try {
|
|
73
|
-
const raw = await fsp.readFile(filePath, "utf-8");
|
|
74
|
-
return String(raw || "")
|
|
75
|
-
.split(/\r?\n/)
|
|
76
|
-
.map((line) => line.trim())
|
|
77
|
-
.filter((line) => line && !line.startsWith("#"));
|
|
78
|
-
} catch (err) {
|
|
79
|
-
if (err && typeof err === "object" && err.code === "ENOENT") {
|
|
80
|
-
return [];
|
|
81
|
-
}
|
|
82
|
-
throw err;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
async function createIgnoreMatcher(rootPath) {
|
|
87
|
-
const matcher = ignore();
|
|
88
|
-
const gitignore = await readIgnorePatterns(path.join(rootPath, ".gitignore"));
|
|
89
|
-
const sentinel = await readIgnorePatterns(
|
|
90
|
-
path.join(rootPath, ".sentinelayerignore")
|
|
91
|
-
);
|
|
92
|
-
matcher.add([...gitignore, ...sentinel]);
|
|
93
|
-
return (relativePath, isDirectory) => {
|
|
94
|
-
const normalized = toPosix(relativePath);
|
|
95
|
-
if (!normalized) {
|
|
96
|
-
return false;
|
|
97
|
-
}
|
|
98
|
-
const candidate = isDirectory ? `${normalized}/` : normalized;
|
|
99
|
-
return matcher.ignores(candidate);
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
export async function* walkRepoFiles({
|
|
104
|
-
rootPath = process.cwd(),
|
|
105
|
-
extensions = new Set(),
|
|
106
|
-
maxFileSize = MAX_FILE_SIZE_BYTES,
|
|
107
|
-
} = {}) {
|
|
108
|
-
const resolvedRoot = path.resolve(rootPath);
|
|
109
|
-
const ignoreMatcher = await createIgnoreMatcher(resolvedRoot);
|
|
110
|
-
const wantedExtensions =
|
|
111
|
-
extensions instanceof Set
|
|
112
|
-
? extensions
|
|
113
|
-
: new Set(Array.isArray(extensions) ? extensions : []);
|
|
114
|
-
const stack = [resolvedRoot];
|
|
115
|
-
while (stack.length > 0) {
|
|
116
|
-
const current = stack.pop();
|
|
117
|
-
let entries = [];
|
|
118
|
-
try {
|
|
119
|
-
entries = await fsp.readdir(current, { withFileTypes: true });
|
|
120
|
-
} catch {
|
|
121
|
-
continue;
|
|
122
|
-
}
|
|
123
|
-
for (const entry of entries) {
|
|
124
|
-
const fullPath = path.join(current, entry.name);
|
|
125
|
-
const relativePath = toPosix(path.relative(resolvedRoot, fullPath));
|
|
126
|
-
if (entry.isDirectory()) {
|
|
127
|
-
if (!relativePath || DEFAULT_IGNORED_DIRS.has(entry.name)) {
|
|
128
|
-
continue;
|
|
129
|
-
}
|
|
130
|
-
if (ignoreMatcher(relativePath, true)) {
|
|
131
|
-
continue;
|
|
132
|
-
}
|
|
133
|
-
stack.push(fullPath);
|
|
134
|
-
continue;
|
|
135
|
-
}
|
|
136
|
-
if (!entry.isFile()) {
|
|
137
|
-
continue;
|
|
138
|
-
}
|
|
139
|
-
if (ignoreMatcher(relativePath, false)) {
|
|
140
|
-
continue;
|
|
141
|
-
}
|
|
142
|
-
const ext = path.extname(entry.name).toLowerCase();
|
|
143
|
-
if (wantedExtensions.size > 0 && !wantedExtensions.has(ext) && !wantedExtensions.has("")) {
|
|
144
|
-
continue;
|
|
145
|
-
}
|
|
146
|
-
let stat = null;
|
|
147
|
-
try {
|
|
148
|
-
stat = await fsp.stat(fullPath);
|
|
149
|
-
} catch {
|
|
150
|
-
stat = null;
|
|
151
|
-
}
|
|
152
|
-
if (!stat || stat.size > maxFileSize) {
|
|
153
|
-
continue;
|
|
154
|
-
}
|
|
155
|
-
yield { fullPath, relativePath, stat };
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
export function findLineMatches(content, pattern) {
|
|
161
|
-
const text = String(content || "");
|
|
162
|
-
if (!pattern) {
|
|
163
|
-
return [];
|
|
164
|
-
}
|
|
165
|
-
const global = new RegExp(
|
|
166
|
-
pattern.source,
|
|
167
|
-
pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`
|
|
168
|
-
);
|
|
169
|
-
const matches = [];
|
|
170
|
-
let match;
|
|
171
|
-
while ((match = global.exec(text)) !== null) {
|
|
172
|
-
const lineIndex = text.slice(0, match.index).split(/\r?\n/).length;
|
|
173
|
-
matches.push({
|
|
174
|
-
index: match.index,
|
|
175
|
-
line: lineIndex,
|
|
176
|
-
match: match[0],
|
|
177
|
-
});
|
|
178
|
-
}
|
|
179
|
-
return matches;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
export function getLineContent(content, line) {
|
|
183
|
-
const lines = String(content || "").split(/\r?\n/);
|
|
184
|
-
return (lines[Math.max(0, (Number(line) || 1) - 1)] || "").trim();
|
|
185
|
-
}
|
|
15
|
+
const DEFAULT_PERSONA = "testing";
|
|
186
16
|
|
|
187
17
|
const TEST_FILE_PATTERNS = [
|
|
188
18
|
/\.test\.(js|jsx|ts|tsx|mjs|cjs)$/,
|
|
@@ -194,9 +24,28 @@ const TEST_FILE_PATTERNS = [
|
|
|
194
24
|
/(^|\/)tests?\/[^/]*\.py$/,
|
|
195
25
|
];
|
|
196
26
|
|
|
27
|
+
export {
|
|
28
|
+
DEFAULT_IGNORED_DIRS,
|
|
29
|
+
MAX_FILE_SIZE_BYTES,
|
|
30
|
+
SEVERITIES,
|
|
31
|
+
findLineMatches,
|
|
32
|
+
getLineContent,
|
|
33
|
+
normalizeSeverity,
|
|
34
|
+
toPosix,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export function createFinding(options = {}) {
|
|
38
|
+
return createDomainFinding(options, {
|
|
39
|
+
defaultPersona: DEFAULT_PERSONA,
|
|
40
|
+
defaultKind: DEFAULT_PERSONA,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function* walkRepoFiles(options = {}) {
|
|
45
|
+
yield* walkDomainRepoFiles(options, { entryMetadata: "stat" });
|
|
46
|
+
}
|
|
47
|
+
|
|
197
48
|
export function isTestFile(relativePath) {
|
|
198
49
|
const normalized = toPosix(relativePath);
|
|
199
50
|
return TEST_FILE_PATTERNS.some((pattern) => pattern.test(normalized));
|
|
200
51
|
}
|
|
201
|
-
|
|
202
|
-
export { DEFAULT_IGNORED_DIRS, MAX_FILE_SIZE_BYTES, SEVERITIES };
|
package/src/cli.js
CHANGED
|
@@ -124,6 +124,11 @@ const COMMAND_REGISTRARS = {
|
|
|
124
124
|
exportName: "registerSessionCommand",
|
|
125
125
|
needsLegacy: false,
|
|
126
126
|
},
|
|
127
|
+
"ring-owner": {
|
|
128
|
+
loader: () => import("./commands/ring-owner.js"),
|
|
129
|
+
exportName: "registerRingOwnerCommand",
|
|
130
|
+
needsLegacy: false,
|
|
131
|
+
},
|
|
127
132
|
};
|
|
128
133
|
|
|
129
134
|
const COMMAND_SET = new Set(Object.keys(COMMAND_REGISTRARS));
|
package/src/commands/mcp.js
CHANGED
|
@@ -236,7 +236,7 @@ Docs: https://github.com/mrrCarter/create-sentinelayer/blob/main/docs/mcp.md`,
|
|
|
236
236
|
.description(
|
|
237
237
|
"Smoke the hosted MCP resource with an in-memory bearer; prints only redacted proof"
|
|
238
238
|
)
|
|
239
|
-
.option("--session <id>", "Optional Senti session id to read through
|
|
239
|
+
.option("--session <id>", "Optional Senti session id to read through sessions_events_list")
|
|
240
240
|
.option("--limit <n>", "Maximum session events to read when --session is set", "5")
|
|
241
241
|
.option(
|
|
242
242
|
"--scope <scopes>",
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import process from "node:process";
|
|
2
|
+
|
|
3
|
+
import { ringOwner } from "../pocket/ring-owner.js";
|
|
4
|
+
|
|
5
|
+
function shouldEmitJson(options, command) {
|
|
6
|
+
if (options && options.json) return true;
|
|
7
|
+
return typeof command?.optsWithGlobals === "function" ? Boolean(command.optsWithGlobals().json) : false;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function registerRingOwnerCommand(program) {
|
|
11
|
+
program
|
|
12
|
+
.command("ring-owner")
|
|
13
|
+
.description("Ring the owner's phone about a decision that needs them (Senti Pocket dial)")
|
|
14
|
+
.argument("<question>", "The question / decision to put to the owner")
|
|
15
|
+
.requiredOption("--session <id>", "The session this decision belongs to")
|
|
16
|
+
.option("--kind <kind>", "decisionYours | pickOption | go | info | checkpointReady", "decisionYours")
|
|
17
|
+
.option(
|
|
18
|
+
"--option <label>",
|
|
19
|
+
"A pickOption choice (repeat for multiple)",
|
|
20
|
+
(value, acc) => { acc.push(value); return acc; },
|
|
21
|
+
[],
|
|
22
|
+
)
|
|
23
|
+
.option("--what-we-need <text>", "A short 'what we need' lead-in for the ring")
|
|
24
|
+
.option("--checkpoint <id>", "An optional checkpoint id for jump-to context")
|
|
25
|
+
.option("--idempotency-key <key>", "Dedupe key so an accidental retry rings only once")
|
|
26
|
+
.option("--gateway-url <url>", "Pocket gateway base URL (else the SENTI_POCKET_URL env var)")
|
|
27
|
+
.option("--json", "Emit machine-readable output")
|
|
28
|
+
.action(async (question, options, command) => {
|
|
29
|
+
const emitJson = shouldEmitJson(options, command);
|
|
30
|
+
try {
|
|
31
|
+
const result = await ringOwner(question, {
|
|
32
|
+
kind: options.kind,
|
|
33
|
+
sessionId: options.session,
|
|
34
|
+
options: options.option || [],
|
|
35
|
+
whatWeNeed: options.whatWeNeed,
|
|
36
|
+
checkpointId: options.checkpoint,
|
|
37
|
+
idempotencyKey: options.idempotencyKey,
|
|
38
|
+
gatewayUrl: options.gatewayUrl,
|
|
39
|
+
env: process.env,
|
|
40
|
+
});
|
|
41
|
+
if (emitJson) {
|
|
42
|
+
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (result && result.dispatched) {
|
|
46
|
+
const replay = result.idempotent ? " (idempotent replay — already ringing)" : "";
|
|
47
|
+
process.stdout.write(`Ringing the owner — dialId ${result.dialId}${replay}\n`);
|
|
48
|
+
} else {
|
|
49
|
+
const reason = result && result.reason ? `: ${result.reason}` : "";
|
|
50
|
+
process.stdout.write(`Not dispatched${reason} (dialId ${(result && result.dialId) || "n/a"})\n`);
|
|
51
|
+
process.exitCode = 1;
|
|
52
|
+
}
|
|
53
|
+
} catch (err) {
|
|
54
|
+
process.stderr.write(`ring-owner failed: ${err && err.message ? err.message : String(err)}\n`);
|
|
55
|
+
process.exitCode = 1;
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
}
|