@aioproductoscom/mcp 0.15.9 → 0.16.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 +57 -28
- package/dist/client.d.ts +16 -0
- package/dist/client.js +30 -198
- package/dist/index.d.ts +2 -0
- package/dist/index.js +19 -269
- package/dist/registry.d.ts +63 -0
- package/dist/registry.js +1074 -0
- package/dist/server.d.ts +20 -0
- package/dist/server.js +128 -0
- package/dist/ui-views.d.ts +15 -0
- package/dist/ui-views.js +71 -0
- package/dist/update-notifier.d.ts +6 -0
- package/dist/update-notifier.js +1 -1
- package/package.json +15 -2
- package/dist/pm-skill.js +0 -40
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
|
+
import { type RestCall } from "./registry.js";
|
|
3
|
+
/** What the server needs from the outside world — injected so tests can stub
|
|
4
|
+
* the platform without a network. */
|
|
5
|
+
export interface ServerDeps {
|
|
6
|
+
version: string;
|
|
7
|
+
/** Execute one REST call against the platform. Never throws for HTTP errors —
|
|
8
|
+
* only for transport failures (which the dispatcher catches). */
|
|
9
|
+
request: (call: RestCall) => Promise<{
|
|
10
|
+
ok: boolean;
|
|
11
|
+
status: number;
|
|
12
|
+
text: string;
|
|
13
|
+
}>;
|
|
14
|
+
/** Optional decorator over every tool result's content — the update banner
|
|
15
|
+
* rides through here (index.ts owns the once-only bookkeeping). */
|
|
16
|
+
decorate?: (content: ContentBlock[]) => ContentBlock[];
|
|
17
|
+
}
|
|
18
|
+
type ContentBlock = Record<string, unknown>;
|
|
19
|
+
export declare function createPmServer(deps: ServerDeps): Server;
|
|
20
|
+
export {};
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// The stdio MCP server, DERIVED from the shared tool registry (registry.ts) —
|
|
2
|
+
// the same single source of truth the hosted /api/mcp connector serves. That
|
|
3
|
+
// is the whole architecture: one registry entry per tool, and both transports
|
|
4
|
+
// (hosted HTTP and this stdio process) read it, so the two surfaces cannot
|
|
5
|
+
// drift in tool set, descriptions, schemas, or annotations. Before this file,
|
|
6
|
+
// parity was 872 hand-written lines kept in sync by discipline alone.
|
|
7
|
+
//
|
|
8
|
+
// tools/list mirrors the hosted dispatcher exactly: title, description, input
|
|
9
|
+
// schema (with worked-example arguments folded into JSON-Schema `examples`),
|
|
10
|
+
// behaviour annotations (readOnlyHint / destructiveHint / …), and the examples
|
|
11
|
+
// array. tools/call executes the registry's transport-agnostic RestCall
|
|
12
|
+
// through the injected platform client, and attaches the tool's rich ui://
|
|
13
|
+
// card when one is defined — text first, always, so hosts without MCP-UI
|
|
14
|
+
// rendering lose nothing.
|
|
15
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
16
|
+
import { CallToolRequestSchema, GetPromptRequestSchema, ListPromptsRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
17
|
+
import { annotationsFor, MCP_TOOLS, MCP_TOOLS_BY_NAME } from "./registry.js";
|
|
18
|
+
const INSTRUCTIONS = "You manage product work on AIOProductOS for the connected member — board, insights, features, and " +
|
|
19
|
+
"analytics all hang off one spine. Call get_pm_playbook first for how to operate: ground in " +
|
|
20
|
+
"get_product_brain, keep work tied to the spine (insight→feature→task→outcome), and prioritize on " +
|
|
21
|
+
"evidence (affected accounts + MRR + reach), never an invented score. Resolve names to ids with " +
|
|
22
|
+
"pm_meta before create_task / update_task; never guess an id. Confirm what you changed in plain " +
|
|
23
|
+
"language, and claim only writes you actually made.";
|
|
24
|
+
/** The slash-command routines. Stdio-only on purpose: prompts surface as host
|
|
25
|
+
* slash commands (e.g. /productos:standup), a concept of the local host; the
|
|
26
|
+
* hosted connector's clients drive routines through their own UX. */
|
|
27
|
+
const PROMPTS = [
|
|
28
|
+
{
|
|
29
|
+
name: "standup",
|
|
30
|
+
description: "Board standup — what moved, what's blocked, what needs you today (read-only).",
|
|
31
|
+
body: "Run the product standup for the connected org. Read-only — don't change anything.\n" +
|
|
32
|
+
"1) pm_meta to resolve lists / statuses / members.\n" +
|
|
33
|
+
"2) list_tasks to read the active board.\n" +
|
|
34
|
+
"Report briefly: what's in progress, what's blocked (and why), what's gone stale with no movement, and the 1–3 " +
|
|
35
|
+
"decisions that need a human today. Pull get_product_brain if revenue/account context sharpens the call. " +
|
|
36
|
+
"End with the single most important thing to do next.",
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: "triage",
|
|
40
|
+
description: "Triage — turn fresh customer signal into prioritized, spine-linked work.",
|
|
41
|
+
body: "Run intake triage for the connected org. Follow get_pm_playbook.\n" +
|
|
42
|
+
"1) get_product_brain — ground in revenue, top accounts, recent verbatim signal, and open work.\n" +
|
|
43
|
+
"2) Review the board (list_tasks, pm_meta) and any fresh insights.\n" +
|
|
44
|
+
"3) Prioritize on EVIDENCE — affected accounts + MRR + reach — never an invented score.\n" +
|
|
45
|
+
"4) Make the writes you're confident in: create_task / update_task, linking insight→feature→task; " +
|
|
46
|
+
"assign owners; place the clear ones in the current sprint. SURFACE the judgment calls for a human instead of guessing.\n" +
|
|
47
|
+
"Confirm only the writes you actually made.",
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: "daily",
|
|
51
|
+
description: "Daily PM briefing — your plate, blockers, and the one thing to do next.",
|
|
52
|
+
body: "Give the connected member their daily briefing.\n" +
|
|
53
|
+
"1) get_product_brain for context.\n" +
|
|
54
|
+
"2) list_tasks + pm_meta — what's assigned to them, what's blocked, what's overdue or stale.\n" +
|
|
55
|
+
"3) Scan the support inbox (list_conversations) and upcoming calls (list_bookings) for anything urgent.\n" +
|
|
56
|
+
"Output a tight briefing: top priorities today, blockers needing a decision, and ONE recommended next action. " +
|
|
57
|
+
"Read-only unless they ask you to act.",
|
|
58
|
+
},
|
|
59
|
+
];
|
|
60
|
+
export function createPmServer(deps) {
|
|
61
|
+
const server = new Server({ name: "AIOProductOS", version: deps.version }, { capabilities: { tools: {}, prompts: {} }, instructions: INSTRUCTIONS });
|
|
62
|
+
const decorate = deps.decorate ?? ((c) => c);
|
|
63
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
64
|
+
tools: MCP_TOOLS.map((t) => {
|
|
65
|
+
// Same derivation as the hosted dispatcher: worked-example arguments fold
|
|
66
|
+
// into JSON-Schema `examples`; no-arg examples ({}) are dropped as noise.
|
|
67
|
+
const argExamples = (t.examples ?? []).map((e) => e.arguments).filter((a) => Object.keys(a).length > 0);
|
|
68
|
+
const inputSchema = (argExamples.length ? { ...t.inputSchema, examples: argExamples } : t.inputSchema);
|
|
69
|
+
return {
|
|
70
|
+
name: t.name,
|
|
71
|
+
...(t.title ? { title: t.title } : {}),
|
|
72
|
+
description: t.description,
|
|
73
|
+
inputSchema,
|
|
74
|
+
annotations: annotationsFor(t),
|
|
75
|
+
};
|
|
76
|
+
}),
|
|
77
|
+
}));
|
|
78
|
+
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
79
|
+
const name = req.params.name;
|
|
80
|
+
const args = req.params.arguments ?? {};
|
|
81
|
+
const tool = MCP_TOOLS_BY_NAME[name];
|
|
82
|
+
if (!tool) {
|
|
83
|
+
return { content: decorate([{ type: "text", text: `Unknown tool: ${name}` }]), isError: true };
|
|
84
|
+
}
|
|
85
|
+
if (tool.kind === "static") {
|
|
86
|
+
return { content: decorate([{ type: "text", text: tool.run(args) }]) };
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
const res = await deps.request(tool.rest(args));
|
|
90
|
+
const content = [
|
|
91
|
+
{ type: "text", text: res.text || (res.ok ? "(no content)" : `HTTP ${res.status}`) },
|
|
92
|
+
];
|
|
93
|
+
// Rich ui:// card when the tool defines one — purely additive, text is
|
|
94
|
+
// always first, and a render failure never breaks the call.
|
|
95
|
+
if (res.ok && tool.htmlView) {
|
|
96
|
+
try {
|
|
97
|
+
const html = tool.htmlView(res.text);
|
|
98
|
+
if (html) {
|
|
99
|
+
content.push({
|
|
100
|
+
type: "resource",
|
|
101
|
+
resource: { uri: `ui://aioproductos/${tool.name}`, mimeType: "text/html", text: html },
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
/* text already carries the data */
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return { content: decorate(content), ...(res.ok ? {} : { isError: true }) };
|
|
110
|
+
}
|
|
111
|
+
catch (e) {
|
|
112
|
+
return { content: decorate([{ type: "text", text: `Tool call failed: ${String(e)}` }]), isError: true };
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
server.setRequestHandler(ListPromptsRequestSchema, async () => ({
|
|
116
|
+
prompts: PROMPTS.map((p) => ({ name: p.name, description: p.description })),
|
|
117
|
+
}));
|
|
118
|
+
server.setRequestHandler(GetPromptRequestSchema, async (req) => {
|
|
119
|
+
const p = PROMPTS.find((x) => x.name === req.params.name);
|
|
120
|
+
if (!p)
|
|
121
|
+
throw new Error(`Unknown prompt: ${req.params.name}`);
|
|
122
|
+
return {
|
|
123
|
+
description: p.description,
|
|
124
|
+
messages: [{ role: "user", content: { type: "text", text: p.body } }],
|
|
125
|
+
};
|
|
126
|
+
});
|
|
127
|
+
return server;
|
|
128
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #65 — MCP Apps `ui://` rich artifacts. A REST tool can attach a self-contained
|
|
3
|
+
* HTML card to its result; the dispatch embeds it as a `ui://` resource AFTER the
|
|
4
|
+
* text block. Hosts that support MCP-UI render the card; every other host shows
|
|
5
|
+
* the text — so this is a progressive enhancement, never a facade (the data is
|
|
6
|
+
* always present). Each view parses the tool's JSON and returns standalone HTML
|
|
7
|
+
* (inline styles, host-neutral tokens) or null if the shape doesn't render.
|
|
8
|
+
*
|
|
9
|
+
* SECURITY: all dynamic values are HTML-escaped — the card is rendered in the
|
|
10
|
+
* host's webview, so even the org's own text must never break out of markup.
|
|
11
|
+
*/
|
|
12
|
+
export declare function escapeHtml(s: unknown): string;
|
|
13
|
+
/** Rich card for get_weekly_signal_memo. Handles both the stored row (inputs_count)
|
|
14
|
+
* and the on-the-fly build (inputsCount). Returns null on any shape mismatch. */
|
|
15
|
+
export declare function weeklyMemoHtml(raw: string): string | null;
|
package/dist/ui-views.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #65 — MCP Apps `ui://` rich artifacts. A REST tool can attach a self-contained
|
|
3
|
+
* HTML card to its result; the dispatch embeds it as a `ui://` resource AFTER the
|
|
4
|
+
* text block. Hosts that support MCP-UI render the card; every other host shows
|
|
5
|
+
* the text — so this is a progressive enhancement, never a facade (the data is
|
|
6
|
+
* always present). Each view parses the tool's JSON and returns standalone HTML
|
|
7
|
+
* (inline styles, host-neutral tokens) or null if the shape doesn't render.
|
|
8
|
+
*
|
|
9
|
+
* SECURITY: all dynamic values are HTML-escaped — the card is rendered in the
|
|
10
|
+
* host's webview, so even the org's own text must never break out of markup.
|
|
11
|
+
*/
|
|
12
|
+
export function escapeHtml(s) {
|
|
13
|
+
return String(s ?? "")
|
|
14
|
+
.replace(/&/g, "&")
|
|
15
|
+
.replace(/</g, "<")
|
|
16
|
+
.replace(/>/g, ">")
|
|
17
|
+
.replace(/"/g, """)
|
|
18
|
+
.replace(/'/g, "'");
|
|
19
|
+
}
|
|
20
|
+
/** Wrap card body in a self-contained, host-neutral shell (works light or dark). */
|
|
21
|
+
function shell(title, body) {
|
|
22
|
+
return `<!doctype html><meta charset="utf-8"><div style="font:14px/1.5 ui-sans-serif,system-ui,sans-serif;color:#1a1a1a;background:#fff;border:1px solid #e5e5e5;border-radius:12px;padding:18px;max-width:560px">
|
|
23
|
+
<div style="font-size:11px;letter-spacing:.04em;text-transform:uppercase;color:#8a8a8a">${escapeHtml(title)}</div>
|
|
24
|
+
${body}
|
|
25
|
+
<div style="margin-top:14px;border-top:1px solid #efefef;padding-top:8px;font-size:11px;color:#aaa">AIOProductOS · every number off real product data</div>
|
|
26
|
+
</div>`;
|
|
27
|
+
}
|
|
28
|
+
function num(v) {
|
|
29
|
+
const n = typeof v === "number" ? v : Number(v);
|
|
30
|
+
return Number.isFinite(n) ? n : 0;
|
|
31
|
+
}
|
|
32
|
+
/** Rich card for get_weekly_signal_memo. Handles both the stored row (inputs_count)
|
|
33
|
+
* and the on-the-fly build (inputsCount). Returns null on any shape mismatch. */
|
|
34
|
+
export function weeklyMemoHtml(raw) {
|
|
35
|
+
let parsed;
|
|
36
|
+
try {
|
|
37
|
+
parsed = JSON.parse(raw);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
const memo = parsed?.memo;
|
|
43
|
+
if (!memo || typeof memo !== "object")
|
|
44
|
+
return null;
|
|
45
|
+
const week = typeof memo.week === "string" ? memo.week : "";
|
|
46
|
+
if (!week)
|
|
47
|
+
return null;
|
|
48
|
+
const inputs = memo.inputsCount ?? memo.inputs_count ?? {};
|
|
49
|
+
const themes = Array.isArray(memo.themes) ? memo.themes : [];
|
|
50
|
+
const stat = (label, value) => `<div><div style="font-size:22px;font-weight:600;font-variant-numeric:tabular-nums">${value}</div><div style="font-size:11px;color:#8a8a8a">${escapeHtml(label)}</div></div>`;
|
|
51
|
+
const statsRow = `<div style="display:flex;gap:28px;margin:12px 0 4px">
|
|
52
|
+
${stat("Insights", num(inputs.insights))}
|
|
53
|
+
${stat("Themes", themes.length)}
|
|
54
|
+
${stat("Shipped", num(inputs.releases))}
|
|
55
|
+
</div>`;
|
|
56
|
+
const themeRows = themes
|
|
57
|
+
.slice(0, 6)
|
|
58
|
+
.map((t) => {
|
|
59
|
+
const tt = t;
|
|
60
|
+
const count = num(tt.evidenceCount);
|
|
61
|
+
return `<li style="display:flex;justify-content:space-between;gap:12px;border-top:1px solid #efefef;padding:7px 0">
|
|
62
|
+
<span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${escapeHtml(tt.label)}</span>
|
|
63
|
+
<span style="color:#8a8a8a;font-variant-numeric:tabular-nums;flex:none">${count} signal${count === 1 ? "" : "s"}</span>
|
|
64
|
+
</li>`;
|
|
65
|
+
})
|
|
66
|
+
.join("");
|
|
67
|
+
const themesBlock = themes.length
|
|
68
|
+
? `<ul style="list-style:none;margin:8px 0 0;padding:0">${themeRows}</ul>`
|
|
69
|
+
: `<p style="color:#8a8a8a;margin:8px 0 0">No themes surfaced this week — not enough recent signal to cluster.</p>`;
|
|
70
|
+
return shell(`Weekly signal · ${week}`, statsRow + themesBlock);
|
|
71
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** true when `latest` is a higher release than `current` (patch/minor/major; pre-release tags ignored). */
|
|
2
|
+
export declare function isNewer(latest: string, current: string): boolean;
|
|
3
|
+
/** Latest published version if it's newer than `current`, else null. Never throws. */
|
|
4
|
+
export declare function checkForUpdate(current: string): Promise<string | null>;
|
|
5
|
+
/** The one-line banner appended to a tool result when an update is available. */
|
|
6
|
+
export declare function updateBanner(latest: string, current: string): string;
|
package/dist/update-notifier.js
CHANGED
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
* and it's disabled under CI.
|
|
10
10
|
*/
|
|
11
11
|
import { readFileSync, writeFileSync } from "node:fs";
|
|
12
|
-
import { join } from "node:path";
|
|
13
12
|
import { tmpdir } from "node:os";
|
|
13
|
+
import { join } from "node:path";
|
|
14
14
|
const PKG = "@aioproductoscom/mcp";
|
|
15
15
|
const CACHE = join(tmpdir(), "aioproductos-mcp-update.json");
|
|
16
16
|
const TTL_MS = 24 * 60 * 60 * 1000; // check npm at most once a day
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aioproductoscom/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"mcpName": "com.aioproductos/mcp",
|
|
5
5
|
"description": "AIOProductOS MCP — manage the board, read your product brain + analytics, and run the support inbox from Claude Code, Cursor, Codex, and any MCP host.",
|
|
6
6
|
"type": "module",
|
|
@@ -27,6 +27,17 @@
|
|
|
27
27
|
"bin": {
|
|
28
28
|
"productos": "dist/index.js"
|
|
29
29
|
},
|
|
30
|
+
"exports": {
|
|
31
|
+
".": "./dist/index.js",
|
|
32
|
+
"./registry": {
|
|
33
|
+
"types": "./dist/registry.d.ts",
|
|
34
|
+
"default": "./dist/registry.js"
|
|
35
|
+
},
|
|
36
|
+
"./server": {
|
|
37
|
+
"types": "./dist/server.d.ts",
|
|
38
|
+
"default": "./dist/server.js"
|
|
39
|
+
}
|
|
40
|
+
},
|
|
30
41
|
"files": [
|
|
31
42
|
"dist",
|
|
32
43
|
"README.md"
|
|
@@ -40,7 +51,9 @@
|
|
|
40
51
|
"scripts": {
|
|
41
52
|
"build": "tsc",
|
|
42
53
|
"prepublishOnly": "npm run build",
|
|
43
|
-
"start": "node dist/index.js"
|
|
54
|
+
"start": "node dist/index.js",
|
|
55
|
+
"test": "npm run build && node --test 'test/**/*.test.mjs'",
|
|
56
|
+
"typecheck": "tsc --noEmit"
|
|
44
57
|
},
|
|
45
58
|
"dependencies": {
|
|
46
59
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
package/dist/pm-skill.js
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
// The PM operating playbook — OUR product logic, handed to the customer's PM-side
|
|
2
|
-
// agent via get_pm_playbook() so it works like a great PM grounded in AIOProductOS,
|
|
3
|
-
// not a generic ticket bot. The server's `instructions` points at it; the agent
|
|
4
|
-
// pulls the full doctrine on demand (keeps it out of every session's context).
|
|
5
|
-
//
|
|
6
|
-
// Bump PM_SKILL_VERSION on any behavioural change so a run traces to a playbook.
|
|
7
|
-
export const PM_SKILL_VERSION = "0.1.0";
|
|
8
|
-
export const PM_PLAYBOOK = `# AIOProductOS · Product Manager
|
|
9
|
-
|
|
10
|
-
You operate the product on AIOProductOS for the connected member. The board, insights, features, and
|
|
11
|
-
analytics all hang off one spine — your job is to keep work tied to the real why and to decide from evidence.
|
|
12
|
-
|
|
13
|
-
## Ground yourself first
|
|
14
|
-
Call \`get_product_brain\` before you reason about priorities: a live, read-only snapshot of THIS product —
|
|
15
|
-
revenue and top accounts, web + product analytics, features, recent verbatim customer signals, and the open
|
|
16
|
-
work. Decide from it, not from memory. Use \`pm_meta\` to resolve names → ids before any write; never guess an id.
|
|
17
|
-
|
|
18
|
-
## The spine logic — keep everything connected
|
|
19
|
-
Customer signal (insight) → the bet (feature) → the work (task) → the result (outcome). Don't create orphans:
|
|
20
|
-
- A task that exists because a customer asked → link its \`insight_id\`. Because it serves a bet → link its \`feature_id\`.
|
|
21
|
-
- When you capture feedback, tie it to the account and the feature it's about, in the customer's own words —
|
|
22
|
-
a specific verbatim beats your paraphrase.
|
|
23
|
-
- Work with no why is the thing to question, not to dutifully schedule.
|
|
24
|
-
|
|
25
|
-
## Prioritize on evidence — never on a score you invented
|
|
26
|
-
This org has its OWN prioritization framework (RICE, ICE, MoSCoW, WSJF, value/effort, Kano — whatever they set).
|
|
27
|
-
Do NOT assume one, and do NOT compute the score yourself. Attach the EVIDENCE and let their framework do the math:
|
|
28
|
-
- Which accounts are affected, and the MRR behind them (the revenue weight).
|
|
29
|
-
- The reach/usage from analytics (\`analyze_funnel\`, \`get_retention\`, \`analyze_paths\`, the brain).
|
|
30
|
-
- The customer signals that back it.
|
|
31
|
-
Hand work off as an opportunity with that evidence attached — a human (or their configured model) does the ranking.
|
|
32
|
-
You ground the call; you never fake the number.
|
|
33
|
-
|
|
34
|
-
## Decide from data, not vibes
|
|
35
|
-
Before you assert a priority or a problem, pull the number — funnel drop, retention curve, the path users take,
|
|
36
|
-
the MRR behind a segment — and quote it. "Signups drop 38% on the company-size field" beats "the form feels long".
|
|
37
|
-
|
|
38
|
-
## Output discipline
|
|
39
|
-
Concise and honest. Confirm only the writes you actually made, in plain language. Resolve ids via \`pm_meta\`.
|
|
40
|
-
Keep titles and descriptions concrete and scoped — a task a teammate can pick up cold.`;
|