aislop 0.8.2 → 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/cli.js +558 -128
- package/dist/index.js +419 -77
- package/dist/{json-DxLkV8n2.js → json-DZfGz2xa.js} +1 -1
- package/dist/{json-BbMwrgyd.js → json-OIzja7OM.js} +1 -1
- package/dist/mcp.js +273 -10
- package/dist/{typecheck-B1MXNAy-.js → typecheck-wVSohmOX.js} +1 -1
- package/dist/{version-G3ekYjY1.js → version-D_rqBdyj.js} +1 -1
- package/package.json +1 -1
package/dist/mcp.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { n as runSubprocess, t as isToolInstalled } from "./subprocess-CCnnN_oQ.js";
|
|
3
3
|
import { createRequire, isBuiltin } from "node:module";
|
|
4
|
+
import { performance } from "node:perf_hooks";
|
|
4
5
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
6
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
7
|
import path from "node:path";
|
|
@@ -9,11 +10,11 @@ import { z } from "zod";
|
|
|
9
10
|
import fs from "node:fs";
|
|
10
11
|
import YAML from "yaml";
|
|
11
12
|
import { z as z$1 } from "zod/v4";
|
|
12
|
-
import { performance } from "node:perf_hooks";
|
|
13
13
|
import micromatch from "micromatch";
|
|
14
14
|
import { fileURLToPath } from "node:url";
|
|
15
15
|
import os from "node:os";
|
|
16
16
|
import "typescript";
|
|
17
|
+
import { randomUUID } from "node:crypto";
|
|
17
18
|
|
|
18
19
|
//#region src/config/defaults.ts
|
|
19
20
|
const DEFAULT_CONFIG = {
|
|
@@ -1253,6 +1254,27 @@ const collectJsDeps = (rootDir, jsDeps) => {
|
|
|
1253
1254
|
collectNestedManifests(rootDir, jsDeps);
|
|
1254
1255
|
return true;
|
|
1255
1256
|
};
|
|
1257
|
+
const TS_CONFIG_FILES = ["tsconfig.json", "jsconfig.json"];
|
|
1258
|
+
const buildAliasMatcher = (key) => {
|
|
1259
|
+
const starIdx = key.indexOf("*");
|
|
1260
|
+
if (starIdx === -1) return (spec) => spec === key;
|
|
1261
|
+
const before = key.slice(0, starIdx);
|
|
1262
|
+
const after = key.slice(starIdx + 1);
|
|
1263
|
+
return (spec) => spec.length >= before.length + after.length && spec.startsWith(before) && spec.endsWith(after);
|
|
1264
|
+
};
|
|
1265
|
+
const collectAliasMatchersFromConfig = (configPath, matchers) => {
|
|
1266
|
+
const opts = readJson(configPath)?.compilerOptions;
|
|
1267
|
+
if (!opts || typeof opts !== "object") return;
|
|
1268
|
+
const paths = opts.paths;
|
|
1269
|
+
if (!paths || typeof paths !== "object") return;
|
|
1270
|
+
for (const key of Object.keys(paths)) matchers.push(buildAliasMatcher(key));
|
|
1271
|
+
};
|
|
1272
|
+
const collectTsPathAliases = (rootDir) => {
|
|
1273
|
+
const matchers = [];
|
|
1274
|
+
const dirs = [rootDir, ...expandWorkspaceDirs(rootDir, readWorkspaceGlobs(rootDir, readJson(path.join(rootDir, "package.json"))))];
|
|
1275
|
+
for (const dir of dirs) for (const fname of TS_CONFIG_FILES) collectAliasMatchersFromConfig(path.join(dir, fname), matchers);
|
|
1276
|
+
return matchers;
|
|
1277
|
+
};
|
|
1256
1278
|
const addPyDep = (pyDeps, name) => {
|
|
1257
1279
|
const normalized = name.toLowerCase().replace(/_/g, "-");
|
|
1258
1280
|
pyDeps.add(normalized);
|
|
@@ -1410,10 +1432,11 @@ const extractPyImports = (content) => {
|
|
|
1410
1432
|
}
|
|
1411
1433
|
return results;
|
|
1412
1434
|
};
|
|
1413
|
-
const checkJsImport = (spec, manifest) => {
|
|
1435
|
+
const checkJsImport = (spec, manifest, tsAliasMatchers) => {
|
|
1414
1436
|
if (isJsRelativeOrAbsolute(spec)) return null;
|
|
1415
1437
|
if (isJsBuiltin(spec)) return null;
|
|
1416
1438
|
if (isJsVirtualModule(spec)) return null;
|
|
1439
|
+
if (tsAliasMatchers.some((m) => m(spec))) return null;
|
|
1417
1440
|
const pkg = packageNameFromImport(spec);
|
|
1418
1441
|
if (manifest.jsDeps.has(pkg)) return null;
|
|
1419
1442
|
if (pkg.startsWith("@types/")) {
|
|
@@ -1434,6 +1457,7 @@ const checkPyImport = (spec, manifest) => {
|
|
|
1434
1457
|
const detectHallucinatedImports = async (context) => {
|
|
1435
1458
|
const manifest = loadManifest(context.rootDirectory);
|
|
1436
1459
|
if (!manifest.hasJsManifest && !manifest.hasPyManifest) return [];
|
|
1460
|
+
const tsAliasMatchers = manifest.hasJsManifest ? collectTsPathAliases(context.rootDirectory) : [];
|
|
1437
1461
|
const diagnostics = [];
|
|
1438
1462
|
const files = getSourceFiles(context);
|
|
1439
1463
|
for (const filePath of files) {
|
|
@@ -1453,7 +1477,7 @@ const detectHallucinatedImports = async (context) => {
|
|
|
1453
1477
|
const relPath = path.relative(context.rootDirectory, filePath);
|
|
1454
1478
|
const imports = isJs ? extractJsImports(content) : extractPyImports(content);
|
|
1455
1479
|
for (const { spec, line } of imports) {
|
|
1456
|
-
const hallucinated = isJs ? checkJsImport(spec, manifest) : checkPyImport(spec, manifest);
|
|
1480
|
+
const hallucinated = isJs ? checkJsImport(spec, manifest, tsAliasMatchers) : checkPyImport(spec, manifest);
|
|
1457
1481
|
if (!hallucinated) continue;
|
|
1458
1482
|
const manifestLabel = isJs ? "package.json" : "requirements.txt / pyproject.toml / Pipfile";
|
|
1459
1483
|
diagnostics.push({
|
|
@@ -5071,7 +5095,225 @@ const handleAislopBaseline = (input) => {
|
|
|
5071
5095
|
|
|
5072
5096
|
//#endregion
|
|
5073
5097
|
//#region src/version.ts
|
|
5074
|
-
const APP_VERSION = "0.
|
|
5098
|
+
const APP_VERSION = "0.9.0";
|
|
5099
|
+
|
|
5100
|
+
//#endregion
|
|
5101
|
+
//#region src/telemetry/env.ts
|
|
5102
|
+
const detectPackageManager = (env = process.env) => {
|
|
5103
|
+
const execPath = env.npm_execpath ?? "";
|
|
5104
|
+
if (execPath.includes("npx")) return "npx";
|
|
5105
|
+
const userAgent = env.npm_config_user_agent ?? "";
|
|
5106
|
+
if (userAgent.startsWith("pnpm/")) return "pnpm";
|
|
5107
|
+
if (userAgent.startsWith("yarn/")) return "yarn";
|
|
5108
|
+
if (userAgent.startsWith("bun/")) return "bun";
|
|
5109
|
+
if (userAgent.startsWith("npm/")) return "npm";
|
|
5110
|
+
if (execPath.includes("pnpm")) return "pnpm";
|
|
5111
|
+
if (execPath.includes("yarn")) return "yarn";
|
|
5112
|
+
if (execPath.includes("bun")) return "bun";
|
|
5113
|
+
if (execPath.includes("npm")) return "npm";
|
|
5114
|
+
return "unknown";
|
|
5115
|
+
};
|
|
5116
|
+
const CI_ENV_KEYS = [
|
|
5117
|
+
"CI",
|
|
5118
|
+
"GITHUB_ACTIONS",
|
|
5119
|
+
"GITLAB_CI",
|
|
5120
|
+
"CIRCLECI",
|
|
5121
|
+
"TRAVIS",
|
|
5122
|
+
"BUILDKITE",
|
|
5123
|
+
"DRONE",
|
|
5124
|
+
"TEAMCITY_VERSION",
|
|
5125
|
+
"TF_BUILD"
|
|
5126
|
+
];
|
|
5127
|
+
const isCiEnv = (env = process.env) => CI_ENV_KEYS.some((k) => {
|
|
5128
|
+
const v = env[k];
|
|
5129
|
+
return v === "true" || v === "1" || v != null && v.length > 0 && k !== "CI";
|
|
5130
|
+
}) || env.CI === "true" || env.CI === "1";
|
|
5131
|
+
|
|
5132
|
+
//#endregion
|
|
5133
|
+
//#region src/telemetry/identity.ts
|
|
5134
|
+
const FILE_BASENAME = "install_id";
|
|
5135
|
+
const resolveInstallIdPath = (homedir = os.homedir(), env = process.env) => {
|
|
5136
|
+
if (process.platform === "linux" && env.XDG_STATE_HOME) return path.join(env.XDG_STATE_HOME, "aislop", FILE_BASENAME);
|
|
5137
|
+
return path.join(homedir, ".aislop", FILE_BASENAME);
|
|
5138
|
+
};
|
|
5139
|
+
const ensureInstallId = (idPath = resolveInstallIdPath()) => {
|
|
5140
|
+
if (fs.existsSync(idPath)) {
|
|
5141
|
+
const existing = fs.readFileSync(idPath, "utf-8").trim();
|
|
5142
|
+
if (existing.length > 0) return {
|
|
5143
|
+
installId: existing,
|
|
5144
|
+
created: false
|
|
5145
|
+
};
|
|
5146
|
+
}
|
|
5147
|
+
const dir = path.dirname(idPath);
|
|
5148
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
5149
|
+
const installId = randomUUID();
|
|
5150
|
+
const tmpPath = `${idPath}.${process.pid}.tmp`;
|
|
5151
|
+
fs.writeFileSync(tmpPath, `${installId}\n`, { mode: 384 });
|
|
5152
|
+
try {
|
|
5153
|
+
fs.renameSync(tmpPath, idPath);
|
|
5154
|
+
return {
|
|
5155
|
+
installId,
|
|
5156
|
+
created: true
|
|
5157
|
+
};
|
|
5158
|
+
} catch {
|
|
5159
|
+
fs.rmSync(tmpPath, { force: true });
|
|
5160
|
+
return {
|
|
5161
|
+
installId: fs.readFileSync(idPath, "utf-8").trim(),
|
|
5162
|
+
created: false
|
|
5163
|
+
};
|
|
5164
|
+
}
|
|
5165
|
+
};
|
|
5166
|
+
|
|
5167
|
+
//#endregion
|
|
5168
|
+
//#region src/telemetry/redaction.ts
|
|
5169
|
+
const SAFE_PROPERTY_NAMES = new Set([
|
|
5170
|
+
"aislop_version",
|
|
5171
|
+
"node_version",
|
|
5172
|
+
"os",
|
|
5173
|
+
"arch",
|
|
5174
|
+
"schema_version",
|
|
5175
|
+
"anonymous_install_id",
|
|
5176
|
+
"package_manager",
|
|
5177
|
+
"is_ci",
|
|
5178
|
+
"command",
|
|
5179
|
+
"language_summary",
|
|
5180
|
+
"lang_typescript",
|
|
5181
|
+
"lang_javascript",
|
|
5182
|
+
"lang_python",
|
|
5183
|
+
"lang_java",
|
|
5184
|
+
"file_count_bucket",
|
|
5185
|
+
"exit_code",
|
|
5186
|
+
"duration_ms",
|
|
5187
|
+
"error_kind",
|
|
5188
|
+
"score",
|
|
5189
|
+
"score_bucket",
|
|
5190
|
+
"finding_count",
|
|
5191
|
+
"error_count",
|
|
5192
|
+
"warning_count",
|
|
5193
|
+
"fixable_count",
|
|
5194
|
+
"fix_steps",
|
|
5195
|
+
"fix_resolved",
|
|
5196
|
+
"fix_score_delta",
|
|
5197
|
+
"engine_format_issues",
|
|
5198
|
+
"engine_format_ms",
|
|
5199
|
+
"engine_lint_issues",
|
|
5200
|
+
"engine_lint_ms",
|
|
5201
|
+
"engine_code_quality_issues",
|
|
5202
|
+
"engine_code_quality_ms",
|
|
5203
|
+
"engine_ai_slop_issues",
|
|
5204
|
+
"engine_ai_slop_ms",
|
|
5205
|
+
"engine_architecture_issues",
|
|
5206
|
+
"engine_architecture_ms",
|
|
5207
|
+
"engine_security_issues",
|
|
5208
|
+
"engine_security_ms",
|
|
5209
|
+
"tool",
|
|
5210
|
+
"ok",
|
|
5211
|
+
"agent",
|
|
5212
|
+
"score_delta"
|
|
5213
|
+
]);
|
|
5214
|
+
const redactProperties = (props) => {
|
|
5215
|
+
const clean = {};
|
|
5216
|
+
const dropped = [];
|
|
5217
|
+
for (const [key, value] of Object.entries(props)) {
|
|
5218
|
+
if (value === void 0) continue;
|
|
5219
|
+
if (SAFE_PROPERTY_NAMES.has(key)) clean[key] = value;
|
|
5220
|
+
else dropped.push(key);
|
|
5221
|
+
}
|
|
5222
|
+
return {
|
|
5223
|
+
clean,
|
|
5224
|
+
dropped
|
|
5225
|
+
};
|
|
5226
|
+
};
|
|
5227
|
+
|
|
5228
|
+
//#endregion
|
|
5229
|
+
//#region src/telemetry/client.ts
|
|
5230
|
+
const POSTHOG_HOST = "https://eu.i.posthog.com";
|
|
5231
|
+
const POSTHOG_KEY = "phc_eY2cOMFva9q24GrWeOuvuVIOhCIdjOALxeAR3ItrqbJ";
|
|
5232
|
+
const SCHEMA_VERSION = "v2";
|
|
5233
|
+
const REQUEST_TIMEOUT_MS = 3e3;
|
|
5234
|
+
const isTelemetryDisabled = (config) => {
|
|
5235
|
+
const env = process.env;
|
|
5236
|
+
if (env.AISLOP_NO_TELEMETRY === "1" || env.DO_NOT_TRACK === "1") return true;
|
|
5237
|
+
if (config?.enabled === false) return true;
|
|
5238
|
+
if (config?.enabled === true) return false;
|
|
5239
|
+
if (env.CI === "true" || env.CI === "1") return true;
|
|
5240
|
+
return false;
|
|
5241
|
+
};
|
|
5242
|
+
const isDebug = () => process.env.AISLOP_TELEMETRY_DEBUG === "1";
|
|
5243
|
+
const pendingRequests = /* @__PURE__ */ new Set();
|
|
5244
|
+
let cachedInstallId = null;
|
|
5245
|
+
let installCreated = false;
|
|
5246
|
+
const baseProperties = (installId) => ({
|
|
5247
|
+
aislop_version: APP_VERSION,
|
|
5248
|
+
node_version: process.version,
|
|
5249
|
+
os: os.platform(),
|
|
5250
|
+
arch: os.arch(),
|
|
5251
|
+
schema_version: SCHEMA_VERSION,
|
|
5252
|
+
anonymous_install_id: installId,
|
|
5253
|
+
package_manager: detectPackageManager(),
|
|
5254
|
+
is_ci: isCiEnv()
|
|
5255
|
+
});
|
|
5256
|
+
const track = (input) => {
|
|
5257
|
+
if (isTelemetryDisabled(input.config)) return { installCreated: false };
|
|
5258
|
+
if (cachedInstallId == null) {
|
|
5259
|
+
const ensured = ensureInstallId(resolveInstallIdPath());
|
|
5260
|
+
cachedInstallId = ensured.installId;
|
|
5261
|
+
installCreated = ensured.created;
|
|
5262
|
+
}
|
|
5263
|
+
const { clean, dropped } = redactProperties({
|
|
5264
|
+
...baseProperties(cachedInstallId),
|
|
5265
|
+
...input.properties
|
|
5266
|
+
});
|
|
5267
|
+
if (isDebug()) {
|
|
5268
|
+
const compact = JSON.stringify({
|
|
5269
|
+
event: input.event,
|
|
5270
|
+
properties: clean
|
|
5271
|
+
});
|
|
5272
|
+
process.stderr.write(`[telemetry] ${compact}\n`);
|
|
5273
|
+
if (dropped.length > 0) for (const key of dropped) process.stderr.write(`[telemetry] dropped non-allowlisted property: ${key}\n`);
|
|
5274
|
+
}
|
|
5275
|
+
if (process.env.AISLOP_TELEMETRY_DRY_RUN === "1") return { installCreated };
|
|
5276
|
+
const payload = {
|
|
5277
|
+
api_key: POSTHOG_KEY,
|
|
5278
|
+
event: input.event,
|
|
5279
|
+
distinct_id: cachedInstallId,
|
|
5280
|
+
properties: clean,
|
|
5281
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
5282
|
+
};
|
|
5283
|
+
const request = fetch(`${POSTHOG_HOST}/capture/`, {
|
|
5284
|
+
method: "POST",
|
|
5285
|
+
headers: { "Content-Type": "application/json" },
|
|
5286
|
+
body: JSON.stringify(payload),
|
|
5287
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
5288
|
+
}).then(() => {}).catch(() => {}).finally(() => {
|
|
5289
|
+
pendingRequests.delete(request);
|
|
5290
|
+
});
|
|
5291
|
+
pendingRequests.add(request);
|
|
5292
|
+
return { installCreated };
|
|
5293
|
+
};
|
|
5294
|
+
const flushTelemetry = async () => {
|
|
5295
|
+
if (pendingRequests.size === 0) return;
|
|
5296
|
+
await Promise.all(pendingRequests);
|
|
5297
|
+
};
|
|
5298
|
+
|
|
5299
|
+
//#endregion
|
|
5300
|
+
//#region src/telemetry/events.ts
|
|
5301
|
+
const buildMcpToolCalledProps = (input) => {
|
|
5302
|
+
const props = {
|
|
5303
|
+
tool: input.tool,
|
|
5304
|
+
duration_ms: Math.round(input.durationMs),
|
|
5305
|
+
ok: input.ok
|
|
5306
|
+
};
|
|
5307
|
+
if (input.errorKind) props.error_kind = input.errorKind;
|
|
5308
|
+
return props;
|
|
5309
|
+
};
|
|
5310
|
+
const errorKindFromException = (error) => {
|
|
5311
|
+
const message = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
|
|
5312
|
+
if (message.includes("timeout") || message.includes("timed out")) return "timeout";
|
|
5313
|
+
if (message.includes("invalid config") || message.includes("config_invalid")) return "config_invalid";
|
|
5314
|
+
if (message.includes("engine") && message.includes("crash")) return "engine_crash";
|
|
5315
|
+
return "unknown";
|
|
5316
|
+
};
|
|
5075
5317
|
|
|
5076
5318
|
//#endregion
|
|
5077
5319
|
//#region src/mcp.ts
|
|
@@ -5086,10 +5328,29 @@ const err = (message) => ({
|
|
|
5086
5328
|
}],
|
|
5087
5329
|
isError: true
|
|
5088
5330
|
});
|
|
5089
|
-
const
|
|
5331
|
+
const instrument = async (tool, fn) => {
|
|
5332
|
+
const startedAt = performance.now();
|
|
5090
5333
|
try {
|
|
5091
|
-
|
|
5334
|
+
const value = await fn();
|
|
5335
|
+
track({
|
|
5336
|
+
event: "mcp_tool_called",
|
|
5337
|
+
properties: buildMcpToolCalledProps({
|
|
5338
|
+
tool,
|
|
5339
|
+
durationMs: performance.now() - startedAt,
|
|
5340
|
+
ok: true
|
|
5341
|
+
})
|
|
5342
|
+
});
|
|
5343
|
+
return ok(value);
|
|
5092
5344
|
} catch (e) {
|
|
5345
|
+
track({
|
|
5346
|
+
event: "mcp_tool_called",
|
|
5347
|
+
properties: buildMcpToolCalledProps({
|
|
5348
|
+
tool,
|
|
5349
|
+
durationMs: performance.now() - startedAt,
|
|
5350
|
+
ok: false,
|
|
5351
|
+
errorKind: errorKindFromException(e)
|
|
5352
|
+
})
|
|
5353
|
+
});
|
|
5093
5354
|
return err(e instanceof Error ? e.message : String(e));
|
|
5094
5355
|
}
|
|
5095
5356
|
};
|
|
@@ -5101,25 +5362,27 @@ const buildServer = () => {
|
|
|
5101
5362
|
server.registerTool(aislopScanTool.name, {
|
|
5102
5363
|
description: aislopScanTool.description,
|
|
5103
5364
|
inputSchema: aislopScanInputSchema.shape
|
|
5104
|
-
}, (input) =>
|
|
5365
|
+
}, (input) => instrument("aislop_scan", () => handleAislopScan(input)));
|
|
5105
5366
|
server.registerTool(aislopFixTool.name, {
|
|
5106
5367
|
description: aislopFixTool.description,
|
|
5107
5368
|
inputSchema: aislopFixInputSchema.shape
|
|
5108
|
-
}, (input) =>
|
|
5369
|
+
}, (input) => instrument("aislop_fix", () => handleAislopFix(input)));
|
|
5109
5370
|
server.registerTool(aislopWhyTool.name, {
|
|
5110
5371
|
description: aislopWhyTool.description,
|
|
5111
5372
|
inputSchema: aislopWhyInputSchema.shape
|
|
5112
|
-
}, (input) =>
|
|
5373
|
+
}, (input) => instrument("aislop_why", () => handleAislopWhy(input)));
|
|
5113
5374
|
server.registerTool(aislopBaselineTool.name, {
|
|
5114
5375
|
description: aislopBaselineTool.description,
|
|
5115
5376
|
inputSchema: aislopBaselineInputSchema.shape
|
|
5116
|
-
}, (input) =>
|
|
5377
|
+
}, (input) => instrument("aislop_baseline", () => handleAislopBaseline(input)));
|
|
5117
5378
|
return server;
|
|
5118
5379
|
};
|
|
5119
5380
|
const main = async () => {
|
|
5120
5381
|
const server = buildServer();
|
|
5121
5382
|
const transport = new StdioServerTransport();
|
|
5122
5383
|
await server.connect(transport);
|
|
5384
|
+
track({ event: "mcp_server_started" });
|
|
5385
|
+
await flushTelemetry();
|
|
5123
5386
|
};
|
|
5124
5387
|
main().catch((e) => {
|
|
5125
5388
|
process.stderr.write(`aislop-mcp failed to start: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
@@ -29,7 +29,7 @@ const getEngineLabel = (engine) => ENGINE_INFO[engine].label;
|
|
|
29
29
|
|
|
30
30
|
//#endregion
|
|
31
31
|
//#region src/version.ts
|
|
32
|
-
const APP_VERSION = "0.
|
|
32
|
+
const APP_VERSION = "0.9.0";
|
|
33
33
|
|
|
34
34
|
//#endregion
|
|
35
35
|
export { ENGINE_INFO as n, getEngineLabel as r, APP_VERSION as t };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aislop",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "The engineering standards layer and quality gate for AI-written code. Define your standard once. Every agent — Claude Code, Cursor, Codex — is held to it automatically, on every edit and every PR. Catches the slop they leave behind, enforces the rules your team sets. 8+ languages. Deterministic.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|