@jcyamacho/agent-memory 0.0.17 → 0.0.19
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 +47 -68
- package/dist/index.js +67 -41
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,16 +3,16 @@
|
|
|
3
3
|
Persistent memory for MCP-powered coding agents.
|
|
4
4
|
|
|
5
5
|
`agent-memory` is a stdio MCP server that gives your LLM durable memory backed
|
|
6
|
-
by SQLite. It
|
|
6
|
+
by SQLite. It helps your agent remember preferences, project context, and prior
|
|
7
|
+
decisions across sessions.
|
|
8
|
+
|
|
9
|
+
It exposes four tools:
|
|
7
10
|
|
|
8
11
|
- `remember` -> save facts, decisions, preferences, and project context
|
|
9
12
|
- `recall` -> retrieve the most relevant memories later
|
|
10
13
|
- `revise` -> update an existing memory when it becomes outdated
|
|
11
14
|
- `forget` -> delete a memory that is no longer relevant
|
|
12
15
|
|
|
13
|
-
Use it when your agent should remember preferences, project facts, and prior
|
|
14
|
-
decisions across sessions.
|
|
15
|
-
|
|
16
16
|
## Quick Start
|
|
17
17
|
|
|
18
18
|
Claude CLI:
|
|
@@ -27,67 +27,72 @@ Codex CLI:
|
|
|
27
27
|
codex mcp add memory -- npx -y @jcyamacho/agent-memory
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
OpenCode:
|
|
31
31
|
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
32
|
+
```jsonc
|
|
33
|
+
{
|
|
34
|
+
"$schema": "https://opencode.ai/config.json",
|
|
35
|
+
"mcp": {
|
|
36
|
+
"memory": {
|
|
37
|
+
"type": "local",
|
|
38
|
+
"command": ["npx", "-y", "@jcyamacho/agent-memory"]
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
41
42
|
```
|
|
42
43
|
|
|
43
|
-
##
|
|
44
|
+
## Optional LLM Instructions
|
|
44
45
|
|
|
45
|
-
|
|
46
|
+
Optional LLM instructions to reinforce the MCP's built-in guidance:
|
|
46
47
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
-
|
|
48
|
+
```md
|
|
49
|
+
## Agent Memory
|
|
50
|
+
|
|
51
|
+
- Use `memory_recall` at conversation start and before design choices,
|
|
52
|
+
conventions, edge cases, or saving memory.
|
|
53
|
+
- Query `memory_recall` with 2-5 short anchor-heavy terms or exact phrases,
|
|
54
|
+
not full questions or sentences.
|
|
55
|
+
- Pass `workspace` for project-scoped memory. Omit it only for facts that
|
|
56
|
+
apply across projects.
|
|
57
|
+
- Use `memory_remember` to save one durable fact when the user states a stable
|
|
58
|
+
preference, correction, or reusable project decision.
|
|
59
|
+
- If the fact already exists, use `memory_revise` instead of creating a duplicate.
|
|
60
|
+
- Use `memory_forget` to remove a wrong or obsolete memory.
|
|
61
|
+
- Do not store secrets or temporary task state in memory.
|
|
62
|
+
```
|
|
51
63
|
|
|
52
64
|
## Web UI
|
|
53
65
|
|
|
54
66
|
Browse, edit, and delete memories in a local web interface:
|
|
55
67
|
|
|
56
68
|
```bash
|
|
57
|
-
npx -y @jcyamacho/agent-memory --ui
|
|
69
|
+
npx -y @jcyamacho/agent-memory@latest --ui
|
|
58
70
|
```
|
|
59
71
|
|
|
60
72
|
Opens at `http://localhost:6580`. Use `--port` to change:
|
|
61
73
|
|
|
62
74
|
```bash
|
|
63
|
-
npx -y @jcyamacho/agent-memory --ui --port 9090
|
|
75
|
+
npx -y @jcyamacho/agent-memory@latest --ui --port 9090
|
|
64
76
|
```
|
|
65
77
|
|
|
66
78
|
The web UI uses the same database as the MCP server.
|
|
67
79
|
|
|
68
|
-
##
|
|
69
|
-
|
|
70
|
-
- `remember` saves durable facts, preferences, decisions, and project context.
|
|
71
|
-
- `recall` retrieves the most relevant saved memories.
|
|
72
|
-
- `revise` updates an existing memory when it becomes outdated.
|
|
73
|
-
- `forget` deletes a memory that is no longer relevant.
|
|
74
|
-
|
|
75
|
-
## How Ranking Works
|
|
80
|
+
## How Recall Finds Memories
|
|
76
81
|
|
|
77
82
|
`recall` uses a multi-signal ranking system to surface the most relevant
|
|
78
83
|
memories:
|
|
79
84
|
|
|
80
85
|
1. **Text relevance** is the primary signal -- memories whose content best
|
|
81
86
|
matches your search terms rank highest.
|
|
82
|
-
2. **
|
|
83
|
-
|
|
87
|
+
2. **Workspace match** is the next strongest signal. When you pass
|
|
88
|
+
`workspace`, exact matches rank highest and all other scoped workspaces rank
|
|
89
|
+
below exact matches.
|
|
90
|
+
3. **Embedding similarity** is a secondary signal. Recall builds an embedding
|
|
91
|
+
from your normalized search terms and boosts memories whose stored
|
|
84
92
|
embeddings are most semantically similar.
|
|
85
|
-
3. **Workspace match** is a strong secondary signal. When you pass
|
|
86
|
-
`workspace`, exact matches rank highest, sibling repositories get a small
|
|
87
|
-
boost, and unrelated workspaces rank lowest.
|
|
88
93
|
4. **Global memories** (saved without a workspace) are treated as relevant
|
|
89
94
|
everywhere. When you pass `workspace`, they rank below exact workspace
|
|
90
|
-
matches and above
|
|
95
|
+
matches and above memories from other workspaces.
|
|
91
96
|
5. **Recency** is a minor tiebreaker -- newer memories rank slightly above older
|
|
92
97
|
ones when other signals are equal.
|
|
93
98
|
|
|
@@ -98,8 +103,12 @@ memories without a workspace only when they apply across all projects.
|
|
|
98
103
|
When you save a memory from a git worktree, `agent-memory` stores the main repo
|
|
99
104
|
root as the workspace. `recall` applies the same normalization to incoming
|
|
100
105
|
workspace queries so linked worktrees still match repo-scoped memories exactly.
|
|
106
|
+
When that happens, recall returns the queried workspace value so callers can
|
|
107
|
+
treat the match as belonging to their current worktree context.
|
|
108
|
+
|
|
109
|
+
## Configuration
|
|
101
110
|
|
|
102
|
-
|
|
111
|
+
### Database Location
|
|
103
112
|
|
|
104
113
|
By default, the SQLite database is created at:
|
|
105
114
|
|
|
@@ -119,7 +128,7 @@ Set `AGENT_MEMORY_DB_PATH` when you want to:
|
|
|
119
128
|
- share a memory DB across multiple clients
|
|
120
129
|
- store the DB somewhere easier to back up or inspect
|
|
121
130
|
|
|
122
|
-
|
|
131
|
+
### Model Cache Location
|
|
123
132
|
|
|
124
133
|
By default, downloaded embedding model files are cached at:
|
|
125
134
|
|
|
@@ -142,36 +151,6 @@ Set `AGENT_MEMORY_MODELS_CACHE_PATH` when you want to:
|
|
|
142
151
|
Schema changes are migrated automatically, including workspace normalization for
|
|
143
152
|
existing git worktree memories when the original path can still be resolved.
|
|
144
153
|
|
|
145
|
-
## Development
|
|
146
|
-
|
|
147
|
-
For working on the project itself or running from source. Requires Bun and
|
|
148
|
-
Node.js.
|
|
149
|
-
|
|
150
|
-
```bash
|
|
151
|
-
bun install
|
|
152
|
-
bun run build
|
|
153
|
-
```
|
|
154
|
-
|
|
155
|
-
To use a local build as your MCP server:
|
|
156
|
-
|
|
157
|
-
```json
|
|
158
|
-
{
|
|
159
|
-
"mcpServers": {
|
|
160
|
-
"memory": {
|
|
161
|
-
"command": "node",
|
|
162
|
-
"args": [
|
|
163
|
-
"/absolute/path/to/agent-memory/dist/index.js"
|
|
164
|
-
]
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
```
|
|
169
|
-
|
|
170
|
-
```bash
|
|
171
|
-
bun lint
|
|
172
|
-
bun test
|
|
173
|
-
```
|
|
174
|
-
|
|
175
154
|
## License
|
|
176
155
|
|
|
177
156
|
MIT
|
package/dist/index.js
CHANGED
|
@@ -12464,7 +12464,7 @@ class StdioServerTransport {
|
|
|
12464
12464
|
}
|
|
12465
12465
|
}
|
|
12466
12466
|
// package.json
|
|
12467
|
-
var version2 = "0.0.
|
|
12467
|
+
var version2 = "0.0.19";
|
|
12468
12468
|
|
|
12469
12469
|
// src/config.ts
|
|
12470
12470
|
import { homedir } from "node:os";
|
|
@@ -20058,11 +20058,17 @@ function parseOptionalDate(value, fieldName) {
|
|
|
20058
20058
|
|
|
20059
20059
|
// src/mcp/tools/forget.ts
|
|
20060
20060
|
var forgetInputSchema = {
|
|
20061
|
-
id: string2().describe("
|
|
20061
|
+
id: string2().describe("Memory id to delete. Use an id returned by `recall`.")
|
|
20062
20062
|
};
|
|
20063
20063
|
function registerForgetTool(server, memory) {
|
|
20064
20064
|
server.registerTool("forget", {
|
|
20065
|
-
|
|
20065
|
+
annotations: {
|
|
20066
|
+
title: "Forget",
|
|
20067
|
+
destructiveHint: true,
|
|
20068
|
+
idempotentHint: true,
|
|
20069
|
+
openWorldHint: false
|
|
20070
|
+
},
|
|
20071
|
+
description: 'Delete a memory that is wrong or obsolete. Use after `recall` when you have the memory id. Use `revise` instead if the fact should remain with corrected wording. Returns `<memory id="..." deleted="true" />`.',
|
|
20066
20072
|
inputSchema: forgetInputSchema
|
|
20067
20073
|
}, async ({ id }) => {
|
|
20068
20074
|
try {
|
|
@@ -20080,15 +20086,14 @@ function registerForgetTool(server, memory) {
|
|
|
20080
20086
|
var toNormalizedScore = (value) => value;
|
|
20081
20087
|
|
|
20082
20088
|
// src/ranking.ts
|
|
20083
|
-
var RETRIEVAL_SCORE_WEIGHT =
|
|
20084
|
-
var EMBEDDING_SIMILARITY_WEIGHT =
|
|
20085
|
-
var WORKSPACE_MATCH_WEIGHT =
|
|
20086
|
-
var RECENCY_WEIGHT =
|
|
20089
|
+
var RETRIEVAL_SCORE_WEIGHT = 9;
|
|
20090
|
+
var EMBEDDING_SIMILARITY_WEIGHT = 4;
|
|
20091
|
+
var WORKSPACE_MATCH_WEIGHT = 5;
|
|
20092
|
+
var RECENCY_WEIGHT = 1;
|
|
20087
20093
|
var MAX_COMPOSITE_SCORE = RETRIEVAL_SCORE_WEIGHT + EMBEDDING_SIMILARITY_WEIGHT + WORKSPACE_MATCH_WEIGHT + RECENCY_WEIGHT;
|
|
20088
20094
|
var GLOBAL_WORKSPACE_SCORE = 0.5;
|
|
20089
|
-
var SIBLING_WORKSPACE_SCORE = 0.25;
|
|
20090
20095
|
function rerankSearchResults(results, workspace, queryEmbedding) {
|
|
20091
|
-
if (results.length
|
|
20096
|
+
if (results.length === 0) {
|
|
20092
20097
|
return results;
|
|
20093
20098
|
}
|
|
20094
20099
|
const normalizedQueryWs = workspace ? normalizeWorkspacePath(workspace) : undefined;
|
|
@@ -20126,14 +20131,7 @@ function computeWorkspaceScore(memoryWs, queryWs) {
|
|
|
20126
20131
|
if (normalizedMemoryWs === queryWs) {
|
|
20127
20132
|
return 1;
|
|
20128
20133
|
}
|
|
20129
|
-
|
|
20130
|
-
const memoryLastSlashIndex = normalizedMemoryWs.lastIndexOf("/");
|
|
20131
|
-
if (queryLastSlashIndex <= 0 || memoryLastSlashIndex <= 0) {
|
|
20132
|
-
return 0;
|
|
20133
|
-
}
|
|
20134
|
-
const queryParent = queryWs.slice(0, queryLastSlashIndex);
|
|
20135
|
-
const memoryParent = normalizedMemoryWs.slice(0, memoryLastSlashIndex);
|
|
20136
|
-
return memoryParent === queryParent ? SIBLING_WORKSPACE_SCORE : 0;
|
|
20134
|
+
return 0;
|
|
20137
20135
|
}
|
|
20138
20136
|
|
|
20139
20137
|
// src/memory-service.ts
|
|
@@ -20205,6 +20203,7 @@ class MemoryService {
|
|
|
20205
20203
|
throw new ValidationError("At least one search term is required.");
|
|
20206
20204
|
}
|
|
20207
20205
|
const requestedLimit = normalizeLimit(input.limit);
|
|
20206
|
+
const queryWorkspace = normalizeOptionalString(input.workspace);
|
|
20208
20207
|
const workspace = await this.workspaceResolver.resolve(input.workspace);
|
|
20209
20208
|
const normalizedQuery = {
|
|
20210
20209
|
terms,
|
|
@@ -20216,7 +20215,7 @@ class MemoryService {
|
|
|
20216
20215
|
this.repository.search(normalizedQuery),
|
|
20217
20216
|
this.embeddingService.createVector(terms.join(" "))
|
|
20218
20217
|
]);
|
|
20219
|
-
return rerankSearchResults(results, workspace, queryEmbedding).slice(0, requestedLimit).map(toPublicSearchResult);
|
|
20218
|
+
return rerankSearchResults(results, workspace, queryEmbedding).slice(0, requestedLimit).map((result) => toPublicSearchResult(remapSearchResultWorkspace(result, workspace, queryWorkspace)));
|
|
20220
20219
|
}
|
|
20221
20220
|
}
|
|
20222
20221
|
function toPublicMemoryRecord(memory) {
|
|
@@ -20266,14 +20265,27 @@ function normalizeTerms(terms) {
|
|
|
20266
20265
|
const normalizedTerms = terms.map((term) => term.trim()).filter(Boolean);
|
|
20267
20266
|
return [...new Set(normalizedTerms)];
|
|
20268
20267
|
}
|
|
20268
|
+
function normalizeOptionalString(value) {
|
|
20269
|
+
const trimmed = value?.trim();
|
|
20270
|
+
return trimmed ? trimmed : undefined;
|
|
20271
|
+
}
|
|
20272
|
+
function remapSearchResultWorkspace(result, canonicalWorkspace, queryWorkspace) {
|
|
20273
|
+
if (!queryWorkspace || !canonicalWorkspace || result.workspace !== canonicalWorkspace) {
|
|
20274
|
+
return result;
|
|
20275
|
+
}
|
|
20276
|
+
return {
|
|
20277
|
+
...result,
|
|
20278
|
+
workspace: queryWorkspace
|
|
20279
|
+
};
|
|
20280
|
+
}
|
|
20269
20281
|
|
|
20270
20282
|
// src/mcp/tools/recall.ts
|
|
20271
20283
|
var recallInputSchema = {
|
|
20272
|
-
terms: array(string2()).min(1).describe("
|
|
20284
|
+
terms: array(string2()).min(1).describe("2-5 short anchor-heavy terms or exact phrases. Prefer identifiers, commands, file paths, and exact wording likely to appear in the memory."),
|
|
20273
20285
|
limit: number2().int().min(1).max(MAX_RECALL_LIMIT).optional().describe("Maximum matches to return. Keep this small when you only need the strongest hits."),
|
|
20274
|
-
workspace: string2().optional().describe("
|
|
20275
|
-
updated_after: string2().optional().describe("Only return memories updated
|
|
20276
|
-
updated_before: string2().optional().describe("Only return memories updated
|
|
20286
|
+
workspace: string2().optional().describe("Current working directory for project-scoped recall. Omit for cross-project recall."),
|
|
20287
|
+
updated_after: string2().optional().describe("Only return memories updated on or after this ISO 8601 timestamp."),
|
|
20288
|
+
updated_before: string2().optional().describe("Only return memories updated on or before this ISO 8601 timestamp.")
|
|
20277
20289
|
};
|
|
20278
20290
|
function toMemoryXml(r) {
|
|
20279
20291
|
const workspace = r.workspace ? ` workspace="${escapeXml(r.workspace)}"` : "";
|
|
@@ -20285,7 +20297,12 @@ ${content}
|
|
|
20285
20297
|
}
|
|
20286
20298
|
function registerRecallTool(server, memory) {
|
|
20287
20299
|
server.registerTool("recall", {
|
|
20288
|
-
|
|
20300
|
+
annotations: {
|
|
20301
|
+
title: "Recall",
|
|
20302
|
+
readOnlyHint: true,
|
|
20303
|
+
openWorldHint: false
|
|
20304
|
+
},
|
|
20305
|
+
description: "Retrieve memories relevant to the current task or check whether a fact already exists before saving. Use at conversation start and before design choices. Pass short anchor-heavy `terms` and `workspace` when available. Results reflect the queried workspace context when applicable. Returns `<memories>...</memories>` or a no-match hint.",
|
|
20289
20306
|
inputSchema: recallInputSchema
|
|
20290
20307
|
}, async ({ terms, limit, workspace, updated_after, updated_before }) => {
|
|
20291
20308
|
try {
|
|
@@ -20296,7 +20313,7 @@ function registerRecallTool(server, memory) {
|
|
|
20296
20313
|
updatedAfter: parseOptionalDate(updated_after, "updated_after"),
|
|
20297
20314
|
updatedBefore: parseOptionalDate(updated_before, "updated_before")
|
|
20298
20315
|
});
|
|
20299
|
-
const text = results.length === 0 ? "No matching memories found. Retry once with 1-3 alternate
|
|
20316
|
+
const text = results.length === 0 ? "No matching memories found. Retry once with 1-3 overlapping alternate terms or an exact identifier, command, file path, or phrase likely to appear in the memory." : `<memories>
|
|
20300
20317
|
${results.map(toMemoryXml).join(`
|
|
20301
20318
|
`)}
|
|
20302
20319
|
</memories>`;
|
|
@@ -20311,12 +20328,18 @@ ${results.map(toMemoryXml).join(`
|
|
|
20311
20328
|
|
|
20312
20329
|
// src/mcp/tools/remember.ts
|
|
20313
20330
|
var rememberInputSchema = {
|
|
20314
|
-
content: string2().describe("One durable fact to save. Use a
|
|
20315
|
-
workspace: string2().optional().describe("
|
|
20331
|
+
content: string2().describe("One new durable fact to save. Use a self-contained sentence or short note."),
|
|
20332
|
+
workspace: string2().optional().describe("Current working directory for project-scoped memory. Omit for facts that apply across projects.")
|
|
20316
20333
|
};
|
|
20317
20334
|
function registerRememberTool(server, memory) {
|
|
20318
20335
|
server.registerTool("remember", {
|
|
20319
|
-
|
|
20336
|
+
annotations: {
|
|
20337
|
+
title: "Remember",
|
|
20338
|
+
destructiveHint: false,
|
|
20339
|
+
idempotentHint: false,
|
|
20340
|
+
openWorldHint: false
|
|
20341
|
+
},
|
|
20342
|
+
description: 'Save one new durable fact for later recall. Use for stable preferences, reusable decisions, and project context not obvious from code or git history. If the fact already exists, use `revise` instead. Returns `<memory id="..." />`.',
|
|
20320
20343
|
inputSchema: rememberInputSchema
|
|
20321
20344
|
}, async ({ content, workspace }) => {
|
|
20322
20345
|
try {
|
|
@@ -20335,12 +20358,18 @@ function registerRememberTool(server, memory) {
|
|
|
20335
20358
|
|
|
20336
20359
|
// src/mcp/tools/revise.ts
|
|
20337
20360
|
var reviseInputSchema = {
|
|
20338
|
-
id: string2().describe("
|
|
20339
|
-
content: string2().describe("
|
|
20361
|
+
id: string2().describe("Memory id to update. Use an id returned by `recall`."),
|
|
20362
|
+
content: string2().describe("Corrected replacement text for that memory.")
|
|
20340
20363
|
};
|
|
20341
20364
|
function registerReviseTool(server, memory) {
|
|
20342
20365
|
server.registerTool("revise", {
|
|
20343
|
-
|
|
20366
|
+
annotations: {
|
|
20367
|
+
title: "Revise",
|
|
20368
|
+
destructiveHint: false,
|
|
20369
|
+
idempotentHint: false,
|
|
20370
|
+
openWorldHint: false
|
|
20371
|
+
},
|
|
20372
|
+
description: 'Update one existing memory when the same fact still applies but its wording or details changed. Use after `recall` when you already have the memory id. Returns `<memory id="..." updated_at="..." />`.',
|
|
20344
20373
|
inputSchema: reviseInputSchema
|
|
20345
20374
|
}, async ({ id, content }) => {
|
|
20346
20375
|
try {
|
|
@@ -20361,15 +20390,12 @@ function registerReviseTool(server, memory) {
|
|
|
20361
20390
|
|
|
20362
20391
|
// src/mcp/server.ts
|
|
20363
20392
|
var SERVER_INSTRUCTIONS = [
|
|
20364
|
-
"Use this server for durable memory:
|
|
20365
|
-
"Use `recall` at conversation start
|
|
20366
|
-
"
|
|
20367
|
-
"`
|
|
20368
|
-
"
|
|
20369
|
-
"
|
|
20370
|
-
"Call `recall` before `remember`; if the fact already exists, use `revise` instead of creating a duplicate.",
|
|
20371
|
-
"Use `revise` to correct an existing memory and `forget` to remove a wrong or obsolete one.",
|
|
20372
|
-
"Pass workspace for project-scoped calls. Git worktree paths are canonicalized to the main repo root on save and recall. Omit workspace only for truly global memories."
|
|
20393
|
+
"Use this server only for durable memory that should survive across turns: stable preferences, corrections, reusable decisions, and project context not obvious from code or git history.",
|
|
20394
|
+
"Use `recall` at conversation start, before design choices, and before saving or revising memory.",
|
|
20395
|
+
"Use `remember` for one new durable fact. Use `revise` when the fact already exists but needs correction.",
|
|
20396
|
+
"Use `forget` only when a memory is wrong or obsolete.",
|
|
20397
|
+
"Pass workspace for project-scoped memory. Omit it only for facts that apply across projects.",
|
|
20398
|
+
"Do not store secrets, temporary task state, or facts obvious from current code or git history."
|
|
20373
20399
|
].join(" ");
|
|
20374
20400
|
function createMcpServer(memory, version3) {
|
|
20375
20401
|
const server = new McpServer({
|
|
@@ -24224,7 +24250,7 @@ function createGitWorkspaceResolver(options = {}) {
|
|
|
24224
24250
|
const cache = new Map;
|
|
24225
24251
|
return {
|
|
24226
24252
|
async resolve(workspace) {
|
|
24227
|
-
const trimmed =
|
|
24253
|
+
const trimmed = normalizeOptionalString2(workspace);
|
|
24228
24254
|
if (!trimmed) {
|
|
24229
24255
|
return;
|
|
24230
24256
|
}
|
|
@@ -24255,7 +24281,7 @@ async function defaultGetGitCommonDir(cwd) {
|
|
|
24255
24281
|
});
|
|
24256
24282
|
return stdout.trim();
|
|
24257
24283
|
}
|
|
24258
|
-
function
|
|
24284
|
+
function normalizeOptionalString2(value) {
|
|
24259
24285
|
const trimmed = value?.trim();
|
|
24260
24286
|
return trimmed ? trimmed : undefined;
|
|
24261
24287
|
}
|