loopctl-mcp-server 1.0.1 → 1.1.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 +15 -5
- package/index.js +262 -12
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
MCP (Model Context Protocol) server for [loopctl](https://loopctl.com) -- structural trust for AI development loops.
|
|
4
4
|
|
|
5
|
-
Wraps the loopctl REST API into
|
|
5
|
+
Wraps the loopctl REST API into 24 typed MCP tools so AI coding agents (Claude Code, etc.) can interact with loopctl without writing curl commands.
|
|
6
6
|
|
|
7
7
|
## Installation
|
|
8
8
|
|
|
@@ -65,7 +65,7 @@ Or if installed locally:
|
|
|
65
65
|
|
|
66
66
|
Key resolution priority: `LOOPCTL_API_KEY` > tool-specific key > `LOOPCTL_ORCH_KEY`.
|
|
67
67
|
|
|
68
|
-
## Tools (
|
|
68
|
+
## Tools (24)
|
|
69
69
|
|
|
70
70
|
### Project Tools
|
|
71
71
|
|
|
@@ -74,14 +74,14 @@ Key resolution priority: `LOOPCTL_API_KEY` > tool-specific key > `LOOPCTL_ORCH_K
|
|
|
74
74
|
| `get_tenant` | Get current tenant info. Use to verify connectivity. |
|
|
75
75
|
| `list_projects` | List all projects in the current tenant. |
|
|
76
76
|
| `create_project` | Create a new project in the current tenant. |
|
|
77
|
-
| `get_progress` | Get progress summary for a project, including story counts by status. |
|
|
77
|
+
| `get_progress` | Get progress summary for a project, including story counts by status. Pass `include_cost=true` for cost data. |
|
|
78
78
|
| `import_stories` | Import stories into a project from a structured payload (Epic 12 import format). |
|
|
79
79
|
|
|
80
80
|
### Story Tools
|
|
81
81
|
|
|
82
82
|
| Tool | Description |
|
|
83
83
|
|---|---|
|
|
84
|
-
| `list_stories` | List stories for a project, optionally filtered by agent_status, verified_status, or epic_id. |
|
|
84
|
+
| `list_stories` | List stories for a project, optionally filtered by agent_status, verified_status, or epic_id. Pass `include_token_totals=true` for per-story token data. |
|
|
85
85
|
| `list_ready_stories` | List stories that are ready to be worked on (contracted, dependencies met). |
|
|
86
86
|
| `get_story` | Get full details for a single story by ID. |
|
|
87
87
|
|
|
@@ -98,7 +98,7 @@ Key resolution priority: `LOOPCTL_API_KEY` > tool-specific key > `LOOPCTL_ORCH_K
|
|
|
98
98
|
|
|
99
99
|
| Tool | Description |
|
|
100
100
|
|---|---|
|
|
101
|
-
| `report_story` | Reviewer confirms the implementation is done. Transitions implementing -> reported_done. |
|
|
101
|
+
| `report_story` | Reviewer confirms the implementation is done. Transitions implementing -> reported_done. Accepts optional `token_usage` object. |
|
|
102
102
|
| `review_complete` | Record that a review has been completed for a story. Required before verify. |
|
|
103
103
|
|
|
104
104
|
### Verification Tools (orchestrator key)
|
|
@@ -115,6 +115,16 @@ Key resolution priority: `LOOPCTL_API_KEY` > tool-specific key > `LOOPCTL_ORCH_K
|
|
|
115
115
|
| `bulk_mark_complete` | Bulk mark multiple stories as complete in a single API call. |
|
|
116
116
|
| `verify_all_in_epic` | Bulk verify all reported_done, unverified stories in an epic. |
|
|
117
117
|
|
|
118
|
+
### Token Efficiency Tools
|
|
119
|
+
|
|
120
|
+
| Tool | Auth Key | Description |
|
|
121
|
+
|---|---|---|
|
|
122
|
+
| `report_token_usage` | agent | Report input/output token counts, model name, and cost for a story session. Calls `POST /api/v1/token-usage`. |
|
|
123
|
+
| `get_cost_summary` | orch | Get cost/token usage summary for a project, optionally broken down by `agent`, `epic`, or `model`. |
|
|
124
|
+
| `get_story_token_usage` | orch | Get all token usage records for a single story. |
|
|
125
|
+
| `get_cost_anomalies` | orch | Get cost anomaly alerts — stories or agents exceeding expected budgets. Optionally filter by project. |
|
|
126
|
+
| `set_token_budget` | orch | Set a token budget (in millicents) for a project, epic, story, or agent scope. Requires orchestrator role. |
|
|
127
|
+
|
|
118
128
|
### Discovery Tools
|
|
119
129
|
|
|
120
130
|
| Tool | Description |
|
package/index.js
CHANGED
|
@@ -115,6 +115,32 @@ function toContent(result) {
|
|
|
115
115
|
};
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
+
/**
|
|
119
|
+
* Compact variant for list endpoints — strips bulky fields (acceptance_criteria,
|
|
120
|
+
* description) from each story to stay within token limits. Callers can use
|
|
121
|
+
* get_story for full details on individual stories.
|
|
122
|
+
*/
|
|
123
|
+
function toContentCompact(result) {
|
|
124
|
+
if (result && result.error === true) return toContent(result);
|
|
125
|
+
|
|
126
|
+
if (result && Array.isArray(result.data)) {
|
|
127
|
+
const compact = {
|
|
128
|
+
...result,
|
|
129
|
+
data: result.data.map(({ acceptance_criteria, description, ...rest }) => rest),
|
|
130
|
+
};
|
|
131
|
+
return {
|
|
132
|
+
content: [
|
|
133
|
+
{
|
|
134
|
+
type: "text",
|
|
135
|
+
text: JSON.stringify(compact, null, 2),
|
|
136
|
+
},
|
|
137
|
+
],
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return toContent(result);
|
|
142
|
+
}
|
|
143
|
+
|
|
118
144
|
// ---------------------------------------------------------------------------
|
|
119
145
|
// Tool implementations
|
|
120
146
|
// ---------------------------------------------------------------------------
|
|
@@ -140,8 +166,11 @@ async function createProject({ name, slug, repo_url, description, tech_stack })
|
|
|
140
166
|
return toContent(result);
|
|
141
167
|
}
|
|
142
168
|
|
|
143
|
-
async function getProgress({ project_id }) {
|
|
144
|
-
const
|
|
169
|
+
async function getProgress({ project_id, include_cost }) {
|
|
170
|
+
const params = new URLSearchParams();
|
|
171
|
+
if (include_cost) params.set("include_cost", "true");
|
|
172
|
+
const query = params.toString() ? `?${params}` : "";
|
|
173
|
+
const result = await apiCall("GET", `/api/v1/projects/${project_id}/progress${query}`);
|
|
145
174
|
return toContent(result);
|
|
146
175
|
}
|
|
147
176
|
|
|
@@ -157,24 +186,25 @@ async function importStories({ project_id, payload }) {
|
|
|
157
186
|
|
|
158
187
|
// --- Story Tools ---
|
|
159
188
|
|
|
160
|
-
async function listStories({ project_id, agent_status, verified_status, epic_id, limit, offset }) {
|
|
189
|
+
async function listStories({ project_id, agent_status, verified_status, epic_id, limit, offset, include_token_totals }) {
|
|
161
190
|
const params = new URLSearchParams({ project_id });
|
|
162
191
|
if (agent_status) params.set("agent_status", agent_status);
|
|
163
192
|
if (verified_status) params.set("verified_status", verified_status);
|
|
164
193
|
if (epic_id) params.set("epic_id", epic_id);
|
|
165
|
-
|
|
194
|
+
params.set("limit", String(limit ?? 20));
|
|
166
195
|
if (offset != null) params.set("offset", String(offset));
|
|
196
|
+
if (include_token_totals) params.set("include_token_totals", "true");
|
|
167
197
|
|
|
168
198
|
const result = await apiCall("GET", `/api/v1/stories?${params}`);
|
|
169
|
-
return
|
|
199
|
+
return toContentCompact(result);
|
|
170
200
|
}
|
|
171
201
|
|
|
172
202
|
async function listReadyStories({ project_id, limit }) {
|
|
173
203
|
const params = new URLSearchParams({ project_id });
|
|
174
|
-
|
|
204
|
+
params.set("limit", String(limit ?? 20));
|
|
175
205
|
|
|
176
206
|
const result = await apiCall("GET", `/api/v1/stories/ready?${params}`);
|
|
177
|
-
return
|
|
207
|
+
return toContentCompact(result);
|
|
178
208
|
}
|
|
179
209
|
|
|
180
210
|
async function getStory({ story_id }) {
|
|
@@ -226,13 +256,16 @@ async function requestReview({ story_id }) {
|
|
|
226
256
|
|
|
227
257
|
// --- Reviewer Tools (orch key — reviewer uses orchestrator role) ---
|
|
228
258
|
|
|
229
|
-
async function reportStory({ story_id, artifact_type, artifact_path }) {
|
|
259
|
+
async function reportStory({ story_id, artifact_type, artifact_path, token_usage }) {
|
|
230
260
|
const body = {};
|
|
231
261
|
if (artifact_type || artifact_path) {
|
|
232
262
|
body.artifact = {};
|
|
233
263
|
if (artifact_type) body.artifact.artifact_type = artifact_type;
|
|
234
264
|
if (artifact_path) body.artifact.path = artifact_path;
|
|
235
265
|
}
|
|
266
|
+
if (token_usage) {
|
|
267
|
+
body.token_usage = token_usage;
|
|
268
|
+
}
|
|
236
269
|
|
|
237
270
|
const result = await apiCall(
|
|
238
271
|
"POST",
|
|
@@ -308,6 +341,58 @@ async function verifyAllInEpic({ epic_id, review_type, summary }) {
|
|
|
308
341
|
return toContent(result);
|
|
309
342
|
}
|
|
310
343
|
|
|
344
|
+
// --- Token Efficiency Tools ---
|
|
345
|
+
|
|
346
|
+
async function reportTokenUsage({ story_id, input_tokens, output_tokens, model_name, cost_millicents, phase, skill_version_id, session_id }) {
|
|
347
|
+
const body = { story_id, input_tokens, output_tokens, model_name, cost_millicents };
|
|
348
|
+
if (phase) body.phase = phase;
|
|
349
|
+
if (skill_version_id) body.skill_version_id = skill_version_id;
|
|
350
|
+
if (session_id) body.session_id = session_id;
|
|
351
|
+
|
|
352
|
+
const result = await apiCall(
|
|
353
|
+
"POST",
|
|
354
|
+
"/api/v1/token-usage",
|
|
355
|
+
body,
|
|
356
|
+
process.env.LOOPCTL_AGENT_KEY
|
|
357
|
+
);
|
|
358
|
+
return toContent(result);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
async function getCostSummary({ project_id, breakdown }) {
|
|
362
|
+
const params = new URLSearchParams({ project_id });
|
|
363
|
+
if (breakdown) params.set("breakdown", breakdown);
|
|
364
|
+
|
|
365
|
+
const result = await apiCall("GET", `/api/v1/projects/${project_id}/cost-summary?${params}`);
|
|
366
|
+
return toContent(result);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
async function getStoryTokenUsage({ story_id }) {
|
|
370
|
+
const result = await apiCall("GET", `/api/v1/stories/${story_id}/token-usage`);
|
|
371
|
+
return toContent(result);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
async function getCostAnomalies({ project_id }) {
|
|
375
|
+
const params = new URLSearchParams();
|
|
376
|
+
if (project_id) params.set("project_id", project_id);
|
|
377
|
+
|
|
378
|
+
const query = params.toString() ? `?${params}` : "";
|
|
379
|
+
const result = await apiCall("GET", `/api/v1/cost-anomalies${query}`);
|
|
380
|
+
return toContent(result);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
async function setTokenBudget({ scope_type, scope_id, budget_millicents, alert_threshold_pct }) {
|
|
384
|
+
const body = { scope_type, scope_id, budget_millicents };
|
|
385
|
+
if (alert_threshold_pct != null) body.alert_threshold_pct = alert_threshold_pct;
|
|
386
|
+
|
|
387
|
+
const result = await apiCall(
|
|
388
|
+
"POST",
|
|
389
|
+
"/api/v1/token-budgets",
|
|
390
|
+
body,
|
|
391
|
+
process.env.LOOPCTL_ORCH_KEY
|
|
392
|
+
);
|
|
393
|
+
return toContent(result);
|
|
394
|
+
}
|
|
395
|
+
|
|
311
396
|
// --- Discovery Tools ---
|
|
312
397
|
|
|
313
398
|
async function listRoutes() {
|
|
@@ -356,7 +441,7 @@ const TOOLS = [
|
|
|
356
441
|
},
|
|
357
442
|
{
|
|
358
443
|
name: "get_progress",
|
|
359
|
-
description: "Get progress summary for a project, including story counts by status.",
|
|
444
|
+
description: "Get progress summary for a project, including story counts by status. Pass include_cost=true to include cost data when available.",
|
|
360
445
|
inputSchema: {
|
|
361
446
|
type: "object",
|
|
362
447
|
properties: {
|
|
@@ -364,6 +449,10 @@ const TOOLS = [
|
|
|
364
449
|
type: "string",
|
|
365
450
|
description: "The UUID of the project.",
|
|
366
451
|
},
|
|
452
|
+
include_cost: {
|
|
453
|
+
type: "boolean",
|
|
454
|
+
description: "Optional: include cost/token summary data in the response.",
|
|
455
|
+
},
|
|
367
456
|
},
|
|
368
457
|
required: ["project_id"],
|
|
369
458
|
},
|
|
@@ -391,7 +480,9 @@ const TOOLS = [
|
|
|
391
480
|
{
|
|
392
481
|
name: "list_stories",
|
|
393
482
|
description:
|
|
394
|
-
"List stories for a project, optionally filtered by agent_status, verified_status, or epic_id."
|
|
483
|
+
"List stories for a project, optionally filtered by agent_status, verified_status, or epic_id. " +
|
|
484
|
+
"Returns compact results (no acceptance_criteria/description) — use get_story for full details. " +
|
|
485
|
+
"Defaults to 20 stories per page; use limit/offset to paginate.",
|
|
395
486
|
inputSchema: {
|
|
396
487
|
type: "object",
|
|
397
488
|
properties: {
|
|
@@ -420,6 +511,10 @@ const TOOLS = [
|
|
|
420
511
|
type: "integer",
|
|
421
512
|
description: "Number of stories to skip (for pagination).",
|
|
422
513
|
},
|
|
514
|
+
include_token_totals: {
|
|
515
|
+
type: "boolean",
|
|
516
|
+
description: "Optional: include per-story token usage totals when available.",
|
|
517
|
+
},
|
|
423
518
|
},
|
|
424
519
|
required: ["project_id"],
|
|
425
520
|
},
|
|
@@ -427,7 +522,8 @@ const TOOLS = [
|
|
|
427
522
|
{
|
|
428
523
|
name: "list_ready_stories",
|
|
429
524
|
description:
|
|
430
|
-
"List stories that are ready to be worked on (contracted, dependencies met).
|
|
525
|
+
"List stories that are ready to be worked on (contracted, dependencies met). " +
|
|
526
|
+
"Returns compact results — use get_story for full details. Defaults to 20 per page.",
|
|
431
527
|
inputSchema: {
|
|
432
528
|
type: "object",
|
|
433
529
|
properties: {
|
|
@@ -554,6 +650,16 @@ const TOOLS = [
|
|
|
554
650
|
type: "string",
|
|
555
651
|
description: "Optional: path or URL of the artifact.",
|
|
556
652
|
},
|
|
653
|
+
token_usage: {
|
|
654
|
+
type: "object",
|
|
655
|
+
description: "Optional: token usage summary for the implementation work.",
|
|
656
|
+
properties: {
|
|
657
|
+
input_tokens: { type: "integer", description: "Total input tokens consumed." },
|
|
658
|
+
output_tokens: { type: "integer", description: "Total output tokens consumed." },
|
|
659
|
+
model_name: { type: "string", description: "Model name (e.g. claude-sonnet-4-5)." },
|
|
660
|
+
cost_millicents: { type: "integer", description: "Total cost in millicents (1/1000 of a cent)." },
|
|
661
|
+
},
|
|
662
|
+
},
|
|
557
663
|
},
|
|
558
664
|
required: ["story_id"],
|
|
559
665
|
},
|
|
@@ -703,6 +809,134 @@ const TOOLS = [
|
|
|
703
809
|
},
|
|
704
810
|
},
|
|
705
811
|
|
|
812
|
+
// Token Efficiency Tools
|
|
813
|
+
{
|
|
814
|
+
name: "report_token_usage",
|
|
815
|
+
description:
|
|
816
|
+
"Report token usage for a story implementation session. " +
|
|
817
|
+
"Stores input/output token counts, model name, and cost. Uses the AGENT key.",
|
|
818
|
+
inputSchema: {
|
|
819
|
+
type: "object",
|
|
820
|
+
properties: {
|
|
821
|
+
story_id: {
|
|
822
|
+
type: "string",
|
|
823
|
+
description: "The UUID of the story this usage is attributed to.",
|
|
824
|
+
},
|
|
825
|
+
input_tokens: {
|
|
826
|
+
type: "integer",
|
|
827
|
+
description: "Number of input (prompt) tokens consumed.",
|
|
828
|
+
},
|
|
829
|
+
output_tokens: {
|
|
830
|
+
type: "integer",
|
|
831
|
+
description: "Number of output (completion) tokens consumed.",
|
|
832
|
+
},
|
|
833
|
+
model_name: {
|
|
834
|
+
type: "string",
|
|
835
|
+
description: "Name of the model used (e.g. claude-sonnet-4-5, gpt-4o).",
|
|
836
|
+
},
|
|
837
|
+
cost_millicents: {
|
|
838
|
+
type: "integer",
|
|
839
|
+
description: "Total cost in millicents (1/1000 of a cent).",
|
|
840
|
+
},
|
|
841
|
+
phase: {
|
|
842
|
+
type: "string",
|
|
843
|
+
description: "Optional: phase of work (e.g. implement, review, verify).",
|
|
844
|
+
},
|
|
845
|
+
skill_version_id: {
|
|
846
|
+
type: "string",
|
|
847
|
+
description: "Optional: UUID of the skill version used.",
|
|
848
|
+
},
|
|
849
|
+
session_id: {
|
|
850
|
+
type: "string",
|
|
851
|
+
description: "Optional: agent session identifier for grouping records.",
|
|
852
|
+
},
|
|
853
|
+
},
|
|
854
|
+
required: ["story_id", "input_tokens", "output_tokens", "model_name", "cost_millicents"],
|
|
855
|
+
},
|
|
856
|
+
},
|
|
857
|
+
{
|
|
858
|
+
name: "get_cost_summary",
|
|
859
|
+
description:
|
|
860
|
+
"Get cost/token usage summary for a project. " +
|
|
861
|
+
"Optionally break down by agent, epic, or model.",
|
|
862
|
+
inputSchema: {
|
|
863
|
+
type: "object",
|
|
864
|
+
properties: {
|
|
865
|
+
project_id: {
|
|
866
|
+
type: "string",
|
|
867
|
+
description: "The UUID of the project.",
|
|
868
|
+
},
|
|
869
|
+
breakdown: {
|
|
870
|
+
type: "string",
|
|
871
|
+
enum: ["agent", "epic", "model"],
|
|
872
|
+
description: "Optional: dimension to group the summary by (agent, epic, or model).",
|
|
873
|
+
},
|
|
874
|
+
},
|
|
875
|
+
required: ["project_id"],
|
|
876
|
+
},
|
|
877
|
+
},
|
|
878
|
+
{
|
|
879
|
+
name: "get_story_token_usage",
|
|
880
|
+
description: "Get token usage records for a single story.",
|
|
881
|
+
inputSchema: {
|
|
882
|
+
type: "object",
|
|
883
|
+
properties: {
|
|
884
|
+
story_id: {
|
|
885
|
+
type: "string",
|
|
886
|
+
description: "The UUID of the story.",
|
|
887
|
+
},
|
|
888
|
+
},
|
|
889
|
+
required: ["story_id"],
|
|
890
|
+
},
|
|
891
|
+
},
|
|
892
|
+
{
|
|
893
|
+
name: "get_cost_anomalies",
|
|
894
|
+
description:
|
|
895
|
+
"Get cost anomaly alerts — stories or agents that exceed expected token budgets. " +
|
|
896
|
+
"Optionally filter by project.",
|
|
897
|
+
inputSchema: {
|
|
898
|
+
type: "object",
|
|
899
|
+
properties: {
|
|
900
|
+
project_id: {
|
|
901
|
+
type: "string",
|
|
902
|
+
description: "Optional: filter anomalies to a specific project UUID.",
|
|
903
|
+
},
|
|
904
|
+
},
|
|
905
|
+
required: [],
|
|
906
|
+
},
|
|
907
|
+
},
|
|
908
|
+
{
|
|
909
|
+
name: "set_token_budget",
|
|
910
|
+
description:
|
|
911
|
+
"Set a token budget for a scope (project, epic, story, or agent). " +
|
|
912
|
+
"Requires orchestrator or user role. Uses the ORCH key.",
|
|
913
|
+
inputSchema: {
|
|
914
|
+
type: "object",
|
|
915
|
+
properties: {
|
|
916
|
+
scope_type: {
|
|
917
|
+
type: "string",
|
|
918
|
+
enum: ["project", "epic", "story", "agent"],
|
|
919
|
+
description: "The type of scope to apply the budget to.",
|
|
920
|
+
},
|
|
921
|
+
scope_id: {
|
|
922
|
+
type: "string",
|
|
923
|
+
description: "The UUID of the scoped resource (project_id, epic_id, story_id, or agent_id).",
|
|
924
|
+
},
|
|
925
|
+
budget_millicents: {
|
|
926
|
+
type: "integer",
|
|
927
|
+
description: "Maximum allowed cost in millicents (1/1000 of a cent).",
|
|
928
|
+
},
|
|
929
|
+
alert_threshold_pct: {
|
|
930
|
+
type: "number",
|
|
931
|
+
description: "Optional: percentage of budget at which to trigger an alert (0–100).",
|
|
932
|
+
minimum: 0,
|
|
933
|
+
maximum: 100,
|
|
934
|
+
},
|
|
935
|
+
},
|
|
936
|
+
required: ["scope_type", "scope_id", "budget_millicents"],
|
|
937
|
+
},
|
|
938
|
+
},
|
|
939
|
+
|
|
706
940
|
// Discovery Tools
|
|
707
941
|
{
|
|
708
942
|
name: "list_routes",
|
|
@@ -722,7 +956,7 @@ const TOOLS = [
|
|
|
722
956
|
const server = new Server(
|
|
723
957
|
{
|
|
724
958
|
name: "loopctl",
|
|
725
|
-
version: "1.
|
|
959
|
+
version: "1.1.0",
|
|
726
960
|
},
|
|
727
961
|
{
|
|
728
962
|
capabilities: { tools: {} },
|
|
@@ -797,6 +1031,22 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
797
1031
|
case "verify_all_in_epic":
|
|
798
1032
|
return await verifyAllInEpic(args);
|
|
799
1033
|
|
|
1034
|
+
// Token Efficiency Tools
|
|
1035
|
+
case "report_token_usage":
|
|
1036
|
+
return await reportTokenUsage(args);
|
|
1037
|
+
|
|
1038
|
+
case "get_cost_summary":
|
|
1039
|
+
return await getCostSummary(args);
|
|
1040
|
+
|
|
1041
|
+
case "get_story_token_usage":
|
|
1042
|
+
return await getStoryTokenUsage(args);
|
|
1043
|
+
|
|
1044
|
+
case "get_cost_anomalies":
|
|
1045
|
+
return await getCostAnomalies(args);
|
|
1046
|
+
|
|
1047
|
+
case "set_token_budget":
|
|
1048
|
+
return await setTokenBudget(args);
|
|
1049
|
+
|
|
800
1050
|
// Discovery Tools
|
|
801
1051
|
case "list_routes":
|
|
802
1052
|
return await listRoutes();
|