everything-gemini 1.8.0 → 2.0.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.
@@ -0,0 +1,221 @@
1
+ ---
2
+ name: triton-inference-server
3
+ description: Deploys, configures, and tunes NVIDIA Triton Inference Server for high-throughput, low-latency multi-model serving with dynamic batching and GPU acceleration.
4
+ ---
5
+
6
+ # Triton Inference Server
7
+
8
+ Provides production architecture and configuration patterns for serving deep learning and LLM models at enterprise scale with NVIDIA Triton Inference Server.
9
+
10
+ ---
11
+
12
+ ## 1. Architecture & Execution Model
13
+
14
+ ```
15
+ Client Application (gRPC / HTTP KServe v2)
16
+ │
17
+ ▼
18
+ ┌──────────────────────────────────────────────┐
19
+ │ NVIDIA Triton Inference Server │
20
+ │ │
21
+ │ ┌──────────────────┐ ┌──────────────────┐ │
22
+ │ │ Dynamic Batching │ │ Model Concurrency│ │
23
+ │ │ Scheduler │ │ & GPU Instances │ │
24
+ │ └─────────┬────────┘ └────────┬─────────┘ │
25
+ │ │ │ │
26
+ │ ▼ ▼ │
27
+ │ ┌────────────────────────────────────────┐ │
28
+ │ │ Backend Engines │ │
29
+ │ │ - TensorRT-LLM (High-speed generative)│ │
30
+ │ │ - vLLM Engine (PagedAttention serving)│ │
31
+ │ │ - ONNX Runtime (Embedding & classifier│ │
32
+ │ │ - Python Backend (Custom preprocessing│ │
33
+ │ └────────────────────────────────────────┘ │
34
+ └──────────────────────┬───────────────────────┘
35
+ │
36
+ ▼
37
+ NVIDIA Tensor Core GPUs (H100/A100/L4)
38
+ ```
39
+
40
+ ---
41
+
42
+ ## 2. Model Repository Layout
43
+
44
+ Triton requires a strict directory layout with versioned subdirectories and `config.pbtxt`:
45
+
46
+ ```text
47
+ model_repository/
48
+ ├── embedding_model/
49
+ │ ├── 1/
50
+ │ │ └── model.onnx
51
+ │ └── config.pbtxt
52
+ └── llm_generator/
53
+ ├── 1/
54
+ │ └── model.py
55
+ └── config.pbtxt
56
+ ```
57
+
58
+ ---
59
+
60
+ ## 3. High-Throughput `config.pbtxt` Configuration
61
+
62
+ Configures dynamic batching, GPU instance groups, and TensorRT acceleration:
63
+
64
+ ```protobuf
65
+ name: "embedding_model"
66
+ platform: "onnxruntime_onnx"
67
+ max_batch_size: 128
68
+
69
+ input [
70
+ {
71
+ name: "input_ids"
72
+ data_type: TYPE_INT64
73
+ dims: [ -1 ]
74
+ },
75
+ {
76
+ name: "attention_mask"
77
+ data_type: TYPE_INT64
78
+ dims: [ -1 ]
79
+ }
80
+ ]
81
+
82
+ output [
83
+ {
84
+ name: "sentence_embedding"
85
+ data_type: TYPE_FP32
86
+ dims: [ 768 ]
87
+ }
88
+ ]
89
+
90
+ # Maximize GPU utilization via dynamic queue batching
91
+ dynamic_batching {
92
+ max_queue_delay_microseconds: 5000 # 5ms SLA window
93
+ preferred_batch_size: [ 16, 32, 64, 128 ]
94
+ }
95
+
96
+ # Run 2 concurrent model instances on GPU 0
97
+ instance_group [
98
+ {
99
+ count: 2
100
+ kind: KIND_GPU
101
+ gpus: [ 0 ]
102
+ }
103
+ ]
104
+
105
+ optimization {
106
+ execution_accelerators {
107
+ gpu_execution_accelerator : [ {
108
+ name : "tensorrt"
109
+ parameters { key: "precision_mode" value: "FP16" }
110
+ parameters { key: "max_workspace_size_bytes" value: "4294967296" }
111
+ }]
112
+ }
113
+ }
114
+ ```
115
+
116
+ ---
117
+
118
+ ## 4. High-Performance gRPC Client (Python)
119
+
120
+ Uses binary gRPC streaming with zero-copy serialization:
121
+
122
+ ```python
123
+ import numpy as np
124
+ import tritonclient.grpc as grpcclient
125
+ from tritonclient.grpc import InferInput, InferRequestedOutput
126
+
127
+ def query_triton_embeddings(
128
+ texts_token_ids: np.ndarray,
129
+ attention_masks: np.ndarray,
130
+ server_url: str = "localhost:8001"
131
+ ) -> np.ndarray:
132
+ """Queries Triton via low-latency gRPC channel."""
133
+ with grpcclient.InferenceServerClient(url=server_url) as client:
134
+ # Check server readiness
135
+ if not client.is_server_ready():
136
+ raise RuntimeError("Triton server is not healthy.")
137
+
138
+ inputs = [
139
+ InferInput("input_ids", texts_token_ids.shape, "INT64"),
140
+ InferInput("attention_mask", attention_masks.shape, "INT64")
141
+ ]
142
+ inputs[0].set_data_from_numpy(texts_token_ids)
143
+ inputs[1].set_data_from_numpy(attention_masks)
144
+
145
+ outputs = [InferRequestedOutput("sentence_embedding")]
146
+
147
+ response = client.infer(
148
+ model_name="embedding_model",
149
+ inputs=inputs,
150
+ outputs=outputs,
151
+ timeout=10.0
152
+ )
153
+
154
+ return response.as_numpy("sentence_embedding")
155
+ ```
156
+
157
+ ---
158
+
159
+ ## 5. Production Docker & Kubernetes Deployment
160
+
161
+ ```yaml
162
+ apiVersion: apps/v1
163
+ kind: Deployment
164
+ metadata:
165
+ name: triton-inference-server
166
+ namespace: mlops
167
+ spec:
168
+ replicas: 2
169
+ selector:
170
+ matchLabels:
171
+ app: triton
172
+ template:
173
+ metadata:
174
+ labels:
175
+ app: triton
176
+ spec:
177
+ containers:
178
+ - name: triton
179
+ image: nvcr.io/nvidia/tritonserver:24.08-py3
180
+ args: ["tritonserver", "--model-repository=/models", "--strict-model-config=true"]
181
+ resources:
182
+ limits:
183
+ nvidia.com/gpu: 1
184
+ memory: 16Gi
185
+ cpu: "8"
186
+ ports:
187
+ - containerPort: 8000 # HTTP
188
+ - containerPort: 8001 # gRPC
189
+ - containerPort: 8002 # Metrics (Prometheus)
190
+ livenessProbe:
191
+ httpGet:
192
+ path: /v2/health/live
193
+ port: 8000
194
+ initialDelaySeconds: 30
195
+ readinessProbe:
196
+ httpGet:
197
+ path: /v2/health/ready
198
+ port: 8000
199
+ initialDelaySeconds: 15
200
+ ```
201
+
202
+ ---
203
+
204
+ ## 6. Performance Benchmarking & Profiling
205
+
206
+ ```bash
207
+ # Verify health and loaded models via v2 API
208
+ curl -s http://localhost:8000/v2/health/ready
209
+ curl -s http://localhost:8000/v2/models/embedding_model/ready
210
+
211
+ # Profile throughput and P99 latency with Perf Analyzer
212
+ perf_analyzer -m embedding_model \
213
+ -u localhost:8001 -i gRPC \
214
+ --concurrency-range 1:16 \
215
+ --shape input_ids:128 \
216
+ --shape attention_mask:128 \
217
+ --percentile 99
218
+
219
+ # Query Prometheus metrics
220
+ curl -s http://localhost:8002/metrics | grep nv_inference_
221
+ ```
@@ -0,0 +1,129 @@
1
+ ---
2
+ name: vault-secrets-management
3
+ description: >-
4
+ Architects and automates HashiCorp Vault secrets management, AppRole machine authentication, dynamic ephemeral database credentials, and PKI mTLS certificate rotation.
5
+ ---
6
+
7
+ # HashiCorp Vault Secrets Management & Ephemeral Credentials
8
+
9
+ This skill guides the design, automation, and operational patterns for HashiCorp Vault enterprise secrets orchestration, dynamic credential generation, and PKI certificate authority workflows.
10
+
11
+ ---
12
+
13
+ ## ⚡ Secret Engine Capabilities Matrix
14
+
15
+ | Secret Engine | Purpose | Lifecycle Behavior |
16
+ | :--- | :--- | :--- |
17
+ | **`kv-v2`** | Static credentials & API keys | Versioned key-value store with soft delete and rollback. |
18
+ | **`database`** | Ephemeral DB access (Postgres, MySQL) | Generates short-lived user credentials and revokes upon lease expiry. |
19
+ | **`pki`** | Internal mTLS & TLS certificates | Acts as CA/sub-CA; dynamically issues certificates in milliseconds. |
20
+ | **`transit`** | Cryptography as a service | Encrypts/decrypts data without exposing raw encryption keys. |
21
+
22
+ ---
23
+
24
+ ## 🔐 AppRole Machine Authentication Workflow
25
+
26
+ Applications authenticate using decoupled `role_id` and short-lived `secret_id`:
27
+
28
+ ```bash
29
+ # 1. Enable AppRole auth method
30
+ vault auth enable approle
31
+
32
+ # 2. Configure service role with strict CIDR and token policies
33
+ vault write auth/approle/role/api-service \
34
+ secret_id_ttl=60m \
35
+ token_ttl=30m \
36
+ token_max_ttl=4h \
37
+ secret_id_num_uses=10 \
38
+ token_policies="api-service-policy" \
39
+ bind_secret_id=true
40
+
41
+ # 3. Retrieve Role ID (stored in deployment manifest)
42
+ vault read auth/approle/role/api-service/role-id
43
+
44
+ # 4. Generate Secret ID (delivered securely via CI/CD or orchestrator)
45
+ vault write -f auth/approle/role/api-service/secret-id
46
+ ```
47
+
48
+ Node.js / Python authentication client:
49
+
50
+ ```typescript
51
+ import axios from 'axios';
52
+
53
+ export async function loginWithAppRole(vaultAddr: string, roleId: string, secretId: string) {
54
+ const response = await axios.post(`${vaultAddr}/v1/auth/approle/login`, {
55
+ role_id: roleId,
56
+ secret_id: secretId,
57
+ });
58
+
59
+ const clientToken = response.data.auth.client_token;
60
+ const leaseDuration = response.data.auth.lease_duration;
61
+ return { clientToken, leaseDuration };
62
+ }
63
+ ```
64
+
65
+ ---
66
+
67
+ ## ⏳ Dynamic Ephemeral Database Credentials
68
+
69
+ Configure Vault to create temporary, unique PostgreSQL roles for each application instance:
70
+
71
+ ```bash
72
+ # 1. Mount database engine
73
+ vault secrets enable database
74
+
75
+ # 2. Configure connection to PostgreSQL
76
+ vault write database/config/production-postgres \
77
+ plugin_name=postgresql-database-plugin \
78
+ allowed_roles="readwrite-service" \
79
+ connection_url="postgresql://{{username}}:{{password}}@postgres:5432/production?sslmode=verify-full" \
80
+ username="vaultadmin" \
81
+ password="supersecretadminpassword"
82
+
83
+ # 3. Define role template with automated role creation SQL
84
+ vault write database/roles/readwrite-service \
85
+ db_name=production-postgres \
86
+ creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; \
87
+ GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO \"{{name}}\";" \
88
+ default_ttl="1h" \
89
+ max_ttl="24h"
90
+ ```
91
+
92
+ Application requests credentials on demand:
93
+
94
+ ```bash
95
+ vault read database/creds/readwrite-service
96
+ # Returns: username: v-approle-readwrite-abc123, password: xyz..., lease_id: database/creds/readwrite-service/xyz...
97
+ ```
98
+
99
+ ---
100
+
101
+ ## 📜 PKI Dynamic mTLS Certificate Issuance
102
+
103
+ Automate TLS certificate issuance without manual CSR handling:
104
+
105
+ ```bash
106
+ # Mount PKI engine and configure Root/Intermediate CA
107
+ vault secrets enable pki
108
+ vault secrets tune -max-lease-ttl=87600h pki
109
+
110
+ # Configure role for cluster services
111
+ vault write pki/roles/internal-mesh \
112
+ allowed_domains="internal.cluster.local" \
113
+ allow_subdomains=true \
114
+ max_ttl="72h"
115
+
116
+ # Generate certificate on demand
117
+ vault write pki/issue/internal-mesh \
118
+ common_name="billing-service.internal.cluster.local" \
119
+ ttl="24h"
120
+ ```
121
+
122
+ ---
123
+
124
+ ## 📋 Architectural Rules & Best Practices
125
+
126
+ 1. **Never Hardcode Secrets**: Eliminate long-lived static DB passwords; adopt dynamic ephemeral database roles with short TTLs (< 2 hours).
127
+ 2. **Strict Secret ID Binding**: Bind AppRole `secret_id` to expected subnet CIDRs (`secret_id_bound_cidrs`) to stop credential theft across VPCs.
128
+ 3. **Lease Renewal & Heartbeats**: Background workers must periodically renew active leases (`vault write sys/leases/renew lease_id=...`) before expiry.
129
+ 4. **Audit Logging**: Enable Vault audit devices (`vault audit enable file file_path=/var/log/vault/audit.log`) with hashed payload sanitization.
@@ -53,6 +53,17 @@
53
53
  "SLACK_BOT_TOKEN": "${SLACK_BOT_TOKEN}",
