@dev-kosaly/kagents 0.1.1 → 0.2.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/README.md +16 -3
- package/agents/architect.md +1 -1
- package/bin/kagents.js +163 -14
- package/bin/postinstall.js +26 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -50,16 +50,29 @@ Prérequis : Node 18 ou plus. Depuis la racine du projet :
|
|
|
50
50
|
|
|
51
51
|
```bash
|
|
52
52
|
npx @dev-kosaly/kagents # ou : pnpm dlx @dev-kosaly/kagents
|
|
53
|
-
npx @dev-kosaly/kagents --tools claude,cursor,agents
|
|
53
|
+
npx @dev-kosaly/kagents --tools claude,cursor,agents # sans question
|
|
54
|
+
npx @dev-kosaly/kagents --configure # rechoisir les outils
|
|
54
55
|
npx @dev-kosaly/kagents --copy # copies au lieu de liens (utile sous Windows)
|
|
55
56
|
npx @dev-kosaly/kagents uninstall
|
|
56
57
|
```
|
|
57
58
|
|
|
58
|
-
|
|
59
|
+
Ajouter le paquet comme dépendance (`npm add @dev-kosaly/kagents` ou `pnpm add @dev-kosaly/kagents`) lance aussi l'installation automatiquement. Avec **pnpm**, les scripts des dépendances sont bloqués tant qu'ils ne sont pas approuvés : lancez `pnpm approve-builds` une fois (ou `pnpm exec kagents` à la main). `KAGENTS_SKIP_POSTINSTALL=1` désactive l'installation automatique.
|
|
60
|
+
|
|
61
|
+
Dans un terminal, l'installation pose la question :
|
|
62
|
+
|
|
63
|
+
```text
|
|
64
|
+
? Pour quels outils installer KAgents ? (↑↓ espace a entrée)
|
|
65
|
+
❯ ◉ Claude Code (.claude/commands, .claude/agents)
|
|
66
|
+
◯ Cursor (.cursor/commands, .cursor/agents, .cursor/rules)
|
|
67
|
+
◉ Codex et autres outils AGENTS.md (.agents/skills)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Le choix est mémorisé dans `.kagents/config.json` et rejoué aux mises à jour. Sans terminal (CI) ou avec `--yes`, les outils sont détectés automatiquement (`.claude/`, `.cursor/`) et `.agents/skills/` est ajouté.
|
|
59
71
|
|
|
60
72
|
Ce que fait l'installation :
|
|
61
|
-
- Le kit est copié dans `.kagents/`. `.claude
|
|
73
|
+
- Le kit est copié dans `.kagents/`. `.claude/` et `.cursor/` reçoivent des liens relatifs vers les commandes et les agents (les skills restent dans `.kagents/skills/` pour ne pas polluer le menu `/`) ; `.agents/skills/` expose les skills aux outils qui le lisent.
|
|
62
74
|
- Le bloc `<!-- kagents:start/end -->` de `AGENTS.md` est régénéré (mode d'emploi, agents, routage). Le reste du fichier n'est jamais modifié.
|
|
75
|
+
- `.gitignore` reçoit un bloc `# kagents:start/end` qui ignore le kit dans `.kagents/` mais pas `.kagents/docs/` (livrables et contexte, à versionner).
|
|
63
76
|
- Un fichier existant qui n'est pas géré par KAgents n'est jamais écrasé.
|
|
64
77
|
|
|
65
78
|
**Mise à jour** : relancer avec `@latest`. Les fichiers du kit dans `.kagents/` sont régénérés (ne pas les éditer). `.kagents/docs/`, qui contient les livrables des agents et `knowledge/context.md`, n'est jamais touché.
|
package/agents/architect.md
CHANGED
|
@@ -116,7 +116,7 @@ Handoff Developer : perimetre implementation **apres** validations ; ne pas defi
|
|
|
116
116
|
| `agents/architect.md` | Qui, limites, modes, principes |
|
|
117
117
|
| `rules/domains/architect-invariants.md` | Invariants non negociables |
|
|
118
118
|
| `rules/domains/architect-chat.md` | Contrat de sortie conversationnelle |
|
|
119
|
-
| `skills/architect-*` | Procedures |
|
|
119
|
+
| `skills/architect-*` | Procedures (installées dans `.kagents/skills/<nom>/SKILL.md` : les charger par ce chemin) |
|
|
120
120
|
| `workflows/architect-*.md` | Enchainement |
|
|
121
121
|
| `templates/architect-output/` | Structure livrable |
|
|
122
122
|
| `commands/architect*.md` | Entrees explicites |
|
package/bin/kagents.js
CHANGED
|
@@ -5,25 +5,29 @@
|
|
|
5
5
|
|
|
6
6
|
const fs = require('fs');
|
|
7
7
|
const path = require('path');
|
|
8
|
+
const readline = require('readline');
|
|
9
|
+
const tty = require('tty');
|
|
8
10
|
|
|
9
11
|
const PKG_ROOT = path.resolve(__dirname, '..');
|
|
10
12
|
const PKG = JSON.parse(fs.readFileSync(path.join(PKG_ROOT, 'package.json'), 'utf8'));
|
|
11
13
|
const KIT_DIRS = ['agents', 'skills', 'commands', 'rules', 'templates', 'checklists', 'workflows', 'governance', 'docs'];
|
|
12
14
|
const SKIP = new Set(['README.md', '.gitkeep']);
|
|
13
15
|
const BLOCK_RE = /<!-- kagents:start -->[\s\S]*?<!-- kagents:end -->/;
|
|
16
|
+
const GITIGNORE_RE = /# kagents:start\n[\s\S]*?# kagents:end\n?/;
|
|
17
|
+
// On ignore le kit (régénérable) mais pas docs/ : livrables et contexte sont à versionner.
|
|
14
18
|
|
|
15
19
|
// Adaptateurs : un outil = une liste [dossier du kit, destination, type].
|
|
16
20
|
// type : files (fichiers), dirs (dossiers), agents (fichiers .md avec `name:`).
|
|
21
|
+
// Les skills ne sont volontairement PAS liées dans .claude/ ni .cursor/ : ces outils les listeraient
|
|
22
|
+
// comme commandes `/`. Les agents et commandes les chargent par chemin depuis .kagents/skills/.
|
|
17
23
|
const ADAPTERS = {
|
|
18
24
|
agents: [['skills', '.agents/skills', 'dirs']],
|
|
19
25
|
claude: [
|
|
20
26
|
['commands', '.claude/commands', 'files'],
|
|
21
|
-
['skills', '.claude/skills', 'dirs'],
|
|
22
27
|
['agents', '.claude/agents', 'agents'],
|
|
23
28
|
],
|
|
24
29
|
cursor: [
|
|
25
30
|
['commands', '.cursor/commands', 'files'],
|
|
26
|
-
['skills', '.cursor/skills', 'dirs'],
|
|
27
31
|
['agents', '.cursor/agents', 'agents'],
|
|
28
32
|
['rules/global', '.cursor/rules', 'files'],
|
|
29
33
|
['rules/domains', '.cursor/rules', 'files'],
|
|
@@ -32,14 +36,18 @@ const ADAPTERS = {
|
|
|
32
36
|
|
|
33
37
|
const HELP = `kagents ${PKG.version}
|
|
34
38
|
|
|
35
|
-
Usage : kagents [install] [--tools claude,cursor,agents|auto] [--copy]
|
|
39
|
+
Usage : kagents [install] [--tools claude,cursor,agents|auto|none] [--yes] [--copy]
|
|
40
|
+
kagents --configure (rechoisir les outils)
|
|
36
41
|
kagents uninstall
|
|
37
42
|
kagents --help | --version
|
|
38
43
|
|
|
39
|
-
À lancer depuis la racine du projet.
|
|
40
|
-
|
|
41
|
-
--
|
|
42
|
-
|
|
44
|
+
À lancer depuis la racine du projet. Dans un terminal, une liste permet de choisir
|
|
45
|
+
les outils à brancher ; le choix est mémorisé dans .kagents/config.json.
|
|
46
|
+
--tools outils à brancher, sans question
|
|
47
|
+
--yes, -y ne pose pas de question (détection automatique)
|
|
48
|
+
--configure pose à nouveau la question
|
|
49
|
+
--copy copies au lieu de liens symboliques (aussi KAGENTS_MODE=copy)
|
|
50
|
+
uninstall retire ce que KAgents a installé (docs/ est conservé)
|
|
43
51
|
`;
|
|
44
52
|
|
|
45
53
|
const log = (m) => console.log(`kagents: ${m}`);
|
|
@@ -51,12 +59,14 @@ const die = (m) => {
|
|
|
51
59
|
|
|
52
60
|
// --- Arguments ----------------------------------------------------------------
|
|
53
61
|
function parseArgs(argv) {
|
|
54
|
-
const o = { cmd: 'install', tools:
|
|
62
|
+
const o = { cmd: 'install', tools: null, yes: false, configure: false, copy: process.env.KAGENTS_MODE === 'copy' };
|
|
55
63
|
for (let i = 0; i < argv.length; i++) {
|
|
56
64
|
const a = argv[i];
|
|
57
65
|
if (a === '-h' || a === '--help') o.cmd = 'help';
|
|
58
66
|
else if (a === '-v' || a === '--version') o.cmd = 'version';
|
|
59
67
|
else if (a === '--copy') o.copy = true;
|
|
68
|
+
else if (a === '-y' || a === '--yes') o.yes = true;
|
|
69
|
+
else if (a === '--configure') o.configure = true;
|
|
60
70
|
else if (a === '--tools') o.tools = argv[++i] || die('--tools attend une valeur');
|
|
61
71
|
else if (a.startsWith('--tools=')) o.tools = a.slice(8);
|
|
62
72
|
else if (a === 'install' || a === 'uninstall') o.cmd = a;
|
|
@@ -102,6 +112,7 @@ class Installer {
|
|
|
102
112
|
this.copy = copy;
|
|
103
113
|
this.kagents = path.join(target, '.kagents');
|
|
104
114
|
this.manifest = path.join(this.kagents, '.installed');
|
|
115
|
+
this.config = path.join(this.kagents, 'config.json');
|
|
105
116
|
this.entries = [];
|
|
106
117
|
}
|
|
107
118
|
|
|
@@ -266,15 +277,49 @@ Document partagé, écrit par l'utilisateur. Les agents le lisent, ne l'écriven
|
|
|
266
277
|
fs.writeFileSync(file, out);
|
|
267
278
|
}
|
|
268
279
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
const
|
|
280
|
+
// Bloc .gitignore : le kit et les liens que l'on a posés, jamais docs/ (sauf les fichiers du kit qui s'y trouvent).
|
|
281
|
+
gitignoreBlock() {
|
|
282
|
+
const extra = this.entries.filter((e) => !e.startsWith('.kagents/') || e.startsWith('.kagents/docs/')).sort();
|
|
283
|
+
return ['# kagents:start', '.kagents/*', '!.kagents/docs/', ...extra, '# kagents:end'].join('\n') + '\n';
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
writeGitignore() {
|
|
287
|
+
const file = path.join(this.target, '.gitignore');
|
|
288
|
+
if (!fs.existsSync(file) && !fs.existsSync(path.join(this.target, '.git'))) return;
|
|
289
|
+
const cur = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : '';
|
|
290
|
+
const block = this.gitignoreBlock();
|
|
291
|
+
const out = GITIGNORE_RE.test(cur)
|
|
292
|
+
? cur.replace(GITIGNORE_RE, () => block)
|
|
293
|
+
: (cur && !cur.endsWith('\n') ? cur + '\n' : cur) + (cur ? '\n' : '') + block;
|
|
294
|
+
fs.writeFileSync(file, out);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
detectTools() {
|
|
272
298
|
const has = (p) => fs.existsSync(path.join(this.target, p));
|
|
299
|
+
const t = [];
|
|
273
300
|
if (has('.claude') || has('CLAUDE.md')) t.push('claude');
|
|
274
301
|
if (has('.cursor')) t.push('cursor');
|
|
275
302
|
return t;
|
|
276
303
|
}
|
|
277
304
|
|
|
305
|
+
resolveTools(tools) {
|
|
306
|
+
return tools === 'auto' ? ['agents', ...this.detectTools()] : tools;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
loadConfig() {
|
|
310
|
+
try {
|
|
311
|
+
const c = JSON.parse(fs.readFileSync(this.config, 'utf8'));
|
|
312
|
+
return Array.isArray(c.tools) ? c.tools : null;
|
|
313
|
+
} catch {
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
saveConfig(tools) {
|
|
319
|
+
fs.mkdirSync(this.kagents, { recursive: true });
|
|
320
|
+
fs.writeFileSync(this.config, JSON.stringify({ tools }, null, 2) + '\n');
|
|
321
|
+
}
|
|
322
|
+
|
|
278
323
|
runAdapters(tools) {
|
|
279
324
|
for (const tool of this.resolveTools(tools)) {
|
|
280
325
|
if (!ADAPTERS[tool]) {
|
|
@@ -326,6 +371,7 @@ Document partagé, écrit par l'utilisateur. Les agents le lisent, ne l'écriven
|
|
|
326
371
|
this.createDocs();
|
|
327
372
|
this.writeAgentsMd();
|
|
328
373
|
this.runAdapters(tools);
|
|
374
|
+
this.writeGitignore();
|
|
329
375
|
this.saveManifest();
|
|
330
376
|
log(`installé dans ${this.kagents} (${this.copy ? 'copies' : 'liens'})`);
|
|
331
377
|
}
|
|
@@ -341,22 +387,125 @@ Document partagé, écrit par l'utilisateur. Les agents le lisent, ne l'écriven
|
|
|
341
387
|
else fs.rmSync(file);
|
|
342
388
|
}
|
|
343
389
|
}
|
|
390
|
+
const gi = path.join(this.target, '.gitignore');
|
|
391
|
+
if (fs.existsSync(gi) && GITIGNORE_RE.test(fs.readFileSync(gi, 'utf8'))) {
|
|
392
|
+
const out = fs.readFileSync(gi, 'utf8').replace(GITIGNORE_RE, '').replace(/\n{3,}/g, '\n\n').replace(/\s+$/, '');
|
|
393
|
+
if (out) fs.writeFileSync(gi, out + '\n');
|
|
394
|
+
else fs.rmSync(gi);
|
|
395
|
+
}
|
|
344
396
|
fs.rmSync(this.manifest, { force: true });
|
|
397
|
+
fs.rmSync(this.config, { force: true });
|
|
345
398
|
this.prune(removed);
|
|
346
399
|
log('désinstallé (.kagents/docs/ conservé : ce sont vos livrables)');
|
|
347
400
|
}
|
|
348
401
|
}
|
|
349
402
|
|
|
403
|
+
// --- Question interactive -----------------------------------------------------
|
|
404
|
+
// Terminal utilisable : le nôtre, ou /dev/tty quand on est lancé par un postinstall (stdin/stdout détournés).
|
|
405
|
+
function openTTY() {
|
|
406
|
+
if (process.stdin.isTTY && process.stdout.isTTY) return { input: process.stdin, output: process.stdout, close() {} };
|
|
407
|
+
if (process.env.KAGENTS_FROM_POSTINSTALL && process.platform !== 'win32') {
|
|
408
|
+
try {
|
|
409
|
+
const rfd = fs.openSync('/dev/tty', 'r');
|
|
410
|
+
const wfd = fs.openSync('/dev/tty', 'w');
|
|
411
|
+
const input = new tty.ReadStream(rfd);
|
|
412
|
+
const output = new tty.WriteStream(wfd);
|
|
413
|
+
return { input, output, close: () => { input.destroy(); output.destroy(); } };
|
|
414
|
+
} catch {
|
|
415
|
+
return null;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
return null;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Liste à cocher : ↑↓ déplacer, espace cocher, a tout, entrée valider. Renvoie les ids cochés.
|
|
422
|
+
function multiselect(io, message, items) {
|
|
423
|
+
return new Promise((resolve) => {
|
|
424
|
+
const { input, output } = io;
|
|
425
|
+
const state = items.map((i) => ({ ...i }));
|
|
426
|
+
let cur = 0;
|
|
427
|
+
let drawn = false;
|
|
428
|
+
const draw = () => {
|
|
429
|
+
if (drawn) output.write(`\x1b[${state.length + 1}A`);
|
|
430
|
+
drawn = true;
|
|
431
|
+
output.write(`\x1b[2K\x1b[1m?\x1b[0m ${message}\n`);
|
|
432
|
+
state.forEach((it, i) => {
|
|
433
|
+
const box = it.checked ? '\x1b[32m◉\x1b[0m' : '◯';
|
|
434
|
+
output.write(`\x1b[2K ${i === cur ? '\x1b[36m❯\x1b[0m' : ' '} ${box} ${it.label}\n`);
|
|
435
|
+
});
|
|
436
|
+
};
|
|
437
|
+
const finish = () => {
|
|
438
|
+
input.removeListener('keypress', onKey);
|
|
439
|
+
input.setRawMode(false);
|
|
440
|
+
input.pause();
|
|
441
|
+
output.write('\x1b[?25h');
|
|
442
|
+
io.close();
|
|
443
|
+
resolve(state.filter((i) => i.checked).map((i) => i.id));
|
|
444
|
+
};
|
|
445
|
+
const onKey = (str, key = {}) => {
|
|
446
|
+
if (key.ctrl && key.name === 'c') {
|
|
447
|
+
output.write('\x1b[?25h\n');
|
|
448
|
+
process.exit(130);
|
|
449
|
+
}
|
|
450
|
+
if (key.name === 'up') cur = (cur + state.length - 1) % state.length;
|
|
451
|
+
else if (key.name === 'down') cur = (cur + 1) % state.length;
|
|
452
|
+
else if (key.name === 'space') state[cur].checked = !state[cur].checked;
|
|
453
|
+
else if (key.name === 'a') {
|
|
454
|
+
const all = state.every((i) => i.checked);
|
|
455
|
+
state.forEach((i) => (i.checked = !all));
|
|
456
|
+
} else if (key.name === 'return') {
|
|
457
|
+
draw();
|
|
458
|
+
return finish();
|
|
459
|
+
}
|
|
460
|
+
draw();
|
|
461
|
+
};
|
|
462
|
+
readline.emitKeypressEvents(input);
|
|
463
|
+
input.setRawMode(true);
|
|
464
|
+
input.resume();
|
|
465
|
+
output.write('\x1b[?25l');
|
|
466
|
+
input.on('keypress', onKey);
|
|
467
|
+
draw();
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
async function chooseTools(inst, o) {
|
|
472
|
+
if (o.tools) {
|
|
473
|
+
const t = o.tools === 'auto' ? 'auto' : o.tools === 'none' ? [] : o.tools.split(',').filter(Boolean);
|
|
474
|
+
if (t !== 'auto') inst.chosen = t;
|
|
475
|
+
return t;
|
|
476
|
+
}
|
|
477
|
+
if (!o.configure) {
|
|
478
|
+
const saved = inst.loadConfig();
|
|
479
|
+
if (saved) return saved;
|
|
480
|
+
}
|
|
481
|
+
if (!o.yes && !process.env.KAGENTS_NO_PROMPT && !process.env.CI) {
|
|
482
|
+
const io = openTTY();
|
|
483
|
+
if (io) {
|
|
484
|
+
const detected = inst.detectTools();
|
|
485
|
+
const picked = await multiselect(io, 'Pour quels outils installer KAgents ? (↑↓ espace a entrée)', [
|
|
486
|
+
{ id: 'claude', label: 'Claude Code (.claude/commands, .claude/agents)', checked: detected.includes('claude') },
|
|
487
|
+
{ id: 'cursor', label: 'Cursor (.cursor/commands, .cursor/agents, .cursor/rules)', checked: detected.includes('cursor') },
|
|
488
|
+
{ id: 'agents', label: 'Codex et autres outils AGENTS.md (.agents/skills)', checked: true },
|
|
489
|
+
]);
|
|
490
|
+
inst.chosen = picked;
|
|
491
|
+
return picked;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
return 'auto';
|
|
495
|
+
}
|
|
496
|
+
|
|
350
497
|
// --- Main ---------------------------------------------------------------------
|
|
351
|
-
function main() {
|
|
498
|
+
async function main() {
|
|
352
499
|
const o = parseArgs(process.argv.slice(2));
|
|
353
500
|
if (o.cmd === 'help') return console.log(HELP);
|
|
354
501
|
if (o.cmd === 'version') return console.log(PKG.version);
|
|
355
502
|
const target = process.cwd();
|
|
356
503
|
if (path.resolve(target) === PKG_ROOT) die("à lancer depuis la racine d'un projet, pas depuis le kit.");
|
|
357
504
|
const inst = new Installer(target, o.copy);
|
|
358
|
-
if (o.cmd === 'uninstall') inst.uninstall();
|
|
359
|
-
|
|
505
|
+
if (o.cmd === 'uninstall') return inst.uninstall();
|
|
506
|
+
const tools = await chooseTools(inst, o);
|
|
507
|
+
inst.install(tools);
|
|
508
|
+
if (inst.chosen) inst.saveConfig(inst.chosen);
|
|
360
509
|
}
|
|
361
510
|
|
|
362
511
|
main();
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
// Lancé par npm/pnpm après `add @dev-kosaly/kagents` : installe le kit dans le projet.
|
|
4
|
+
// Ne doit jamais faire échouer l'installation des dépendances.
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { spawnSync } = require('child_process');
|
|
7
|
+
|
|
8
|
+
const pkgRoot = path.resolve(__dirname, '..');
|
|
9
|
+
const project = process.env.INIT_CWD;
|
|
10
|
+
|
|
11
|
+
const skip = (why) => {
|
|
12
|
+
if (why) console.log(`kagents: installation automatique ignorée (${why}). Lancez : npx kagents`);
|
|
13
|
+
process.exit(0);
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
if (process.env.KAGENTS_SKIP_POSTINSTALL) skip();
|
|
17
|
+
if (!project) skip();
|
|
18
|
+
if (process.env.npm_config_global === 'true') skip('installation globale');
|
|
19
|
+
if (!pkgRoot.split(path.sep).includes('node_modules')) skip(); // développement du kit lui-même
|
|
20
|
+
if (path.resolve(project) === pkgRoot) skip();
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
spawnSync(process.execPath, [path.join(__dirname, 'kagents.js')], { cwd: project, stdio: 'inherit', env: { ...process.env, KAGENTS_FROM_POSTINSTALL: '1' } });
|
|
24
|
+
} catch {
|
|
25
|
+
/* jamais bloquant */
|
|
26
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dev-kosaly/kagents",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Kit d'agents IA d'ingénierie installable dans un projet (Claude Code, Cursor, AGENTS.md)",
|
|
5
5
|
"bin": {
|
|
6
6
|
"kagents": "bin/kagents.js"
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
"access": "public"
|
|
30
30
|
},
|
|
31
31
|
"scripts": {
|
|
32
|
+
"postinstall": "node bin/postinstall.js",
|
|
32
33
|
"test": "node scripts/smoke-test.js",
|
|
33
34
|
"prepublishOnly": "npm test"
|
|
34
35
|
},
|