@holon-run/agentinbox 1.0.0-beta.2 → 1.0.0-beta.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/README.md +1 -1
- package/dist/src/adapters.js +49 -1
- package/dist/src/cli.js +96 -2
- package/dist/src/current_agent.js +22 -4
- package/dist/src/http.js +53 -3
- package/dist/src/service.js +316 -18
- package/dist/src/source_resolution.js +10 -0
- package/dist/src/source_schema.js +4 -0
- package/dist/src/sources/github.js +218 -1
- package/dist/src/sources/github_ci.js +64 -23
- package/dist/src/sources/remote.js +183 -1
- package/dist/src/sources/remote_modules.js +23 -3
- package/dist/src/store.js +8 -4
- package/dist/src/terminal.js +4 -3
- package/drizzle/migrations/0001_activation_entry_boundary.sql +11 -0
- package/drizzle/schema.ts +2 -0
- package/package.json +2 -2
package/README.md
CHANGED
package/dist/src/adapters.js
CHANGED
|
@@ -44,7 +44,9 @@ class AdapterRegistry {
|
|
|
44
44
|
remoteModuleRegistry;
|
|
45
45
|
constructor(store, appendSourceEvent, options) {
|
|
46
46
|
this.homeDir = options?.homeDir ?? (0, paths_1.resolveAgentInboxHome)(process.env);
|
|
47
|
-
this.remoteModuleRegistry = options?.remoteModuleRegistry ?? new remote_modules_1.RemoteSourceModuleRegistry(
|
|
47
|
+
this.remoteModuleRegistry = options?.remoteModuleRegistry ?? new remote_modules_1.RemoteSourceModuleRegistry({
|
|
48
|
+
githubCallClient: options?.githubCallClient,
|
|
49
|
+
});
|
|
48
50
|
this.remoteSource = new remote_1.RemoteSourceRuntime(store, appendSourceEvent, {
|
|
49
51
|
homeDir: this.homeDir,
|
|
50
52
|
client: options?.remoteSourceClient,
|
|
@@ -140,6 +142,31 @@ class AdapterRegistry {
|
|
|
140
142
|
}
|
|
141
143
|
return this.remoteSource.expandSubscriptionShortcut(source, input);
|
|
142
144
|
}
|
|
145
|
+
async listFollowTemplates(source) {
|
|
146
|
+
if (source.sourceType === "local_event") {
|
|
147
|
+
return [];
|
|
148
|
+
}
|
|
149
|
+
return this.remoteSource.listFollowTemplates(source);
|
|
150
|
+
}
|
|
151
|
+
async expandFollowTemplate(source, input) {
|
|
152
|
+
if (source.sourceType === "local_event") {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
return this.remoteSource.expandFollowTemplate(source, input);
|
|
156
|
+
}
|
|
157
|
+
followPreviewRefsForProviderOrKind(providerOrKind, config) {
|
|
158
|
+
const trimmed = providerOrKind.trim();
|
|
159
|
+
if (trimmed.length === 0) {
|
|
160
|
+
throw new Error("follow requires a providerOrKind");
|
|
161
|
+
}
|
|
162
|
+
if (isExplicitFollowPreviewRef(trimmed)) {
|
|
163
|
+
return [trimmed];
|
|
164
|
+
}
|
|
165
|
+
const candidates = hasRemoteSourceModulePath(config)
|
|
166
|
+
? ["remote_source", ...(0, remote_modules_1.builtinRemoteSourceTypes)()]
|
|
167
|
+
: (0, remote_modules_1.builtinRemoteSourceTypes)();
|
|
168
|
+
return Array.from(new Set(candidates));
|
|
169
|
+
}
|
|
143
170
|
async deriveInlinePreview(source, item) {
|
|
144
171
|
if (source.sourceType === "local_event") {
|
|
145
172
|
return null;
|
|
@@ -152,6 +179,15 @@ class AdapterRegistry {
|
|
|
152
179
|
}
|
|
153
180
|
return this.remoteSource.deriveNotificationGrouping(source, item);
|
|
154
181
|
}
|
|
182
|
+
async getSourceRuntime(source) {
|
|
183
|
+
if (source.sourceType === "local_event") {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
return this.remoteSource.getSourceRuntime(source);
|
|
187
|
+
}
|
|
188
|
+
async listSourceRuntimes(sources) {
|
|
189
|
+
return this.remoteSource.listSourceRuntimes(sources);
|
|
190
|
+
}
|
|
155
191
|
async summarizeDigestThread(source, items, grouping) {
|
|
156
192
|
if (source.sourceType === "local_event") {
|
|
157
193
|
return null;
|
|
@@ -194,6 +230,18 @@ class AdapterRegistry {
|
|
|
194
230
|
}
|
|
195
231
|
}
|
|
196
232
|
exports.AdapterRegistry = AdapterRegistry;
|
|
233
|
+
function isExplicitFollowPreviewRef(value) {
|
|
234
|
+
return (value === "local_event" ||
|
|
235
|
+
value === "remote_source" ||
|
|
236
|
+
value === "github_repo" ||
|
|
237
|
+
value === "github_repo_ci" ||
|
|
238
|
+
value === "feishu_bot" ||
|
|
239
|
+
value.startsWith("remote:"));
|
|
240
|
+
}
|
|
241
|
+
function hasRemoteSourceModulePath(config) {
|
|
242
|
+
const modulePath = config?.modulePath;
|
|
243
|
+
return typeof modulePath === "string" && modulePath.trim().length > 0;
|
|
244
|
+
}
|
|
197
245
|
function syntheticBuiltinSource(sourceType) {
|
|
198
246
|
return {
|
|
199
247
|
sourceId: `builtin:${sourceType}`,
|
package/dist/src/cli.js
CHANGED
|
@@ -182,6 +182,47 @@ async function main() {
|
|
|
182
182
|
await printRemote(client, `/streams/${encodeURIComponent(streamRef)}/schema`, undefined, "GET");
|
|
183
183
|
return;
|
|
184
184
|
}
|
|
185
|
+
if (command === "follow") {
|
|
186
|
+
const args = normalized.slice(1);
|
|
187
|
+
const positionals = positionalArgs(args, [
|
|
188
|
+
"--agent-id",
|
|
189
|
+
"--args-json",
|
|
190
|
+
"--arg",
|
|
191
|
+
"--config-json",
|
|
192
|
+
"--config-ref",
|
|
193
|
+
"--start-policy",
|
|
194
|
+
"--start-offset",
|
|
195
|
+
"--start-time",
|
|
196
|
+
]);
|
|
197
|
+
const [providerOrKind, template] = positionals;
|
|
198
|
+
if (!providerOrKind || !template || positionals[2]) {
|
|
199
|
+
throw new Error("usage: agentinbox follow <providerOrKind> <template> [--agent-id ID] [--args-json JSON | --arg KEY=VALUE ...] [--config-json JSON] [--config-ref REF] [--start-policy POLICY] [--start-offset N] [--start-time ISO8601]");
|
|
200
|
+
}
|
|
201
|
+
const argsJson = takeFlagValue(normalized, "--args-json");
|
|
202
|
+
const hasArgFlags = hasFlag(normalized, "--arg");
|
|
203
|
+
if (argsJson != null && hasArgFlags) {
|
|
204
|
+
throw new Error("follow accepts either --args-json or repeated --arg flags, but not both");
|
|
205
|
+
}
|
|
206
|
+
const selection = await selectAgentForCommand(client, {
|
|
207
|
+
explicitAgentId: takeFlagValue(normalized, "--agent-id"),
|
|
208
|
+
autoRegister: true,
|
|
209
|
+
});
|
|
210
|
+
const response = await requestRemote(client, "/follow", {
|
|
211
|
+
agentId: selection.agentId,
|
|
212
|
+
providerOrKind,
|
|
213
|
+
template,
|
|
214
|
+
templateArgs: argsJson != null
|
|
215
|
+
? (0, util_1.parseJsonArg)(argsJson, "--args-json")
|
|
216
|
+
: readFollowTemplateArgs(normalized),
|
|
217
|
+
configRef: takeFlagValue(normalized, "--config-ref") ?? undefined,
|
|
218
|
+
config: (0, util_1.parseJsonArg)(takeFlagValue(normalized, "--config-json")),
|
|
219
|
+
startPolicy: takeFlagValue(normalized, "--start-policy") ?? undefined,
|
|
220
|
+
startOffset: parseOptionalNumber(takeFlagValue(normalized, "--start-offset")),
|
|
221
|
+
startTime: takeFlagValue(normalized, "--start-time") ?? undefined,
|
|
222
|
+
});
|
|
223
|
+
console.log((0, util_1.jsonResponse)(withCommandMetadata(response.data, selection)));
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
185
226
|
if (command === "stream" && normalized[1] === "poll") {
|
|
186
227
|
const streamId = normalized[2];
|
|
187
228
|
if (!streamId) {
|
|
@@ -871,7 +912,7 @@ async function printCurrentAgent(client) {
|
|
|
871
912
|
const records = await listAgentsWithTargets(client);
|
|
872
913
|
const current = (0, current_agent_1.resolveCurrentAgent)(records, context);
|
|
873
914
|
if (!current) {
|
|
874
|
-
throw new Error(
|
|
915
|
+
throw new Error(noCurrentAgentMessage());
|
|
875
916
|
}
|
|
876
917
|
console.log((0, util_1.jsonResponse)(current));
|
|
877
918
|
}
|
|
@@ -907,7 +948,7 @@ async function selectAgentForCommand(client, options) {
|
|
|
907
948
|
};
|
|
908
949
|
}
|
|
909
950
|
if (!options.autoRegister) {
|
|
910
|
-
throw new Error(
|
|
951
|
+
throw new Error(noCurrentAgentMessage());
|
|
911
952
|
}
|
|
912
953
|
await requestRemote(client, "/agents", {
|
|
913
954
|
backend: contextForCurrent.backend,
|
|
@@ -946,6 +987,9 @@ async function requestRemote(client, endpoint, body, method = "POST") {
|
|
|
946
987
|
}
|
|
947
988
|
return { data: response.data };
|
|
948
989
|
}
|
|
990
|
+
function noCurrentAgentMessage() {
|
|
991
|
+
return "no current agent is registered for this terminal/runtime context; run `agentinbox agent list` to inspect offline agents, then `agentinbox agent register` or `agentinbox agent register --force-rebind --agent-id <agentId>` to rebind one";
|
|
992
|
+
}
|
|
949
993
|
function takeFlagValue(args, flag) {
|
|
950
994
|
const index = args.indexOf(flag);
|
|
951
995
|
if (index === -1) {
|
|
@@ -1062,6 +1106,50 @@ function readSubscriptionFilter(args) {
|
|
|
1062
1106
|
}
|
|
1063
1107
|
return {};
|
|
1064
1108
|
}
|
|
1109
|
+
function readFollowTemplateArgs(args) {
|
|
1110
|
+
const parsed = {};
|
|
1111
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
1112
|
+
if (args[index] !== "--arg") {
|
|
1113
|
+
continue;
|
|
1114
|
+
}
|
|
1115
|
+
const raw = args[index + 1];
|
|
1116
|
+
if (!raw || raw.startsWith("--")) {
|
|
1117
|
+
throw new Error("flag --arg requires KEY=VALUE");
|
|
1118
|
+
}
|
|
1119
|
+
const separator = raw.indexOf("=");
|
|
1120
|
+
if (separator <= 0) {
|
|
1121
|
+
throw new Error("flag --arg requires KEY=VALUE");
|
|
1122
|
+
}
|
|
1123
|
+
const key = raw.slice(0, separator).trim();
|
|
1124
|
+
const rawValue = raw.slice(separator + 1);
|
|
1125
|
+
if (key.length === 0) {
|
|
1126
|
+
throw new Error("flag --arg requires a non-empty key");
|
|
1127
|
+
}
|
|
1128
|
+
parsed[key] = parseFollowTemplateArgValue(rawValue);
|
|
1129
|
+
index += 1;
|
|
1130
|
+
}
|
|
1131
|
+
return parsed;
|
|
1132
|
+
}
|
|
1133
|
+
function parseFollowTemplateArgValue(raw) {
|
|
1134
|
+
if (raw === "true") {
|
|
1135
|
+
return true;
|
|
1136
|
+
}
|
|
1137
|
+
if (raw === "false") {
|
|
1138
|
+
return false;
|
|
1139
|
+
}
|
|
1140
|
+
if (/^-?\d+(?:\.\d+)?$/.test(raw)) {
|
|
1141
|
+
return Number(raw);
|
|
1142
|
+
}
|
|
1143
|
+
if ((raw.startsWith("[") && raw.endsWith("]")) || (raw.startsWith("{") && raw.endsWith("}")) || raw === "null") {
|
|
1144
|
+
try {
|
|
1145
|
+
return JSON.parse(raw);
|
|
1146
|
+
}
|
|
1147
|
+
catch {
|
|
1148
|
+
return raw;
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
return raw;
|
|
1152
|
+
}
|
|
1065
1153
|
function withCommandMetadata(data, selection) {
|
|
1066
1154
|
return {
|
|
1067
1155
|
...data,
|
|
@@ -1139,6 +1227,7 @@ Usage:
|
|
|
1139
1227
|
Commands:
|
|
1140
1228
|
serve
|
|
1141
1229
|
daemon
|
|
1230
|
+
follow
|
|
1142
1231
|
source
|
|
1143
1232
|
agent
|
|
1144
1233
|
timer
|
|
@@ -1184,6 +1273,11 @@ Usage:
|
|
|
1184
1273
|
agentinbox stream schema preview <sourceKind|sourceType> [--config-json JSON] [--config-ref REF]
|
|
1185
1274
|
agentinbox stream poll <streamId>
|
|
1186
1275
|
agentinbox stream event <streamId> --native-id ID --event EVENT [--occurred-at ISO8601] [--metadata-json JSON] [--payload-json JSON]
|
|
1276
|
+
`,
|
|
1277
|
+
follow: `agentinbox follow
|
|
1278
|
+
|
|
1279
|
+
Usage:
|
|
1280
|
+
agentinbox follow <providerOrKind> <template> [--agent-id ID] [--args-json JSON | --arg KEY=VALUE ...] [--config-json JSON] [--config-ref REF] [--start-policy POLICY] [--start-offset N] [--start-time ISO8601]
|
|
1187
1281
|
`,
|
|
1188
1282
|
source: `agentinbox source
|
|
1189
1283
|
|
|
@@ -39,6 +39,9 @@ function resolveCurrentAgent(agents, context) {
|
|
|
39
39
|
matchesCurrentTerminal: matchesCurrentTerminal(agent, context),
|
|
40
40
|
matchesCurrentRuntime: matchesCurrentRuntime(agent, context),
|
|
41
41
|
terminalIdentity: terminalIdentityForAgent(agent),
|
|
42
|
+
notifyCapable: notifyCapableForAgent(agent),
|
|
43
|
+
activeTargetCount: activeActivationTargetCount(agent),
|
|
44
|
+
offlineTargetCount: offlineActivationTargetCount(agent),
|
|
42
45
|
};
|
|
43
46
|
}
|
|
44
47
|
function annotateAgents(agents, context) {
|
|
@@ -52,24 +55,27 @@ function annotateAgents(agents, context) {
|
|
|
52
55
|
matchesCurrentRuntime: context ? matchesCurrentRuntime(entry, context) : false,
|
|
53
56
|
terminalIdentity: terminalIdentityForAgent(entry),
|
|
54
57
|
isCurrent: current?.agentId === entry.agent.agentId,
|
|
58
|
+
notifyCapable: notifyCapableForAgent(entry),
|
|
59
|
+
activeTargetCount: activeActivationTargetCount(entry),
|
|
60
|
+
offlineTargetCount: offlineActivationTargetCount(entry),
|
|
55
61
|
})),
|
|
56
62
|
};
|
|
57
63
|
}
|
|
58
64
|
function resolveAgentRecord(agents, context) {
|
|
59
65
|
if (context.tmuxPaneId) {
|
|
60
|
-
const match = agents.find((agent) =>
|
|
66
|
+
const match = agents.find((agent) => terminalTargetsForCurrentMatch(agent).some((target) => target.backend === "tmux" && target.tmuxPaneId === context.tmuxPaneId));
|
|
61
67
|
if (match) {
|
|
62
68
|
return match;
|
|
63
69
|
}
|
|
64
70
|
}
|
|
65
71
|
if (context.itermSessionId) {
|
|
66
|
-
const match = agents.find((agent) =>
|
|
72
|
+
const match = agents.find((agent) => terminalTargetsForCurrentMatch(agent).some((target) => target.backend === "iterm2" && target.itermSessionId === context.itermSessionId));
|
|
67
73
|
if (match) {
|
|
68
74
|
return match;
|
|
69
75
|
}
|
|
70
76
|
}
|
|
71
77
|
if (context.tty) {
|
|
72
|
-
const match = agents.find((agent) =>
|
|
78
|
+
const match = agents.find((agent) => terminalTargetsForCurrentMatch(agent).some((target) => target.tty === context.tty));
|
|
73
79
|
if (match) {
|
|
74
80
|
return match;
|
|
75
81
|
}
|
|
@@ -94,6 +100,18 @@ function activeTerminalTargets(agent) {
|
|
|
94
100
|
}
|
|
95
101
|
return terminalTargets(agent).filter((target) => target.status === "active");
|
|
96
102
|
}
|
|
103
|
+
function terminalTargetsForCurrentMatch(agent) {
|
|
104
|
+
return activeTerminalTargets(agent);
|
|
105
|
+
}
|
|
106
|
+
function activeActivationTargetCount(agent) {
|
|
107
|
+
return agent.activationTargets.filter((target) => target.status === "active").length;
|
|
108
|
+
}
|
|
109
|
+
function offlineActivationTargetCount(agent) {
|
|
110
|
+
return agent.activationTargets.filter((target) => target.status === "offline").length;
|
|
111
|
+
}
|
|
112
|
+
function notifyCapableForAgent(agent) {
|
|
113
|
+
return agent.agent.status === "active" && activeActivationTargetCount(agent) > 0;
|
|
114
|
+
}
|
|
97
115
|
function terminalIdentityForAgent(agent) {
|
|
98
116
|
const target = activeTerminalTargets(agent)[0];
|
|
99
117
|
if (!target) {
|
|
@@ -111,7 +129,7 @@ function terminalIdentityForAgent(agent) {
|
|
|
111
129
|
return null;
|
|
112
130
|
}
|
|
113
131
|
function matchesCurrentTerminal(agent, context) {
|
|
114
|
-
return
|
|
132
|
+
return terminalTargetsForCurrentMatch(agent).some((target) => (context.tmuxPaneId != null && target.tmuxPaneId === context.tmuxPaneId)
|
|
115
133
|
|| (context.itermSessionId != null && target.itermSessionId === context.itermSessionId)
|
|
116
134
|
|| (context.tty != null && target.tty === context.tty));
|
|
117
135
|
}
|
package/dist/src/http.js
CHANGED
|
@@ -29,6 +29,10 @@ const errorResponseSchema = {
|
|
|
29
29
|
error: { type: "string" },
|
|
30
30
|
},
|
|
31
31
|
};
|
|
32
|
+
const subscriptionStartPolicySchema = {
|
|
33
|
+
type: "string",
|
|
34
|
+
enum: ["latest", "earliest", "at_offset", "at_time"],
|
|
35
|
+
};
|
|
32
36
|
const subscriptionCollectionSchema = {
|
|
33
37
|
type: "object",
|
|
34
38
|
required: ["subscriptions"],
|
|
@@ -286,7 +290,7 @@ function buildFastifyServer(service) {
|
|
|
286
290
|
},
|
|
287
291
|
},
|
|
288
292
|
},
|
|
289
|
-
}, async () => ({ sources: service.
|
|
293
|
+
}, async () => ({ sources: await service.listSourceViews() }));
|
|
290
294
|
app.post("/sources", {
|
|
291
295
|
schema: {
|
|
292
296
|
tags: ["sources"],
|
|
@@ -464,6 +468,48 @@ function buildFastifyServer(service) {
|
|
|
464
468
|
},
|
|
465
469
|
},
|
|
466
470
|
}, async (request) => service.previewSourceSchema(request.body));
|
|
471
|
+
app.post("/follow", {
|
|
472
|
+
schema: {
|
|
473
|
+
tags: ["subscriptions"],
|
|
474
|
+
body: {
|
|
475
|
+
type: "object",
|
|
476
|
+
additionalProperties: false,
|
|
477
|
+
required: ["agentId", "providerOrKind", "template"],
|
|
478
|
+
properties: {
|
|
479
|
+
agentId: { type: "string", minLength: 1 },
|
|
480
|
+
providerOrKind: { type: "string", minLength: 1 },
|
|
481
|
+
template: { type: "string", minLength: 1 },
|
|
482
|
+
templateArgs: jsonObjectSchema,
|
|
483
|
+
configRef: { anyOf: [{ type: "string" }, { type: "null" }] },
|
|
484
|
+
config: jsonObjectSchema,
|
|
485
|
+
startPolicy: subscriptionStartPolicySchema,
|
|
486
|
+
startOffset: { type: "integer" },
|
|
487
|
+
startTime: { type: "string" },
|
|
488
|
+
},
|
|
489
|
+
},
|
|
490
|
+
response: {
|
|
491
|
+
200: jsonObjectSchema,
|
|
492
|
+
400: errorResponseSchema,
|
|
493
|
+
},
|
|
494
|
+
},
|
|
495
|
+
}, async (request) => {
|
|
496
|
+
const body = request.body;
|
|
497
|
+
return service.follow({
|
|
498
|
+
agentId: String(body.agentId),
|
|
499
|
+
providerOrKind: String(body.providerOrKind),
|
|
500
|
+
template: String(body.template),
|
|
501
|
+
templateArgs: body.templateArgs && typeof body.templateArgs === "object"
|
|
502
|
+
? body.templateArgs
|
|
503
|
+
: undefined,
|
|
504
|
+
configRef: optionalString(body.configRef) ?? null,
|
|
505
|
+
config: body.config && typeof body.config === "object"
|
|
506
|
+
? body.config
|
|
507
|
+
: undefined,
|
|
508
|
+
startPolicy: optionalString(body.startPolicy),
|
|
509
|
+
startOffset: typeof body.startOffset === "number" ? body.startOffset : undefined,
|
|
510
|
+
startTime: optionalString(body.startTime) ?? undefined,
|
|
511
|
+
});
|
|
512
|
+
});
|
|
467
513
|
app.post("/streams/:streamId/poll", {
|
|
468
514
|
schema: {
|
|
469
515
|
tags: ["sources"],
|
|
@@ -1083,7 +1129,7 @@ function buildFastifyServer(service) {
|
|
|
1083
1129
|
filter: jsonObjectSchema,
|
|
1084
1130
|
trackedResourceRef: { type: "string" },
|
|
1085
1131
|
cleanupPolicy: jsonObjectSchema,
|
|
1086
|
-
startPolicy:
|
|
1132
|
+
startPolicy: subscriptionStartPolicySchema,
|
|
1087
1133
|
startOffset: { type: "integer" },
|
|
1088
1134
|
startTime: { type: "string" },
|
|
1089
1135
|
},
|
|
@@ -1204,7 +1250,7 @@ function buildFastifyServer(service) {
|
|
|
1204
1250
|
additionalProperties: false,
|
|
1205
1251
|
required: ["startPolicy"],
|
|
1206
1252
|
properties: {
|
|
1207
|
-
startPolicy:
|
|
1253
|
+
startPolicy: subscriptionStartPolicySchema,
|
|
1208
1254
|
startOffset: { type: "integer" },
|
|
1209
1255
|
startTime: { type: "string" },
|
|
1210
1256
|
},
|
|
@@ -1690,6 +1736,10 @@ function isBadRequestError(message) {
|
|
|
1690
1736
|
message.startsWith("cleanupPolicy ") ||
|
|
1691
1737
|
message.startsWith("trackedResourceRef ") ||
|
|
1692
1738
|
message.startsWith("preview failed: ") ||
|
|
1739
|
+
message.startsWith("follow preview failed: ") ||
|
|
1740
|
+
message.startsWith("follow template ") ||
|
|
1741
|
+
message.startsWith("unknown follow template ") ||
|
|
1742
|
+
message.startsWith("follow requires a providerOrKind") ||
|
|
1693
1743
|
message.startsWith("unknown source kind or type for preview") ||
|
|
1694
1744
|
message.startsWith("preview source kind ") ||
|
|
1695
1745
|
message.startsWith("subscription add shortcut") ||
|