autotel-cli 0.8.15 → 0.9.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/dist/index.js +250 -14
- package/dist/index.js.map +1 -1
- package/package.json +8 -8
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import { Command as
|
|
4
|
+
import { Command as Command12 } from "commander";
|
|
5
5
|
|
|
6
6
|
// src/commands/init.ts
|
|
7
7
|
import * as fs4 from "fs";
|
|
@@ -4381,6 +4381,12 @@ var INVESTIGATE_COMMANDS = [
|
|
|
4381
4381
|
investigateCmd("llm expensive", "Top token-spend traces"),
|
|
4382
4382
|
investigateCmd("llm slow", "Slowest LLM traces"),
|
|
4383
4383
|
investigateCmd("llm tools", "Tool/function spans grouped by tool name"),
|
|
4384
|
+
investigateCmd("security", "Security telemetry (parent)"),
|
|
4385
|
+
investigateCmd(
|
|
4386
|
+
"security summary",
|
|
4387
|
+
"Security posture: events by severity/category, probe signals, denied responses"
|
|
4388
|
+
),
|
|
4389
|
+
investigateCmd("security events", "List spans carrying security.* events"),
|
|
4384
4390
|
investigateCmd("semconv", "Semantic conventions lookup (parent)", { static: true }),
|
|
4385
4391
|
investigateCmd("semconv list", "List semconv namespaces", { static: true, network: true }),
|
|
4386
4392
|
investigateCmd("semconv get", "Groups for one namespace", {
|
|
@@ -4720,6 +4726,16 @@ function staticFlagsFromOpts(opts) {
|
|
|
4720
4726
|
function addTimeWindowFlags(cmd) {
|
|
4721
4727
|
return cmd.option("--service-name <name>", "Filter by service name").option("--operation-name <name>", "Filter by operation name").option("--lookback-minutes <n>", "Lookback window in minutes", intArg).option("--from <iso>", "Start time (ISO 8601)").option("--to <iso>", "End time (ISO 8601)").option("--limit <n>", "Max results", intArg);
|
|
4722
4728
|
}
|
|
4729
|
+
function windowFlagsFromOpts(opts) {
|
|
4730
|
+
return {
|
|
4731
|
+
serviceName: opts.serviceName,
|
|
4732
|
+
operationName: opts.operationName,
|
|
4733
|
+
lookbackMinutes: opts.lookbackMinutes,
|
|
4734
|
+
from: opts.from,
|
|
4735
|
+
to: opts.to,
|
|
4736
|
+
limit: opts.limit
|
|
4737
|
+
};
|
|
4738
|
+
}
|
|
4723
4739
|
|
|
4724
4740
|
// src/commands/investigate/health.ts
|
|
4725
4741
|
async function runHealth(flags) {
|
|
@@ -5782,14 +5798,233 @@ function registerCollectorCommands(program) {
|
|
|
5782
5798
|
program.addCommand(collectorCmd);
|
|
5783
5799
|
}
|
|
5784
5800
|
|
|
5801
|
+
// src/commands/investigate/security.ts
|
|
5802
|
+
import { Command as Command11 } from "commander";
|
|
5803
|
+
import { toSpanSearchQuery as toSpanSearchQuery2 } from "autotel-mcp";
|
|
5804
|
+
var DEFAULT_LIMIT = 500;
|
|
5805
|
+
var DEFAULT_DENIED_STATUSES = [401, 403, 429];
|
|
5806
|
+
var SAMPLE_TRACE_IDS = 10;
|
|
5807
|
+
function countBy(spans, tagKey) {
|
|
5808
|
+
const counts = {};
|
|
5809
|
+
for (const span of spans) {
|
|
5810
|
+
const value = span.tags[tagKey];
|
|
5811
|
+
if (value === void 0) continue;
|
|
5812
|
+
const key = String(value);
|
|
5813
|
+
counts[key] = (counts[key] ?? 0) + 1;
|
|
5814
|
+
}
|
|
5815
|
+
return counts;
|
|
5816
|
+
}
|
|
5817
|
+
function topEntries(counts, n) {
|
|
5818
|
+
return Object.entries(counts).toSorted((a, b) => b[1] - a[1]).slice(0, n).map(([value, count]) => ({ value, count }));
|
|
5819
|
+
}
|
|
5820
|
+
function sampleTraceIds(spans) {
|
|
5821
|
+
const ids = /* @__PURE__ */ new Set();
|
|
5822
|
+
for (const span of spans) {
|
|
5823
|
+
ids.add(span.traceId);
|
|
5824
|
+
if (ids.size >= SAMPLE_TRACE_IDS) break;
|
|
5825
|
+
}
|
|
5826
|
+
return [...ids];
|
|
5827
|
+
}
|
|
5828
|
+
function dedupeSpans(...lists) {
|
|
5829
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5830
|
+
const merged = [];
|
|
5831
|
+
for (const list of lists) {
|
|
5832
|
+
for (const span of list) {
|
|
5833
|
+
const key = `${span.traceId}:${span.spanId}`;
|
|
5834
|
+
if (seen.has(key)) continue;
|
|
5835
|
+
seen.add(key);
|
|
5836
|
+
merged.push(span);
|
|
5837
|
+
}
|
|
5838
|
+
}
|
|
5839
|
+
return merged;
|
|
5840
|
+
}
|
|
5841
|
+
function countByService(spans) {
|
|
5842
|
+
const counts = {};
|
|
5843
|
+
for (const span of spans) {
|
|
5844
|
+
counts[span.serviceName] = (counts[span.serviceName] ?? 0) + 1;
|
|
5845
|
+
}
|
|
5846
|
+
return counts;
|
|
5847
|
+
}
|
|
5848
|
+
function readStatus(span) {
|
|
5849
|
+
const value = span.tags["http.response.status_code"] ?? span.tags["http.status_code"];
|
|
5850
|
+
if (typeof value === "number") return value;
|
|
5851
|
+
if (typeof value === "string") {
|
|
5852
|
+
const parsed = Number.parseInt(value, 10);
|
|
5853
|
+
if (!Number.isNaN(parsed)) return parsed;
|
|
5854
|
+
}
|
|
5855
|
+
return void 0;
|
|
5856
|
+
}
|
|
5857
|
+
async function runSecuritySummary(flags) {
|
|
5858
|
+
await runInvestigate("security summary", flags, async (backend) => {
|
|
5859
|
+
const limit = flags.limit ?? DEFAULT_LIMIT;
|
|
5860
|
+
const deniedStatuses = flags.deniedStatuses ?? DEFAULT_DENIED_STATUSES;
|
|
5861
|
+
const base = toSpanSearchQuery2({
|
|
5862
|
+
serviceName: flags.serviceName,
|
|
5863
|
+
lookbackMinutes: flags.lookbackMinutes,
|
|
5864
|
+
from: flags.from,
|
|
5865
|
+
to: flags.to,
|
|
5866
|
+
limit
|
|
5867
|
+
});
|
|
5868
|
+
const statusValues = deniedStatuses;
|
|
5869
|
+
const [events, suspicious, deniedNew, deniedLegacy] = await Promise.all([
|
|
5870
|
+
backend.searchSpans({
|
|
5871
|
+
...base,
|
|
5872
|
+
filters: [{ field: "security.event", operator: "exists" }]
|
|
5873
|
+
}),
|
|
5874
|
+
backend.searchSpans({
|
|
5875
|
+
...base,
|
|
5876
|
+
filters: [
|
|
5877
|
+
{ field: "security.suspicious_request", operator: "exists" }
|
|
5878
|
+
]
|
|
5879
|
+
}),
|
|
5880
|
+
backend.searchSpans({
|
|
5881
|
+
...base,
|
|
5882
|
+
filters: [
|
|
5883
|
+
{
|
|
5884
|
+
field: "http.response.status_code",
|
|
5885
|
+
operator: "in",
|
|
5886
|
+
value: statusValues,
|
|
5887
|
+
valueType: "number"
|
|
5888
|
+
}
|
|
5889
|
+
]
|
|
5890
|
+
}),
|
|
5891
|
+
backend.searchSpans({
|
|
5892
|
+
...base,
|
|
5893
|
+
filters: [
|
|
5894
|
+
{
|
|
5895
|
+
field: "http.status_code",
|
|
5896
|
+
operator: "in",
|
|
5897
|
+
value: statusValues,
|
|
5898
|
+
valueType: "number"
|
|
5899
|
+
}
|
|
5900
|
+
]
|
|
5901
|
+
})
|
|
5902
|
+
]);
|
|
5903
|
+
const denied = dedupeSpans(deniedNew.items, deniedLegacy.items);
|
|
5904
|
+
const deniedByStatus = {};
|
|
5905
|
+
for (const span of denied) {
|
|
5906
|
+
const status = readStatus(span);
|
|
5907
|
+
if (status === void 0) continue;
|
|
5908
|
+
deniedByStatus[status] = (deniedByStatus[status] ?? 0) + 1;
|
|
5909
|
+
}
|
|
5910
|
+
return {
|
|
5911
|
+
window: {
|
|
5912
|
+
lookbackMinutes: flags.lookbackMinutes ?? 60,
|
|
5913
|
+
from: flags.from,
|
|
5914
|
+
to: flags.to,
|
|
5915
|
+
limitPerQuery: limit
|
|
5916
|
+
},
|
|
5917
|
+
securityEvents: {
|
|
5918
|
+
total: events.items.length,
|
|
5919
|
+
bySeverity: countBy(events.items, "security.severity"),
|
|
5920
|
+
byCategory: countBy(events.items, "security.category"),
|
|
5921
|
+
byOutcome: countBy(events.items, "security.outcome"),
|
|
5922
|
+
topEvents: topEntries(countBy(events.items, "security.event"), 10),
|
|
5923
|
+
sampleTraceIds: sampleTraceIds(events.items)
|
|
5924
|
+
},
|
|
5925
|
+
suspiciousRequests: {
|
|
5926
|
+
total: suspicious.items.length,
|
|
5927
|
+
bySignal: countBy(suspicious.items, "security.signal"),
|
|
5928
|
+
byService: countByService(suspicious.items),
|
|
5929
|
+
sampleTraceIds: sampleTraceIds(suspicious.items)
|
|
5930
|
+
},
|
|
5931
|
+
deniedResponses: {
|
|
5932
|
+
total: denied.length,
|
|
5933
|
+
byStatus: deniedByStatus,
|
|
5934
|
+
topClients: topEntries(countBy(denied, "client.address"), 10),
|
|
5935
|
+
sampleTraceIds: sampleTraceIds(denied)
|
|
5936
|
+
}
|
|
5937
|
+
};
|
|
5938
|
+
});
|
|
5939
|
+
}
|
|
5940
|
+
async function runSecurityEvents(flags) {
|
|
5941
|
+
await runInvestigate("security events", flags, async (backend) => {
|
|
5942
|
+
const base = toSpanSearchQuery2({
|
|
5943
|
+
serviceName: flags.serviceName,
|
|
5944
|
+
lookbackMinutes: flags.lookbackMinutes,
|
|
5945
|
+
from: flags.from,
|
|
5946
|
+
to: flags.to,
|
|
5947
|
+
limit: flags.limit ?? DEFAULT_LIMIT
|
|
5948
|
+
});
|
|
5949
|
+
const filters = [
|
|
5950
|
+
{ field: "security.event", operator: "exists" }
|
|
5951
|
+
];
|
|
5952
|
+
if (flags.category !== void 0) {
|
|
5953
|
+
filters.push({
|
|
5954
|
+
field: "security.category",
|
|
5955
|
+
operator: "equals",
|
|
5956
|
+
value: flags.category
|
|
5957
|
+
});
|
|
5958
|
+
}
|
|
5959
|
+
if (flags.severity !== void 0) {
|
|
5960
|
+
filters.push({
|
|
5961
|
+
field: "security.severity",
|
|
5962
|
+
operator: "equals",
|
|
5963
|
+
value: flags.severity
|
|
5964
|
+
});
|
|
5965
|
+
}
|
|
5966
|
+
const result = await backend.searchSpans({ ...base, filters });
|
|
5967
|
+
return {
|
|
5968
|
+
totalCount: result.totalCount,
|
|
5969
|
+
items: result.items.map((span) => ({
|
|
5970
|
+
traceId: span.traceId,
|
|
5971
|
+
spanId: span.spanId,
|
|
5972
|
+
serviceName: span.serviceName,
|
|
5973
|
+
operationName: span.operationName,
|
|
5974
|
+
startTimeUnixMs: span.startTimeUnixMs,
|
|
5975
|
+
event: span.tags["security.event"],
|
|
5976
|
+
category: span.tags["security.category"],
|
|
5977
|
+
outcome: span.tags["security.outcome"],
|
|
5978
|
+
severity: span.tags["security.severity"],
|
|
5979
|
+
reason: span.tags["security.reason"]
|
|
5980
|
+
}))
|
|
5981
|
+
};
|
|
5982
|
+
});
|
|
5983
|
+
}
|
|
5984
|
+
function csvIntArg(value) {
|
|
5985
|
+
return value.split(",").map((part) => Number.parseInt(part.trim(), 10)).filter((n) => !Number.isNaN(n));
|
|
5986
|
+
}
|
|
5987
|
+
function registerSecurityCommands(program) {
|
|
5988
|
+
const securityCmd = new Command11("security").description(
|
|
5989
|
+
"Security telemetry: events, suspicious requests, denied responses (JSON)"
|
|
5990
|
+
);
|
|
5991
|
+
const summaryCmd = addTimeWindowFlags(new Command11("summary")).description(
|
|
5992
|
+
"Security posture summary: events by severity/category, probe signals, denied responses with top clients"
|
|
5993
|
+
).option(
|
|
5994
|
+
"--denied-statuses <csv>",
|
|
5995
|
+
"HTTP statuses counted as denied (default 401,403,429)",
|
|
5996
|
+
csvIntArg
|
|
5997
|
+
).action(async function() {
|
|
5998
|
+
const o = this.optsWithGlobals();
|
|
5999
|
+
await runSecuritySummary({
|
|
6000
|
+
...backendFlagsFromOpts(o),
|
|
6001
|
+
...windowFlagsFromOpts(o),
|
|
6002
|
+
deniedStatuses: o.deniedStatuses
|
|
6003
|
+
});
|
|
6004
|
+
});
|
|
6005
|
+
const eventsCmd = addTimeWindowFlags(new Command11("events")).description("List spans carrying security events (security.* schema)").option("--category <name>", "Filter by security.category").option("--severity <level>", "Filter by security.severity").action(async function() {
|
|
6006
|
+
const o = this.optsWithGlobals();
|
|
6007
|
+
await runSecurityEvents({
|
|
6008
|
+
...backendFlagsFromOpts(o),
|
|
6009
|
+
...windowFlagsFromOpts(o),
|
|
6010
|
+
category: o.category,
|
|
6011
|
+
severity: o.severity
|
|
6012
|
+
});
|
|
6013
|
+
});
|
|
6014
|
+
addBackendFlags(securityCmd);
|
|
6015
|
+
securityCmd.addCommand(summaryCmd);
|
|
6016
|
+
securityCmd.addCommand(eventsCmd);
|
|
6017
|
+
program.addCommand(securityCmd);
|
|
6018
|
+
}
|
|
6019
|
+
|
|
5785
6020
|
// src/cli.ts
|
|
5786
6021
|
function createProgram() {
|
|
5787
|
-
const program = new
|
|
6022
|
+
const program = new Command12();
|
|
5788
6023
|
program.name("autotel").description("CLI for autotel - setup wizard, diagnostics, and incremental features").version("0.1.0");
|
|
5789
6024
|
const addGlobalOptions = (cmd) => {
|
|
5790
6025
|
return cmd.option("--cwd <path>", "Target directory", process.cwd()).option("--verbose", "Show detailed output").option("--quiet", "Only show warnings and errors");
|
|
5791
6026
|
};
|
|
5792
|
-
const initCmd = new
|
|
6027
|
+
const initCmd = new Command12("init").description("Initialize autotel in your project").option("--dry-run", "Skip installation and print what would be done").option("--no-install", "Generate files only, skip package installation").option("--print-install-cmd", "Output the install command without running it").option("-y, --yes", "Accept defaults, non-interactive").option("--preset <name>", "Use a quick preset (e.g., node-datadog-pino)").option("--force", "Overwrite existing config (creates backup first)").option("--workspace-root", "Install at workspace root instead of package root").option("--no-detect", "Skip auto-detection of installed deps").option("--detect-only", "Run detection, print the plan, write nothing").option("--plan <path>", "Apply a pre-built InitPlan JSON file").option("--input <path>", "Read InitPlan JSON from stdin (-) or a file").option("--scan-env", "Consent to reading .env / .env.local for backend detection").option("--json", "Emit machine-readable JSON").option("--output-file <path>", "Persist JSON output to this file").option("--no-secrets-in-output", "Redact secret-shaped values").option("--no-interactive", "Never prompt; fail if input would be required").action(async (opts) => {
|
|
5793
6028
|
const options = {
|
|
5794
6029
|
cwd: opts.cwd ?? process.cwd(),
|
|
5795
6030
|
dryRun: opts.dryRun ?? false,
|
|
@@ -5819,7 +6054,7 @@ function createProgram() {
|
|
|
5819
6054
|
});
|
|
5820
6055
|
addGlobalOptions(initCmd);
|
|
5821
6056
|
program.addCommand(initCmd);
|
|
5822
|
-
const doctorCmd = new
|
|
6057
|
+
const doctorCmd = new Command12("doctor").description("Run diagnostics on your autotel setup").option("--json", "Output machine-readable JSON").option("--fix", "Auto-fix resolvable issues").option("--list-checks", "List all available checks").option("--env-file <path>", "Specify env file to check").action(async (opts) => {
|
|
5823
6058
|
const options = {
|
|
5824
6059
|
cwd: opts.cwd ?? process.cwd(),
|
|
5825
6060
|
dryRun: false,
|
|
@@ -5837,7 +6072,7 @@ function createProgram() {
|
|
|
5837
6072
|
});
|
|
5838
6073
|
addGlobalOptions(doctorCmd);
|
|
5839
6074
|
program.addCommand(doctorCmd);
|
|
5840
|
-
const addCmd = new
|
|
6075
|
+
const addCmd = new Command12("add").description("Add a backend, subscriber, plugin, or platform").argument("[type]", "Preset type (backend, subscriber, plugin, platform)").argument("[name]", "Preset name (e.g., datadog, posthog, mongoose)").option("--list", "List available presets for the given type").option("--dry-run", "Skip installation and print what would be done").option("--no-install", "Generate files only, skip package installation").option("--print-install-cmd", "Output the install command without running it").option("-y, --yes", "Accept defaults, non-interactive").option("--force", "Overwrite non-CLI-owned config (creates backup first)").option("--json", "Output machine-readable JSON (for --list)").option("--workspace-root", "Install at workspace root instead of package root").action(async (type, name, opts) => {
|
|
5841
6076
|
const options = {
|
|
5842
6077
|
cwd: opts.cwd ?? process.cwd(),
|
|
5843
6078
|
dryRun: opts.dryRun ?? false,
|
|
@@ -5859,8 +6094,8 @@ function createProgram() {
|
|
|
5859
6094
|
});
|
|
5860
6095
|
addGlobalOptions(addCmd);
|
|
5861
6096
|
program.addCommand(addCmd);
|
|
5862
|
-
const codemodCmd = new
|
|
5863
|
-
const traceCmd = new
|
|
6097
|
+
const codemodCmd = new Command12("codemod").description("Codemod commands for adopting autotel");
|
|
6098
|
+
const traceCmd = new Command12("trace").description("Wrap functions in trace() with span name from function/variable/method name").argument("<path>", "File path or glob (e.g. src/index.ts, src/**/*.ts)").option("--dry-run", "Print changes without writing files").option("--name-pattern <pattern>", "Span name template: {name}, {file}, {path}").option("--skip <regex>...", "Skip functions whose name matches (repeatable)").option("--print-files", "Print per-file summary (wrapped count, skipped)").action(async (pathArg, opts) => {
|
|
5864
6099
|
const options = {
|
|
5865
6100
|
cwd: opts.cwd ?? process.cwd(),
|
|
5866
6101
|
dryRun: opts.dryRun ?? false,
|
|
@@ -5880,27 +6115,27 @@ function createProgram() {
|
|
|
5880
6115
|
codemodCmd.addCommand(traceCmd);
|
|
5881
6116
|
addGlobalOptions(codemodCmd);
|
|
5882
6117
|
program.addCommand(codemodCmd);
|
|
5883
|
-
const schemaCmd = new
|
|
6118
|
+
const schemaCmd = new Command12("schema").description("Print the CLI manifest as JSON (agent discovery)").option("--output-file <path>", "Persist JSON to a file").option("--no-secrets-in-output", "Redact secret-shaped values").action((opts) => {
|
|
5884
6119
|
runSchema({ outputFile: opts.outputFile, noSecrets: opts.secretsInOutput === false });
|
|
5885
6120
|
});
|
|
5886
|
-
const schemaErrorsCmd = new
|
|
6121
|
+
const schemaErrorsCmd = new Command12("errors").description("Print error envelope shape + AUTOTEL_E_* codes").option("--output-file <path>", "Persist JSON to a file").action((opts) => {
|
|
5887
6122
|
runSchemaErrors({ outputFile: opts.outputFile });
|
|
5888
6123
|
});
|
|
5889
|
-
const schemaOutputsCmd = new
|
|
6124
|
+
const schemaOutputsCmd = new Command12("outputs").description("Print JSON output shapes per command").option("--output-file <path>", "Persist JSON to a file").action((opts) => {
|
|
5890
6125
|
runSchemaOutputs({ outputFile: opts.outputFile });
|
|
5891
6126
|
});
|
|
5892
6127
|
schemaCmd.addCommand(schemaErrorsCmd);
|
|
5893
6128
|
schemaCmd.addCommand(schemaOutputsCmd);
|
|
5894
6129
|
program.addCommand(schemaCmd);
|
|
5895
|
-
const commandsCmd = new
|
|
6130
|
+
const commandsCmd = new Command12("commands").description("Print compact tool-style listing of commands").option("--output-file <path>", "Persist JSON to a file").action((opts) => {
|
|
5896
6131
|
runCommandsListing({ outputFile: opts.outputFile });
|
|
5897
6132
|
});
|
|
5898
6133
|
program.addCommand(commandsCmd);
|
|
5899
|
-
const examplesCmd = new
|
|
6134
|
+
const examplesCmd = new Command12("examples").description("Print copy-pasteable examples for a command").argument("[command]", "Command name (omit for all)").option("--output-file <path>", "Persist JSON to a file").action((name, opts) => {
|
|
5900
6135
|
runExamples(name, { outputFile: opts.outputFile });
|
|
5901
6136
|
});
|
|
5902
6137
|
program.addCommand(examplesCmd);
|
|
5903
|
-
const versionCmd = new
|
|
6138
|
+
const versionCmd = new Command12("version").description("Print version info as JSON").option("--output-file <path>", "Persist JSON to a file").action((opts) => {
|
|
5904
6139
|
runVersion({ outputFile: opts.outputFile });
|
|
5905
6140
|
});
|
|
5906
6141
|
program.addCommand(versionCmd);
|
|
@@ -5915,6 +6150,7 @@ function createProgram() {
|
|
|
5915
6150
|
registerSemconvCommands(program);
|
|
5916
6151
|
registerScoreCommands(program);
|
|
5917
6152
|
registerCollectorCommands(program);
|
|
6153
|
+
registerSecurityCommands(program);
|
|
5918
6154
|
return program;
|
|
5919
6155
|
}
|
|
5920
6156
|
async function run() {
|
|
@@ -5966,7 +6202,7 @@ function jsonModeRequested() {
|
|
|
5966
6202
|
run().catch((error2) => {
|
|
5967
6203
|
const err = commanderErrorToAutotel(error2) ?? toAutotelError(error2);
|
|
5968
6204
|
const isJson = jsonModeRequested() || // schema/commands/examples/version + investigate commands are JSON-only
|
|
5969
|
-
/^(schema|commands|examples|version|health|capabilities|discover|query|trace|diagnose|topology|correlate|llm|semconv|score|collector)\b/.test(
|
|
6205
|
+
/^(schema|commands|examples|version|health|capabilities|discover|query|trace|diagnose|topology|correlate|llm|semconv|score|collector|security)\b/.test(
|
|
5970
6206
|
process.argv.slice(2).join(" ")
|
|
5971
6207
|
);
|
|
5972
6208
|
if (isJson) {
|