sentinelayer-cli 0.30.13 → 0.30.14
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 +5 -0
- package/package.json +1 -1
- package/src/commands/mcp.js +34 -0
- package/src/commands/session.js +89 -19
- package/src/mcp/cli-command-tools.js +506 -0
- package/src/mcp/cli-registry.js +231 -0
- package/src/mcp/registry.js +260 -0
- package/src/mcp/session-stdio-server.js +436 -6
- package/src/review/persona-prompts.js +8 -0
- package/src/session/daemon-spawn.js +35 -15
- package/src/session/rotating-log.js +181 -0
- package/src/session/sync.js +7 -1
|
@@ -0,0 +1,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
|
+
function collectLeafCommands(command, { includeHidden = false } = {}) {
|
|
113
|
+
const leaves = [];
|
|
114
|
+
for (const child of command.commands || []) {
|
|
115
|
+
if (!includeHidden && isHiddenCommand(child)) {
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (child.commands && child.commands.length > 0) {
|
|
119
|
+
leaves.push(...collectLeafCommands(child, { includeHidden }));
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
leaves.push(child);
|
|
123
|
+
}
|
|
124
|
+
return leaves;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function buildToolInputSchema(command) {
|
|
128
|
+
const properties = {};
|
|
129
|
+
const required = [];
|
|
130
|
+
|
|
131
|
+
for (const argument of command.registeredArguments || []) {
|
|
132
|
+
const name = argument.name();
|
|
133
|
+
properties[name] = schemaForArgument(argument);
|
|
134
|
+
if (argument.required) {
|
|
135
|
+
required.push(name);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
for (const option of command.options || []) {
|
|
140
|
+
const name = optionPropertyName(option);
|
|
141
|
+
if (!name || properties[name]) {
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
properties[name] = schemaForOption(option);
|
|
145
|
+
if (option.mandatory) {
|
|
146
|
+
required.push(name);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
type: "object",
|
|
152
|
+
additionalProperties: false,
|
|
153
|
+
...(required.length > 0 ? { required } : {}),
|
|
154
|
+
properties,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function buildCliTool(command) {
|
|
159
|
+
const pathParts = commandPath(command);
|
|
160
|
+
const commandName = pathParts.join(" ");
|
|
161
|
+
const dottedName = pathParts.join(".");
|
|
162
|
+
const toolName = `${CLI_TOOL_PREFIX}.${dottedName}`;
|
|
163
|
+
return {
|
|
164
|
+
name: toolName,
|
|
165
|
+
title: toTitle(toolName),
|
|
166
|
+
description: normalizeDescription(
|
|
167
|
+
command.description(),
|
|
168
|
+
`Run sentinelayer-cli ${commandName}.`
|
|
169
|
+
),
|
|
170
|
+
input_schema: buildToolInputSchema(command),
|
|
171
|
+
transport: {
|
|
172
|
+
type: "bridge",
|
|
173
|
+
method: "POST",
|
|
174
|
+
url: `sentinelayer://cli/${dottedName}`,
|
|
175
|
+
timeout_ms: CLI_TOOL_TIMEOUT_MS,
|
|
176
|
+
auth: { mode: "none" },
|
|
177
|
+
},
|
|
178
|
+
budgets: {
|
|
179
|
+
max_calls_per_run: CLI_TOOL_MAX_CALLS_PER_RUN,
|
|
180
|
+
max_runtime_ms: CLI_TOOL_TIMEOUT_MS,
|
|
181
|
+
},
|
|
182
|
+
security: {
|
|
183
|
+
requires_human_approval: true,
|
|
184
|
+
kill_switch: "enabled",
|
|
185
|
+
scopes: ["cli:execute"],
|
|
186
|
+
},
|
|
187
|
+
metadata: {
|
|
188
|
+
provider: "sentinelayer",
|
|
189
|
+
adapter: "sentinelayer-cli",
|
|
190
|
+
command: commandName,
|
|
191
|
+
argv: pathParts,
|
|
192
|
+
generated_from: "commander",
|
|
193
|
+
execution: "bridge",
|
|
194
|
+
arguments: (command.registeredArguments || []).map(summarizeArgument),
|
|
195
|
+
options: (command.options || []).map(summarizeOption),
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Build an MCP tool registry for the installed SentinelLayer CLI command tree.
|
|
202
|
+
*
|
|
203
|
+
* The generated registry is intentionally execution-policy-first: every tool is
|
|
204
|
+
* marked as requiring human approval because this is the full CLI surface.
|
|
205
|
+
*
|
|
206
|
+
* @param {{ generatedAt?: string, program?: import("commander").Command, includeHidden?: boolean }} [options]
|
|
207
|
+
* @returns {Promise<Record<string, any>>}
|
|
208
|
+
*/
|
|
209
|
+
export async function buildSentinelayerCliRegistryTemplate({
|
|
210
|
+
generatedAt = new Date().toISOString(),
|
|
211
|
+
program = null,
|
|
212
|
+
includeHidden = false,
|
|
213
|
+
} = {}) {
|
|
214
|
+
const cliProgram =
|
|
215
|
+
program ||
|
|
216
|
+
(await buildCliProgram({
|
|
217
|
+
invokeLegacy: async () => {},
|
|
218
|
+
}));
|
|
219
|
+
const tools = collectLeafCommands(cliProgram, { includeHidden }).map(buildCliTool);
|
|
220
|
+
return {
|
|
221
|
+
version: MCP_TOOL_REGISTRY_SCHEMA_VERSION,
|
|
222
|
+
generated_at: generatedAt,
|
|
223
|
+
tools,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export const __cliRegistryInternals = Object.freeze({
|
|
228
|
+
collectLeafCommands,
|
|
229
|
+
buildToolInputSchema,
|
|
230
|
+
optionPropertyName,
|
|
231
|
+
});
|
package/src/mcp/registry.js
CHANGED
|
@@ -500,6 +500,266 @@ export function buildSentinelayerSessionRegistryTemplate({ generatedAt = new Dat
|
|
|
500
500
|
server: "sentinelayer-session-mcp",
|
|
501
501
|
},
|
|
502
502
|
},
|
|
503
|
+
{
|
|
504
|
+
name: "session_action",
|
|
505
|
+
title: "Record Senti Session Action",
|
|
506
|
+
description:
|
|
507
|
+
"Record a low-noise message action such as ack, working_on, disregard, view, like, dislike, or reply against a target session event.",
|
|
508
|
+
input_schema: {
|
|
509
|
+
type: "object",
|
|
510
|
+
additionalProperties: false,
|
|
511
|
+
required: ["sessionId", "agentId", "actionType"],
|
|
512
|
+
properties: {
|
|
513
|
+
sessionId: { type: "string" },
|
|
514
|
+
agentId: { type: "string" },
|
|
515
|
+
actionType: {
|
|
516
|
+
type: "string",
|
|
517
|
+
enum: ["ack", "working_on", "reply", "like", "dislike", "disregard", "view"],
|
|
518
|
+
},
|
|
519
|
+
targetSequenceId: { type: "integer", minimum: 1 },
|
|
520
|
+
targetCursor: { type: "string" },
|
|
521
|
+
targetActionId: { type: "string" },
|
|
522
|
+
note: { type: "string" },
|
|
523
|
+
idempotencyKey: { type: "string" },
|
|
524
|
+
timeoutMs: { type: "integer", minimum: 1 },
|
|
525
|
+
dryRun: { type: "boolean" },
|
|
526
|
+
},
|
|
527
|
+
},
|
|
528
|
+
transport: {
|
|
529
|
+
type: "internal",
|
|
530
|
+
method: "POST",
|
|
531
|
+
url: "sentinelayer://session/session_action",
|
|
532
|
+
timeout_ms: 15000,
|
|
533
|
+
auth: { mode: "none" },
|
|
534
|
+
},
|
|
535
|
+
budgets: {
|
|
536
|
+
max_calls_per_run: 40,
|
|
537
|
+
max_runtime_ms: 15000,
|
|
538
|
+
},
|
|
539
|
+
security: {
|
|
540
|
+
requires_human_approval: false,
|
|
541
|
+
kill_switch: "enabled",
|
|
542
|
+
scopes: ["session:write", "session:action"],
|
|
543
|
+
},
|
|
544
|
+
metadata: {
|
|
545
|
+
provider: "sentinelayer",
|
|
546
|
+
adapter: "sentinelayer-cli",
|
|
547
|
+
server: "sentinelayer-session-mcp",
|
|
548
|
+
},
|
|
549
|
+
},
|
|
550
|
+
{
|
|
551
|
+
name: "session_react",
|
|
552
|
+
title: "React To Senti Message",
|
|
553
|
+
description:
|
|
554
|
+
"Acknowledge or react to a target session event with ack, like, or dislike.",
|
|
555
|
+
input_schema: {
|
|
556
|
+
type: "object",
|
|
557
|
+
additionalProperties: false,
|
|
558
|
+
required: ["sessionId", "agentId", "reaction"],
|
|
559
|
+
properties: {
|
|
560
|
+
sessionId: { type: "string" },
|
|
561
|
+
agentId: { type: "string" },
|
|
562
|
+
reaction: { type: "string", enum: ["ack", "like", "dislike"] },
|
|
563
|
+
targetSequenceId: { type: "integer", minimum: 1 },
|
|
564
|
+
targetCursor: { type: "string" },
|
|
565
|
+
targetActionId: { type: "string" },
|
|
566
|
+
idempotencyKey: { type: "string" },
|
|
567
|
+
timeoutMs: { type: "integer", minimum: 1 },
|
|
568
|
+
dryRun: { type: "boolean" },
|
|
569
|
+
},
|
|
570
|
+
},
|
|
571
|
+
transport: {
|
|
572
|
+
type: "internal",
|
|
573
|
+
method: "POST",
|
|
574
|
+
url: "sentinelayer://session/session_react",
|
|
575
|
+
timeout_ms: 15000,
|
|
576
|
+
auth: { mode: "none" },
|
|
577
|
+
},
|
|
578
|
+
budgets: {
|
|
579
|
+
max_calls_per_run: 60,
|
|
580
|
+
max_runtime_ms: 15000,
|
|
581
|
+
},
|
|
582
|
+
security: {
|
|
583
|
+
requires_human_approval: false,
|
|
584
|
+
kill_switch: "enabled",
|
|
585
|
+
scopes: ["session:write", "session:action"],
|
|
586
|
+
},
|
|
587
|
+
metadata: {
|
|
588
|
+
provider: "sentinelayer",
|
|
589
|
+
adapter: "sentinelayer-cli",
|
|
590
|
+
server: "sentinelayer-session-mcp",
|
|
591
|
+
},
|
|
592
|
+
},
|
|
593
|
+
{
|
|
594
|
+
name: "session_reply",
|
|
595
|
+
title: "Reply In Senti Thread",
|
|
596
|
+
description:
|
|
597
|
+
"Add a threaded reply/comment under a specific session event using the session action channel.",
|
|
598
|
+
input_schema: {
|
|
599
|
+
type: "object",
|
|
600
|
+
additionalProperties: false,
|
|
601
|
+
required: ["sessionId", "agentId", "targetSequenceId", "message"],
|
|
602
|
+
properties: {
|
|
603
|
+
sessionId: { type: "string" },
|
|
604
|
+
agentId: { type: "string" },
|
|
605
|
+
targetSequenceId: { type: "integer", minimum: 1 },
|
|
606
|
+
targetCursor: { type: "string" },
|
|
607
|
+
targetActionId: { type: "string" },
|
|
608
|
+
message: { type: "string" },
|
|
609
|
+
idempotencyKey: { type: "string" },
|
|
610
|
+
timeoutMs: { type: "integer", minimum: 1 },
|
|
611
|
+
dryRun: { type: "boolean" },
|
|
612
|
+
},
|
|
613
|
+
},
|
|
614
|
+
transport: {
|
|
615
|
+
type: "internal",
|
|
616
|
+
method: "POST",
|
|
617
|
+
url: "sentinelayer://session/session_reply",
|
|
618
|
+
timeout_ms: 15000,
|
|
619
|
+
auth: { mode: "none" },
|
|
620
|
+
},
|
|
621
|
+
budgets: {
|
|
622
|
+
max_calls_per_run: 40,
|
|
623
|
+
max_runtime_ms: 15000,
|
|
624
|
+
},
|
|
625
|
+
security: {
|
|
626
|
+
requires_human_approval: false,
|
|
627
|
+
kill_switch: "enabled",
|
|
628
|
+
scopes: ["session:write", "session:action"],
|
|
629
|
+
},
|
|
630
|
+
metadata: {
|
|
631
|
+
provider: "sentinelayer",
|
|
632
|
+
adapter: "sentinelayer-cli",
|
|
633
|
+
server: "sentinelayer-session-mcp",
|
|
634
|
+
},
|
|
635
|
+
},
|
|
636
|
+
{
|
|
637
|
+
name: "session_lock",
|
|
638
|
+
title: "Lock Senti Files",
|
|
639
|
+
description:
|
|
640
|
+
"Claim session-scoped file locks before editing files, using the same fail-closed lock registry as the CLI.",
|
|
641
|
+
input_schema: {
|
|
642
|
+
type: "object",
|
|
643
|
+
additionalProperties: false,
|
|
644
|
+
required: ["sessionId", "agentId", "files"],
|
|
645
|
+
properties: {
|
|
646
|
+
sessionId: { type: "string" },
|
|
647
|
+
agentId: { type: "string" },
|
|
648
|
+
files: {
|
|
649
|
+
anyOf: [
|
|
650
|
+
{ type: "string" },
|
|
651
|
+
{ type: "array", items: { type: "string" }, minItems: 1 },
|
|
652
|
+
],
|
|
653
|
+
},
|
|
654
|
+
intent: { type: "string" },
|
|
655
|
+
ttlSeconds: { type: "integer", minimum: 1 },
|
|
656
|
+
syncRemote: { type: "boolean" },
|
|
657
|
+
awaitRemoteSync: { type: "boolean" },
|
|
658
|
+
},
|
|
659
|
+
},
|
|
660
|
+
transport: {
|
|
661
|
+
type: "internal",
|
|
662
|
+
method: "POST",
|
|
663
|
+
url: "sentinelayer://session/session_lock",
|
|
664
|
+
timeout_ms: 15000,
|
|
665
|
+
auth: { mode: "none" },
|
|
666
|
+
},
|
|
667
|
+
budgets: {
|
|
668
|
+
max_calls_per_run: 20,
|
|
669
|
+
max_runtime_ms: 15000,
|
|
670
|
+
},
|
|
671
|
+
security: {
|
|
672
|
+
requires_human_approval: false,
|
|
673
|
+
kill_switch: "enabled",
|
|
674
|
+
scopes: ["session:lock"],
|
|
675
|
+
},
|
|
676
|
+
metadata: {
|
|
677
|
+
provider: "sentinelayer",
|
|
678
|
+
adapter: "sentinelayer-cli",
|
|
679
|
+
server: "sentinelayer-session-mcp",
|
|
680
|
+
},
|
|
681
|
+
},
|
|
682
|
+
{
|
|
683
|
+
name: "session_unlock",
|
|
684
|
+
title: "Unlock Senti Files",
|
|
685
|
+
description:
|
|
686
|
+
"Release session-scoped file locks held by an agent.",
|
|
687
|
+
input_schema: {
|
|
688
|
+
type: "object",
|
|
689
|
+
additionalProperties: false,
|
|
690
|
+
required: ["sessionId", "agentId", "files"],
|
|
691
|
+
properties: {
|
|
692
|
+
sessionId: { type: "string" },
|
|
693
|
+
agentId: { type: "string" },
|
|
694
|
+
files: {
|
|
695
|
+
anyOf: [
|
|
696
|
+
{ type: "string" },
|
|
697
|
+
{ type: "array", items: { type: "string" }, minItems: 1 },
|
|
698
|
+
],
|
|
699
|
+
},
|
|
700
|
+
reason: { type: "string" },
|
|
701
|
+
force: { type: "boolean" },
|
|
702
|
+
syncRemote: { type: "boolean" },
|
|
703
|
+
awaitRemoteSync: { type: "boolean" },
|
|
704
|
+
},
|
|
705
|
+
},
|
|
706
|
+
transport: {
|
|
707
|
+
type: "internal",
|
|
708
|
+
method: "POST",
|
|
709
|
+
url: "sentinelayer://session/session_unlock",
|
|
710
|
+
timeout_ms: 15000,
|
|
711
|
+
auth: { mode: "none" },
|
|
712
|
+
},
|
|
713
|
+
budgets: {
|
|
714
|
+
max_calls_per_run: 20,
|
|
715
|
+
max_runtime_ms: 15000,
|
|
716
|
+
},
|
|
717
|
+
security: {
|
|
718
|
+
requires_human_approval: false,
|
|
719
|
+
kill_switch: "enabled",
|
|
720
|
+
scopes: ["session:lock"],
|
|
721
|
+
},
|
|
722
|
+
metadata: {
|
|
723
|
+
provider: "sentinelayer",
|
|
724
|
+
adapter: "sentinelayer-cli",
|
|
725
|
+
server: "sentinelayer-session-mcp",
|
|
726
|
+
},
|
|
727
|
+
},
|
|
728
|
+
{
|
|
729
|
+
name: "session_locks",
|
|
730
|
+
title: "List Senti File Locks",
|
|
731
|
+
description:
|
|
732
|
+
"List active file locks for a session.",
|
|
733
|
+
input_schema: {
|
|
734
|
+
type: "object",
|
|
735
|
+
additionalProperties: false,
|
|
736
|
+
required: ["sessionId"],
|
|
737
|
+
properties: {
|
|
738
|
+
sessionId: { type: "string" },
|
|
739
|
+
},
|
|
740
|
+
},
|
|
741
|
+
transport: {
|
|
742
|
+
type: "internal",
|
|
743
|
+
method: "POST",
|
|
744
|
+
url: "sentinelayer://session/session_locks",
|
|
745
|
+
timeout_ms: 15000,
|
|
746
|
+
auth: { mode: "none" },
|
|
747
|
+
},
|
|
748
|
+
budgets: {
|
|
749
|
+
max_calls_per_run: 20,
|
|
750
|
+
max_runtime_ms: 15000,
|
|
751
|
+
},
|
|
752
|
+
security: {
|
|
753
|
+
requires_human_approval: false,
|
|
754
|
+
kill_switch: "enabled",
|
|
755
|
+
scopes: ["session:lock"],
|
|
756
|
+
},
|
|
757
|
+
metadata: {
|
|
758
|
+
provider: "sentinelayer",
|
|
759
|
+
adapter: "sentinelayer-cli",
|
|
760
|
+
server: "sentinelayer-session-mcp",
|
|
761
|
+
},
|
|
762
|
+
},
|
|
503
763
|
{
|
|
504
764
|
name: "attention_request",
|
|
505
765
|
title: "Request Senti Attention",
|