sentinelayer-cli 0.34.2 → 0.34.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 +1 -1
- package/src/commands/session.js +6 -1
- package/src/mcp/cli-command-tools.js +2 -0
- package/src/mcp/cli-registry.js +246 -231
- package/src/session/sync.js +6 -1
package/package.json
CHANGED
package/src/commands/session.js
CHANGED
|
@@ -783,9 +783,14 @@ function mergeSessionActionEvents(events = [], actionEvents = []) {
|
|
|
783
783
|
return dedupeSessionEvents([...(Array.isArray(events) ? events : []), ...actionEvents])
|
|
784
784
|
.map((event, index) => ({ event, index }))
|
|
785
785
|
.sort((left, right) => {
|
|
786
|
+
const leftSequence = eventSequenceNumber(left.event);
|
|
787
|
+
const rightSequence = eventSequenceNumber(right.event);
|
|
788
|
+
if (leftSequence > 0 && rightSequence > 0 && leftSequence !== rightSequence) {
|
|
789
|
+
return leftSequence - rightSequence;
|
|
790
|
+
}
|
|
786
791
|
const timeDiff = eventTimestampMs(left.event) - eventTimestampMs(right.event);
|
|
787
792
|
if (timeDiff !== 0) return timeDiff;
|
|
788
|
-
const sequenceDiff =
|
|
793
|
+
const sequenceDiff = leftSequence - rightSequence;
|
|
789
794
|
if (sequenceDiff !== 0) return sequenceDiff;
|
|
790
795
|
return left.index - right.index;
|
|
791
796
|
})
|
|
@@ -293,11 +293,13 @@ export async function buildCliCommandMcpTools({
|
|
|
293
293
|
generatedAt = "1970-01-01T00:00:00.000Z",
|
|
294
294
|
program = null,
|
|
295
295
|
includeHidden = false,
|
|
296
|
+
includeSensitive = false,
|
|
296
297
|
} = {}) {
|
|
297
298
|
const registry = await buildRegistryTemplateFn({
|
|
298
299
|
generatedAt,
|
|
299
300
|
program: program || (buildProgramFn ? await buildProgramFn() : null),
|
|
300
301
|
includeHidden,
|
|
302
|
+
includeSensitive,
|
|
301
303
|
});
|
|
302
304
|
return (Array.isArray(registry?.tools) ? registry.tools : [])
|
|
303
305
|
.map(normalizeRegistryTool)
|
package/src/mcp/cli-registry.js
CHANGED
|
@@ -1,231 +1,246 @@
|
|
|
1
|
-
import { buildCliProgram } from "../cli.js";
|
|
2
|
-
import { MCP_TOOL_REGISTRY_SCHEMA_VERSION } from "./registry.js";
|
|
3
|
-
|
|
4
|
-
const CLI_TOOL_PREFIX = "sl";
|
|
5
|
-
const CLI_TOOL_TIMEOUT_MS = 60_000;
|
|
6
|
-
const CLI_TOOL_MAX_CALLS_PER_RUN = 5;
|
|
7
|
-
|
|
8
|
-
function normalizeDescription(value, fallback) {
|
|
9
|
-
const normalized = String(value || "").trim();
|
|
10
|
-
return normalized || fallback;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function toTitle(value) {
|
|
14
|
-
return String(value || "")
|
|
15
|
-
.split(/[\s_.:-]+/g)
|
|
16
|
-
.filter(Boolean)
|
|
17
|
-
.map((part) => part.slice(0, 1).toUpperCase() + part.slice(1))
|
|
18
|
-
.join(" ");
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function optionPropertyName(option) {
|
|
22
|
-
if (typeof option.attributeName === "function") {
|
|
23
|
-
return option.attributeName();
|
|
24
|
-
}
|
|
25
|
-
const long = String(option.long || option.flags || "")
|
|
26
|
-
.split(/[ ,|]+/g)
|
|
27
|
-
.find((part) => part.startsWith("--"));
|
|
28
|
-
return String(long || "")
|
|
29
|
-
.replace(/^--no-/, "")
|
|
30
|
-
.replace(/^--/, "")
|
|
31
|
-
.replace(/-([a-zA-Z0-9])/g, (_, char) => char.toUpperCase());
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function schemaForArgument(argument) {
|
|
35
|
-
const description = normalizeDescription(argument.description, "");
|
|
36
|
-
if (argument.variadic) {
|
|
37
|
-
return {
|
|
38
|
-
type: "array",
|
|
39
|
-
items: { type: "string" },
|
|
40
|
-
...(description ? { description } : {}),
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
return {
|
|
44
|
-
type: "string",
|
|
45
|
-
...(description ? { description } : {}),
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function optionTakesValue(option) {
|
|
50
|
-
return Boolean(option.required || option.optional || option.variadic);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function schemaForOption(option) {
|
|
54
|
-
const description = normalizeDescription(option.description, "");
|
|
55
|
-
let schema;
|
|
56
|
-
if (option.variadic) {
|
|
57
|
-
schema = {
|
|
58
|
-
type: "array",
|
|
59
|
-
items: { type: "string" },
|
|
60
|
-
};
|
|
61
|
-
} else if (optionTakesValue(option)) {
|
|
62
|
-
schema = { type: "string" };
|
|
63
|
-
} else {
|
|
64
|
-
schema = { type: "boolean" };
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
if (description) {
|
|
68
|
-
schema.description = description;
|
|
69
|
-
}
|
|
70
|
-
if (
|
|
71
|
-
option.defaultValue !== undefined &&
|
|
72
|
-
["string", "number", "boolean"].includes(typeof option.defaultValue)
|
|
73
|
-
) {
|
|
74
|
-
schema.default = option.defaultValue;
|
|
75
|
-
}
|
|
76
|
-
return schema;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function summarizeArgument(argument) {
|
|
80
|
-
return {
|
|
81
|
-
name: argument.name(),
|
|
82
|
-
required: Boolean(argument.required),
|
|
83
|
-
variadic: Boolean(argument.variadic),
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function summarizeOption(option) {
|
|
88
|
-
return {
|
|
89
|
-
name: optionPropertyName(option),
|
|
90
|
-
flags: option.flags,
|
|
91
|
-
required: Boolean(option.mandatory),
|
|
92
|
-
takes_value: optionTakesValue(option),
|
|
93
|
-
variadic: Boolean(option.variadic),
|
|
94
|
-
negated: Boolean(option.negate),
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function isHiddenCommand(command) {
|
|
99
|
-
return Boolean(command.hidden);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function commandPath(command) {
|
|
103
|
-
const names = [];
|
|
104
|
-
let current = command;
|
|
105
|
-
while (current && current.parent) {
|
|
106
|
-
names.unshift(current.name());
|
|
107
|
-
current = current.parent;
|
|
108
|
-
}
|
|
109
|
-
return names;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
1
|
+
import { buildCliProgram } from "../cli.js";
|
|
2
|
+
import { MCP_TOOL_REGISTRY_SCHEMA_VERSION } from "./registry.js";
|
|
3
|
+
|
|
4
|
+
const CLI_TOOL_PREFIX = "sl";
|
|
5
|
+
const CLI_TOOL_TIMEOUT_MS = 60_000;
|
|
6
|
+
const CLI_TOOL_MAX_CALLS_PER_RUN = 5;
|
|
7
|
+
|
|
8
|
+
function normalizeDescription(value, fallback) {
|
|
9
|
+
const normalized = String(value || "").trim();
|
|
10
|
+
return normalized || fallback;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function toTitle(value) {
|
|
14
|
+
return String(value || "")
|
|
15
|
+
.split(/[\s_.:-]+/g)
|
|
16
|
+
.filter(Boolean)
|
|
17
|
+
.map((part) => part.slice(0, 1).toUpperCase() + part.slice(1))
|
|
18
|
+
.join(" ");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function optionPropertyName(option) {
|
|
22
|
+
if (typeof option.attributeName === "function") {
|
|
23
|
+
return option.attributeName();
|
|
24
|
+
}
|
|
25
|
+
const long = String(option.long || option.flags || "")
|
|
26
|
+
.split(/[ ,|]+/g)
|
|
27
|
+
.find((part) => part.startsWith("--"));
|
|
28
|
+
return String(long || "")
|
|
29
|
+
.replace(/^--no-/, "")
|
|
30
|
+
.replace(/^--/, "")
|
|
31
|
+
.replace(/-([a-zA-Z0-9])/g, (_, char) => char.toUpperCase());
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function schemaForArgument(argument) {
|
|
35
|
+
const description = normalizeDescription(argument.description, "");
|
|
36
|
+
if (argument.variadic) {
|
|
37
|
+
return {
|
|
38
|
+
type: "array",
|
|
39
|
+
items: { type: "string" },
|
|
40
|
+
...(description ? { description } : {}),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
type: "string",
|
|
45
|
+
...(description ? { description } : {}),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function optionTakesValue(option) {
|
|
50
|
+
return Boolean(option.required || option.optional || option.variadic);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function schemaForOption(option) {
|
|
54
|
+
const description = normalizeDescription(option.description, "");
|
|
55
|
+
let schema;
|
|
56
|
+
if (option.variadic) {
|
|
57
|
+
schema = {
|
|
58
|
+
type: "array",
|
|
59
|
+
items: { type: "string" },
|
|
60
|
+
};
|
|
61
|
+
} else if (optionTakesValue(option)) {
|
|
62
|
+
schema = { type: "string" };
|
|
63
|
+
} else {
|
|
64
|
+
schema = { type: "boolean" };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (description) {
|
|
68
|
+
schema.description = description;
|
|
69
|
+
}
|
|
70
|
+
if (
|
|
71
|
+
option.defaultValue !== undefined &&
|
|
72
|
+
["string", "number", "boolean"].includes(typeof option.defaultValue)
|
|
73
|
+
) {
|
|
74
|
+
schema.default = option.defaultValue;
|
|
75
|
+
}
|
|
76
|
+
return schema;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function summarizeArgument(argument) {
|
|
80
|
+
return {
|
|
81
|
+
name: argument.name(),
|
|
82
|
+
required: Boolean(argument.required),
|
|
83
|
+
variadic: Boolean(argument.variadic),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function summarizeOption(option) {
|
|
88
|
+
return {
|
|
89
|
+
name: optionPropertyName(option),
|
|
90
|
+
flags: option.flags,
|
|
91
|
+
required: Boolean(option.mandatory),
|
|
92
|
+
takes_value: optionTakesValue(option),
|
|
93
|
+
variadic: Boolean(option.variadic),
|
|
94
|
+
negated: Boolean(option.negate),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function isHiddenCommand(command) {
|
|
99
|
+
return Boolean(command.hidden);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function commandPath(command) {
|
|
103
|
+
const names = [];
|
|
104
|
+
let current = command;
|
|
105
|
+
while (current && current.parent) {
|
|
106
|
+
names.unshift(current.name());
|
|
107
|
+
current = current.parent;
|
|
108
|
+
}
|
|
109
|
+
return names;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Command groups excluded from the generated MCP bridge tool set: they read or mutate
|
|
113
|
+
// credentials / config secrets and must not be remotely tool-callable without dedicated
|
|
114
|
+
// server-side auth. Defense-in-depth beyond per-command output redaction. Opt back in with
|
|
115
|
+
// includeSensitive for local/trusted contexts.
|
|
116
|
+
const SENSITIVE_COMMAND_GROUPS = new Set(["config", "auth"]);
|
|
117
|
+
|
|
118
|
+
function isSensitiveCommand(command) {
|
|
119
|
+
const parts = commandPath(command);
|
|
120
|
+
return parts.length > 0 && SENSITIVE_COMMAND_GROUPS.has(parts[0]);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function collectLeafCommands(command, { includeHidden = false, includeSensitive = false } = {}) {
|
|
124
|
+
const leaves = [];
|
|
125
|
+
for (const child of command.commands || []) {
|
|
126
|
+
if (!includeHidden && isHiddenCommand(child)) {
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (!includeSensitive && isSensitiveCommand(child)) {
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (child.commands && child.commands.length > 0) {
|
|
133
|
+
leaves.push(...collectLeafCommands(child, { includeHidden, includeSensitive }));
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
leaves.push(child);
|
|
137
|
+
}
|
|
138
|
+
return leaves;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function buildToolInputSchema(command) {
|
|
142
|
+
const properties = {};
|
|
143
|
+
const required = [];
|
|
144
|
+
|
|
145
|
+
for (const argument of command.registeredArguments || []) {
|
|
146
|
+
const name = argument.name();
|
|
147
|
+
properties[name] = schemaForArgument(argument);
|
|
148
|
+
if (argument.required) {
|
|
149
|
+
required.push(name);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
for (const option of command.options || []) {
|
|
154
|
+
const name = optionPropertyName(option);
|
|
155
|
+
if (!name || properties[name]) {
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
properties[name] = schemaForOption(option);
|
|
159
|
+
if (option.mandatory) {
|
|
160
|
+
required.push(name);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
type: "object",
|
|
166
|
+
additionalProperties: false,
|
|
167
|
+
...(required.length > 0 ? { required } : {}),
|
|
168
|
+
properties,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function buildCliTool(command) {
|
|
173
|
+
const pathParts = commandPath(command);
|
|
174
|
+
const commandName = pathParts.join(" ");
|
|
175
|
+
const dottedName = pathParts.join(".");
|
|
176
|
+
const toolName = `${CLI_TOOL_PREFIX}.${dottedName}`;
|
|
177
|
+
return {
|
|
178
|
+
name: toolName,
|
|
179
|
+
title: toTitle(toolName),
|
|
180
|
+
description: normalizeDescription(
|
|
181
|
+
command.description(),
|
|
182
|
+
`Run sentinelayer-cli ${commandName}.`
|
|
183
|
+
),
|
|
184
|
+
input_schema: buildToolInputSchema(command),
|
|
185
|
+
transport: {
|
|
186
|
+
type: "bridge",
|
|
187
|
+
method: "POST",
|
|
188
|
+
url: `sentinelayer://cli/${dottedName}`,
|
|
189
|
+
timeout_ms: CLI_TOOL_TIMEOUT_MS,
|
|
190
|
+
auth: { mode: "none" },
|
|
191
|
+
},
|
|
192
|
+
budgets: {
|
|
193
|
+
max_calls_per_run: CLI_TOOL_MAX_CALLS_PER_RUN,
|
|
194
|
+
max_runtime_ms: CLI_TOOL_TIMEOUT_MS,
|
|
195
|
+
},
|
|
196
|
+
security: {
|
|
197
|
+
requires_human_approval: true,
|
|
198
|
+
kill_switch: "enabled",
|
|
199
|
+
scopes: ["cli:execute"],
|
|
200
|
+
},
|
|
201
|
+
metadata: {
|
|
202
|
+
provider: "sentinelayer",
|
|
203
|
+
adapter: "sentinelayer-cli",
|
|
204
|
+
command: commandName,
|
|
205
|
+
argv: pathParts,
|
|
206
|
+
generated_from: "commander",
|
|
207
|
+
execution: "bridge",
|
|
208
|
+
arguments: (command.registeredArguments || []).map(summarizeArgument),
|
|
209
|
+
options: (command.options || []).map(summarizeOption),
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Build an MCP tool registry for the installed SentinelLayer CLI command tree.
|
|
216
|
+
*
|
|
217
|
+
* The generated registry is intentionally execution-policy-first: every tool is
|
|
218
|
+
* marked as requiring human approval because this is the full CLI surface.
|
|
219
|
+
*
|
|
220
|
+
* @param {{ generatedAt?: string, program?: import("commander").Command, includeHidden?: boolean, includeSensitive?: boolean }} [options]
|
|
221
|
+
* @returns {Promise<Record<string, any>>}
|
|
222
|
+
*/
|
|
223
|
+
export async function buildSentinelayerCliRegistryTemplate({
|
|
224
|
+
generatedAt = new Date().toISOString(),
|
|
225
|
+
program = null,
|
|
226
|
+
includeHidden = false,
|
|
227
|
+
includeSensitive = false,
|
|
228
|
+
} = {}) {
|
|
229
|
+
const cliProgram =
|
|
230
|
+
program ||
|
|
231
|
+
(await buildCliProgram({
|
|
232
|
+
invokeLegacy: async () => {},
|
|
233
|
+
}));
|
|
234
|
+
const tools = collectLeafCommands(cliProgram, { includeHidden, includeSensitive }).map(buildCliTool);
|
|
235
|
+
return {
|
|
236
|
+
version: MCP_TOOL_REGISTRY_SCHEMA_VERSION,
|
|
237
|
+
generated_at: generatedAt,
|
|
238
|
+
tools,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export const __cliRegistryInternals = Object.freeze({
|
|
243
|
+
collectLeafCommands,
|
|
244
|
+
buildToolInputSchema,
|
|
245
|
+
optionPropertyName,
|
|
246
|
+
});
|
package/src/session/sync.js
CHANGED
|
@@ -253,9 +253,14 @@ function chronologicalSessionEvents(events = []) {
|
|
|
253
253
|
return (Array.isArray(events) ? events : [])
|
|
254
254
|
.map((event, index) => ({ event, index }))
|
|
255
255
|
.sort((left, right) => {
|
|
256
|
+
const leftSequence = eventSequenceNumber(left.event);
|
|
257
|
+
const rightSequence = eventSequenceNumber(right.event);
|
|
258
|
+
if (leftSequence > 0 && rightSequence > 0 && leftSequence !== rightSequence) {
|
|
259
|
+
return leftSequence - rightSequence;
|
|
260
|
+
}
|
|
256
261
|
const timeDiff = eventTimestampMs(left.event) - eventTimestampMs(right.event);
|
|
257
262
|
if (timeDiff !== 0) return timeDiff;
|
|
258
|
-
const sequenceDiff =
|
|
263
|
+
const sequenceDiff = leftSequence - rightSequence;
|
|
259
264
|
if (sequenceDiff !== 0) return sequenceDiff;
|
|
260
265
|
return left.index - right.index;
|
|
261
266
|
})
|