54
54
  "SLACK_TEAM_ID": "${SLACK_TEAM_ID}"
55
55
  }
56
+ },
57
+ "puppeteer": {
58
+ "command": "npx",
59
+ "args": ["-y", "@modelcontextprotocol/server-puppeteer"]
60
+ },
61
+ "brave-search": {
62
+ "command": "npx",
63
+ "args": ["-y", "@modelcontextprotocol/server-brave-search"],
64
+ "env": {
65
+ "BRAVE_API_KEY": "${BRAVE_API_KEY}"
66
+ }
56
67
  }
57
68
  }
58
69
  }
package/tests/cli.test.js CHANGED
@@ -90,9 +90,9 @@ it('should benchmark ecosystem components and output valid JSON with --json', ()
90
90
  const res = runCli('benchmark --json');
91
91
  assert.strictEqual(res.status, 0);
92
92
  const data = JSON.parse(res.stdout);
93
- assert.strictEqual(data.totalComponents, 70);
94
- assert.strictEqual(data.progressiveSkillsCount, 52);
95
- assert.strictEqual(data.scopedAgentsCount, 11);
93
+ assert.strictEqual(data.totalComponents, 80);
94
+ assert.strictEqual(data.progressiveSkillsCount, 60);
95
+ assert.strictEqual(data.scopedAgentsCount, 13);
96
96
  assert.strictEqual(data.lifecycleHooksCount, 7);
97
97
  assert(data.totalRunbookTokens > 15000, 'Expected >15,000 runbook tokens');
98
98
  assert(data.estimatedTokensSavedPerTurn > 8000, 'Expected significant token savings');
@@ -316,7 +316,7 @@ try {
316
316
  const dryRes = runCli(`bundle --output "${bundleTarget}" --dry-run`);
317
317
  assert.strictEqual(dryRes.status, 0);
318
318
  assert.match(dryRes.stdout, /DRY RUN MODE ACTIVATED/);
319
- assert.match(dryRes.stdout, /Skills to bundle: 52/);
319
+ assert.match(dryRes.stdout, /Skills to bundle: 60/);
320
320
  assert.strictEqual(fs.existsSync(bundleTarget), false, 'Dry run should not create destination directory');
321
321
 
322
322
  // JSON Dry-run mode
@@ -324,7 +324,7 @@ try {
324
324
  assert.strictEqual(jsonRes.status, 0);
325
325
  const dryData = JSON.parse(jsonRes.stdout);
326
326
  assert.strictEqual(dryData.dryRun, true);
327
- assert.strictEqual(dryData.manifest.components.totalCount, 70);
327
+ assert.strictEqual(dryData.manifest.components.totalCount, 80);
328
328
 
329
329
  // Live mode
330
330
  const liveRes = runCli(`bundle --output "${bundleTarget}"`);
@@ -332,8 +332,49 @@ try {
332
332
  assert.match(liveRes.stdout, /Offline Distribution Bundle Created/);
333
333
  assert.strictEqual(fs.existsSync(path.join(bundleTarget, 'bundle-manifest.json')), true);
334
334
  assert.strictEqual(fs.existsSync(path.join(bundleTarget, 'offline-install.sh')), true);
335
- assert.strictEqual(fs.existsSync(path.join(bundleTarget, 'skills', 'vllm-inference')), true);
336
- assert.strictEqual(fs.existsSync(path.join(bundleTarget, 'agents', 'incident-responder.md')), true);
335
+ assert.strictEqual(fs.existsSync(path.join(bundleTarget, 'skills', 'milvus-vector-clustering')), true);
336
+ assert.strictEqual(fs.existsSync(path.join(bundleTarget, 'agents', 'platform-engineer.md')), true);
337
+ });
338
+
339
+ // Scaffold Command
340
+ it('should scaffold skill, agent, and hook in dry-run and live modes', () => {
341
+ const scaffoldWorkspace = path.join(tempWorkspace, 'scaffold-ws');
342
+
343
+ // Skill Dry-run
344
+ const drySkill = runCli(`scaffold skill demo-test-skill --workspace "${scaffoldWorkspace}" --dry-run --json`);
345
+ assert.strictEqual(drySkill.status, 0);
346
+ const drySkillData = JSON.parse(drySkill.stdout);
347
+ assert.strictEqual(drySkillData.isDryRun, true);
348
+ assert.strictEqual(drySkillData.type, 'skill');
349
+
350
+ // Live Agent Scaffold
351
+ const liveAgent = runCli(`scaffold agent demo-qa-agent --workspace "${scaffoldWorkspace}"`);
352
+ assert.strictEqual(liveAgent.status, 0);
353
+ assert.match(liveAgent.stdout, /Created subagent profile/);
354
+ assert.strictEqual(fs.existsSync(path.join(scaffoldWorkspace, 'agents', 'demo-qa-agent.md')), true);
355
+
356
+ // Live Hook Scaffold
357
+ const liveHook = runCli(`scaffold hook demo-safety --workspace "${scaffoldWorkspace}"`);
358
+ assert.strictEqual(liveHook.status, 0);
359
+ assert.match(liveHook.stdout, /Created lifecycle hook/);
360
+ assert.strictEqual(fs.existsSync(path.join(scaffoldWorkspace, 'hooks', 'demo-safety', 'hooks.json')), true);
361
+ assert.strictEqual(fs.existsSync(path.join(scaffoldWorkspace, 'hooks', 'demo-safety', 'scripts', 'demo-safety-gate.sh')), true);
362
+ });
363
+
364
+ // Sync Command
365
+ it('should synchronize workspace with sync command in dry-run and live modes', () => {
366
+ // Dry-run
367
+ const drySync = runCli(`sync --workspace "${tempWorkspace}" --dry-run`);
368
+ assert.strictEqual(drySync.status, 0);
369
+ assert.match(drySync.stdout, /DRY RUN MODE ACTIVATED/);
370
+ assert.match(drySync.stdout, /Synchronizing Workspace with Catalog/);
371
+
372
+ // JSON Dry-run
373
+ const jsonSync = runCli(`sync --workspace "${tempWorkspace}" --dry-run --json`);
374
+ assert.strictEqual(jsonSync.status, 0);
375
+ const syncData = JSON.parse(jsonSync.stdout);
376
+ assert.strictEqual(syncData.isDryRun, true);
377
+ assert.strictEqual(syncData.success, true);
337
378
  });
338
379
 
339
380
  // Info for incident-responder and v1.8.0 skills
@@ -359,14 +400,80 @@ try {
359
400
  assert.match(resLite.stdout, /LiteLLM/);
360
401
  });
361
402
 
403
+ // Info for platform-engineer and v1.9.0 skills
404
+ it('should display info for platform-engineer subagent and v1.9.0 skills', () => {
405
+ const resPlat = runCli('info platform-engineer');
406
+ assert.strictEqual(resPlat.status, 0);
407
+ assert.match(resPlat.stdout, /Platform Engineer/);
408
+
409
+ const resMilvus = runCli('info milvus-vector-clustering');
410
+ assert.strictEqual(resMilvus.status, 0);
411
+ assert.match(resMilvus.stdout, /Milvus/);
412
+
413
+ const resR2 = runCli('info cloudflare-r2-object-storage');
414
+ assert.strictEqual(resR2.status, 0);
415
+ assert.match(resR2.stdout, /Cloudflare R2/);
416
+
417
+ const resVault = runCli('info vault-secrets-management');
418
+ assert.strictEqual(resVault.status, 0);
419
+ assert.match(resVault.stdout, /Vault/);
420
+
421
+ const resRate = runCli('info distributed-rate-limiting');
422
+ assert.strictEqual(resRate.status, 0);
423
+ assert.match(resRate.stdout, /Distributed Rate Limiting/);
424
+ });
425
+
426
+ // Info for mlops-engineer and v2.0.0 skills
427
+ it('should display info for mlops-engineer subagent and v2.0.0 skills', () => {
428
+ const resMl = runCli('info mlops-engineer');
429
+ assert.strictEqual(resMl.status, 0);
430
+ assert.match(resMl.stdout, /MLOps Engineer/);
431
+
432
+ const resKeda = runCli('info keda-event-autoscaling');
433
+ assert.strictEqual(resKeda.status, 0);
434
+ assert.match(resKeda.stdout, /KEDA/);
435
+
436
+ const resChroma = runCli('info chromadb-vector-store');
437
+ assert.strictEqual(resChroma.status, 0);
438
+ assert.match(resChroma.stdout, /ChromaDB/);
439
+
440
+ const resOpa = runCli('info opa-gatekeeper-policy');
441
+ assert.strictEqual(resOpa.status, 0);
442
+ assert.match(resOpa.stdout, /Gatekeeper/);
443
+
444
+ const resTriton = runCli('info triton-inference-server');
445
+ assert.strictEqual(resTriton.status, 0);
446
+ assert.match(resTriton.stdout, /Triton/);
447
+ });
448
+
449
+ // Graph command verification
450
+ it('should render interaction graph in terminal, mermaid, and JSON formats', () => {
451
+ const resTerm = runCli('graph');
452
+ assert.strictEqual(resTerm.status, 0);
453
+ assert.match(resTerm.stdout, /Multi-Agent Topology & Dependency Graph/);
454
+ assert.match(resTerm.stdout, /MLOPS-ENGINEER/);
455
+
456
+ const resMermaid = runCli('graph --mermaid');
457
+ assert.strictEqual(resMermaid.status, 0);
458
+ assert.match(resMermaid.stdout, /graph TD/);
459
+ assert.match(resMermaid.stdout, /A_mlops_engineer/);
460
+
461
+ const resJson = runCli('graph --json');
462
+ assert.strictEqual(resJson.status, 0);
463
+ const data = JSON.parse(resJson.stdout);
464
+ assert.ok(data.totalNodes > 50, 'Graph should contain > 50 nodes');
465
+ assert.ok(data.totalEdges > 50, 'Graph should contain > 50 edges');
466
+ assert.ok(data.nodes.find(n => n.id === 'agent:mlops-engineer'));
467
+ });
468
+
362
469
  // Benchmark Components Verification
363
- it('should report 70 components, 52 skills, and 11 subagents in benchmark --json', () => {
470
+ it('should report 80 components, 60 skills, and 13 subagents in benchmark --json', () => {
364
471
  const res = runCli('benchmark --json');
365
472
  assert.strictEqual(res.status, 0);
366
473
  const data = JSON.parse(res.stdout);
367
- assert.strictEqual(data.totalComponents, 70, 'Total components should be 70');
368
- assert.strictEqual(data.progressiveSkillsCount, 52, 'Progressive skills should be 52');
369
- assert.strictEqual(data.scopedAgentsCount, 11, 'Scoped agents should be 11');
474
+ assert.strictEqual(data.totalComponents, 80, 'Total components should be 80');
475
+ assert.strictEqual(data.progressiveSkillsCount, 60, 'Progressive skills should be 60');
476
+ assert.strictEqual(data.scopedAgentsCount, 13, 'Scoped agents should be 13');
370
477
  assert.strictEqual(data.lifecycleHooksCount, 7, 'Lifecycle hooks should be 7');
371
478
  });
372
479