@gamaze/hicortex 0.16.0 → 0.16.1
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 +1 -0
- package/dist/consolidate.js +4 -1
- package/dist/distiller.js +19 -12
- package/dist/eval/relevance-eval.d.ts +64 -0
- package/dist/eval/relevance-eval.js +1954 -0
- package/dist/index.js +3 -2
- package/dist/lessons-context.js +3 -2
- package/dist/mcp-server.js +1 -0
- package/dist/prompts.js +22 -6
- package/dist/recall-index.d.ts +43 -4
- package/dist/recall-index.js +40 -8
- package/dist/seed-lesson.d.ts +1 -1
- package/dist/seed-lesson.js +1 -1
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -163,10 +163,11 @@ async function buildLessonsBlock(project) {
|
|
|
163
163
|
if (selected.length === 0)
|
|
164
164
|
return null;
|
|
165
165
|
const formatted = selected.map((l) => {
|
|
166
|
-
const titleMatch = l.content.match(/## Lesson: (.+)/);
|
|
167
166
|
const typeMatch = l.content.match(/\*\*Type:\*\* (\w+)/);
|
|
168
167
|
const severityMatch = l.content.match(/\*\*Severity:\*\* (\w+)/);
|
|
169
|
-
|
|
168
|
+
// First line, with any legacy `## Lesson:` prefix stripped — new lessons
|
|
169
|
+
// are stored topic-first without the prefix (memory_type carries the type).
|
|
170
|
+
const title = l.content.replace(/^##\s*Lesson:\s*/i, "").split("\n")[0].slice(0, 150);
|
|
170
171
|
const meta = [severityMatch?.[1], typeMatch?.[1]].filter(Boolean).join(", ");
|
|
171
172
|
return `- ${title}${meta ? ` (${meta})` : ""}`;
|
|
172
173
|
});
|
package/dist/lessons-context.js
CHANGED
|
@@ -90,10 +90,11 @@ async function fetchLessonsBlock(cfg) {
|
|
|
90
90
|
const project = (0, node_path_1.basename)(process.cwd()) || null;
|
|
91
91
|
const selected = await (0, extensions_js_1.getLessonSelector)().select(data.lessons, { maxLessons, moduleIndex, project });
|
|
92
92
|
const lessonLines = selected.map((l) => {
|
|
93
|
-
const titleMatch = l.content.match(/## Lesson: (.+)/);
|
|
94
93
|
const typeMatch = l.content.match(/\*\*Type:\*\* (\w+)/);
|
|
95
94
|
const severityMatch = l.content.match(/\*\*Severity:\*\* (\w+)/);
|
|
96
|
-
|
|
95
|
+
// First line, with any legacy `## Lesson:` prefix stripped — new lessons
|
|
96
|
+
// are stored topic-first without the prefix (memory_type carries the type).
|
|
97
|
+
const title = l.content.replace(/^##\s*Lesson:\s*/i, "").split("\n")[0].slice(0, 150);
|
|
97
98
|
const meta = [severityMatch?.[1], typeMatch?.[1]].filter(Boolean).join(", ");
|
|
98
99
|
return `- ${title}${meta ? ` (${meta})` : ""}`;
|
|
99
100
|
});
|
package/dist/mcp-server.js
CHANGED
|
@@ -494,6 +494,7 @@ async function startServer(options = {}) {
|
|
|
494
494
|
minSimilarity: savedConfig?.recallMinSimilarity,
|
|
495
495
|
maxItems: savedConfig?.recallMaxItems,
|
|
496
496
|
minPromptLength: savedConfig?.recallMinPromptChars,
|
|
497
|
+
titleChars: savedConfig?.recallTitleChars,
|
|
497
498
|
};
|
|
498
499
|
memoryInstructionsEnabled = savedConfig?.memoryInstructions !== false;
|
|
499
500
|
if (resolvedAgents.dropped.length > 0) {
|
package/dist/prompts.js
CHANGED
|
@@ -108,25 +108,39 @@ EXTRACT into this markdown format:
|
|
|
108
108
|
## Classification: [pick one: PUBLIC / WORK / PERSONAL / SENSITIVE]
|
|
109
109
|
|
|
110
110
|
### Decisions Made
|
|
111
|
-
- [
|
|
111
|
+
- [SUBJECT]: [decision] — [reasoning] (${date})
|
|
112
112
|
|
|
113
113
|
### Facts Learned
|
|
114
|
-
- [
|
|
114
|
+
- [SUBJECT]: [fact] — [context/source] (${date})
|
|
115
115
|
|
|
116
116
|
### Problems & Solutions
|
|
117
|
-
- [problem] → [solution that worked] (${date})
|
|
117
|
+
- [SUBJECT]: [problem] → [solution that worked] (${date})
|
|
118
118
|
|
|
119
119
|
### Project State Changes
|
|
120
|
-
- [what changed]
|
|
120
|
+
- [SUBJECT]: [what changed], [from → to] (${date})
|
|
121
121
|
|
|
122
122
|
### Key Entities & Relationships
|
|
123
123
|
- [entity A] → [relationship] → [entity B] (${date})
|
|
124
124
|
|
|
125
125
|
### Corrections & Rejections
|
|
126
|
-
- [what AI proposed] → [why rejected/corrected] → [what user wanted instead] (${date})
|
|
126
|
+
- [SUBJECT]: [what AI proposed] → [why rejected/corrected] → [what user wanted instead] (${date})
|
|
127
127
|
(Include: tool use denials, "no/wrong/redo", style feedback, approach rejections,
|
|
128
128
|
user corrections of AI assumptions, quality complaints like "too verbose")
|
|
129
129
|
|
|
130
|
+
TOPIC-FIRST RULE (critical — read carefully):
|
|
131
|
+
Every item MUST begin with its [SUBJECT]: the concrete thing it is about — the
|
|
132
|
+
system, file, component, decision area, or entity. The subject is what a future
|
|
133
|
+
reader would search for.
|
|
134
|
+
- Write: "Electrical load calculation: don't bundle unknown loads into one figure — user rejected the estimate"
|
|
135
|
+
- NOT: "User rejected AI's bundling of unknown loads"
|
|
136
|
+
- Write: "Nightly capture (Hermes): cron sessions are excluded — source='cron' is skipped before distillation"
|
|
137
|
+
- NOT: "Discovered that cron sessions are filtered out"
|
|
138
|
+
Reason: each item's first words become the memory's one-line index entry AND
|
|
139
|
+
dominate its search embedding. An item that opens with a category label, a
|
|
140
|
+
sentiment ("Strong Negative"), or "User rejected…" is unfindable — it matches
|
|
141
|
+
every emotionally-similar prompt and no topically-relevant one. Front-load the
|
|
142
|
+
subject; put reaction, intensity and reasoning AFTER it.
|
|
143
|
+
|
|
130
144
|
RULES:
|
|
131
145
|
- Extract MAX 20 items total (quality over quantity)
|
|
132
146
|
- Each must be useful if recalled in a future session
|
|
@@ -136,7 +150,9 @@ RULES:
|
|
|
136
150
|
- PRIORITIZE Corrections & Rejections — these are high-value signals for learning
|
|
137
151
|
what the user does NOT want. Even a single "no" or style correction is worth extracting.
|
|
138
152
|
- Strong language or profanity from the user is a high-intensity signal — it indicates
|
|
139
|
-
the correction matters deeply. Note the intensity
|
|
153
|
+
the correction matters deeply. Note the intensity AFTER the subject, never before it
|
|
154
|
+
(e.g. "Pricing tiers: strongly rejected per-agent billing — …", not
|
|
155
|
+
"[Strong Negative] User rejected per-agent billing"). The subject always comes first.
|
|
140
156
|
- PRIVACY CLASSIFICATION (one of):
|
|
141
157
|
- PUBLIC: general tech knowledge, open-source patterns, publicly available info
|
|
142
158
|
- WORK: project-specific decisions, architecture choices, client/business context
|
package/dist/recall-index.d.ts
CHANGED
|
@@ -25,13 +25,31 @@ import { SessionRecallRegistry } from "./recall-registry.js";
|
|
|
25
25
|
export interface RecallIndexOptions {
|
|
26
26
|
/** Minimum measured cosine for vector-only candidates (config
|
|
27
27
|
* `recallMinSimilarity`). FTS-matched candidates pass regardless — a BM25
|
|
28
|
-
* text match is direct evidence of relevance. Default 0.
|
|
29
|
-
*
|
|
28
|
+
* text match is direct evidence of relevance. Default 0.62 (raised from 0.55
|
|
29
|
+
* on 2026-08-03 per a 0.01-step floor sweep on the rewritten corpus): steady
|
|
30
|
+
* ~3:1 noise:signal removal with no knee; 0.62 = +2.2pts precision, 10/98
|
|
31
|
+
* prompts silent, sits below the 0.63 local pessimum. The floor is a noise
|
|
32
|
+
* dial, NOT a silence mechanism — at 0.62 each correctly-silenced empty prompt
|
|
33
|
+
* comes with ~1.5 wrongly-silenced (real signal); a non-cosine gate is the
|
|
34
|
+
* real silence fix (eval #3 §4). */
|
|
30
35
|
minSimilarity?: number;
|
|
31
|
-
/** Max index lines per response (config `recallMaxItems`). Default
|
|
36
|
+
/** Max index lines per response (config `recallMaxItems`). Default 5
|
|
37
|
+
* (lowered from 6 on 2026-08-03). Per-slot decomposition at floor 0.62:
|
|
38
|
+
* slot 6 gives NO prompt its first relevant memory — "6 is wrong" is the
|
|
39
|
+
* robust, prompt-set-independent finding, and 5 captures it. The K-sweep
|
|
40
|
+
* is monotone (precision@4 33.7% > @6 30.6% > @8 28.3%), so 4 is
|
|
41
|
+
* lower-noise — but the 4-vs-5 distinction rests on 5 of 98 prompts and is
|
|
42
|
+
* overfitting-fragile (K and the floor were tuned on the same set); 5 hedges
|
|
43
|
+
* with coverage at modest cost. Lower to 4 if a fresh-prompt eval replicates. */
|
|
32
44
|
maxItems?: number;
|
|
33
45
|
/** Prompts shorter than this are skipped (continuations, "yes", "do it"). */
|
|
34
46
|
minPromptLength?: number;
|
|
47
|
+
/** Max chars of the memory's first line shown in an index entry (config
|
|
48
|
+
* `recallTitleChars`). Default 100 (reverted from 150 on 2026-08-03): the
|
|
49
|
+
* full-corpus relevance eval (#3, §5) found 100 vs 150 statistically
|
|
50
|
+
* identical (0.6pts apart, N=40, full CI overlap); 100 saves ~13% tokens
|
|
51
|
+
* per block. */
|
|
52
|
+
titleChars?: number;
|
|
35
53
|
}
|
|
36
54
|
export interface RecallIndexResult {
|
|
37
55
|
status: number;
|
|
@@ -39,7 +57,28 @@ export interface RecallIndexResult {
|
|
|
39
57
|
}
|
|
40
58
|
/** First content line, de-markdowned and truncated — the index line title. */
|
|
41
59
|
export declare function memoryTitle(content: string, maxLen?: number): string;
|
|
42
|
-
/**
|
|
60
|
+
/**
|
|
61
|
+
* Render one production index line. Exported (2026-08-02, relevance eval #v2)
|
|
62
|
+
* so the eval can measure the REAL rendered surface instead of reimplementing
|
|
63
|
+
* it — `maxLen` threads through to `memoryTitle` unchanged (default
|
|
64
|
+
* DEFAULT_TITLE_CHARS = 100, config `recallTitleChars`) so the eval's snippet-length
|
|
65
|
+
* sweep (spec §4.2) can call this SAME function at 100/150/title1sent without
|
|
66
|
+
* duplicating the date/scope/agent/type meta-line logic.
|
|
67
|
+
*/
|
|
68
|
+
export declare function formatIndexLine(r: MemorySearchResult & {
|
|
69
|
+
domain?: string | null;
|
|
70
|
+
}, maxLen?: number): string;
|
|
71
|
+
/**
|
|
72
|
+
* Relevance gate: a real BM25 text match (FTS) passes unconditionally; a
|
|
73
|
+
* vector-only candidate must clear `minSimilarity`.
|
|
74
|
+
*
|
|
75
|
+
* NOTE: FTS hits BYPASS the similarity floor, so raising the floor shifts
|
|
76
|
+
* weight toward FTS-sourced entries. In practice FTS is currently inert on
|
|
77
|
+
* real prompts — eval #3 had 0 FTS rows / 2,208 (2,203 vector + 5 graph), and
|
|
78
|
+
* a 12-prompt live bedrock sample returned 96/96 vector — so the floor change
|
|
79
|
+
* is safe as measured. But FTS quality is unmeasured; if FTS starts firing
|
|
80
|
+
* (e.g. as #205's fielded-BM25 retune beds in), give it its own eval.
|
|
81
|
+
*/
|
|
43
82
|
export declare function passesRelevanceGate(r: MemorySearchResult, minSimilarity: number): boolean;
|
|
44
83
|
/** Recall filters a client may push per request (#193 review F1): a scoped
|
|
45
84
|
* plugin (Hermes privacy_filter / default_project) must be able to narrow
|
package/dist/recall-index.js
CHANGED
|
@@ -55,6 +55,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
55
55
|
})();
|
|
56
56
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
57
57
|
exports.memoryTitle = memoryTitle;
|
|
58
|
+
exports.formatIndexLine = formatIndexLine;
|
|
58
59
|
exports.passesRelevanceGate = passesRelevanceGate;
|
|
59
60
|
exports.parseStringListParam = parseStringListParam;
|
|
60
61
|
exports.parsePrivacyParam = parsePrivacyParam;
|
|
@@ -62,13 +63,25 @@ exports.handleRecallIndex = handleRecallIndex;
|
|
|
62
63
|
exports.handleMemoryGet = handleMemoryGet;
|
|
63
64
|
exports.formatMemoryGetText = formatMemoryGetText;
|
|
64
65
|
const storage = __importStar(require("./storage.js"));
|
|
65
|
-
|
|
66
|
-
|
|
66
|
+
/** Relevance-gate floor for vector-only candidates (config `recallMinSimilarity`).
|
|
67
|
+
* 0.62 (was 0.55; raised 2026-08-03 on the fine-grain floor sweep — see the
|
|
68
|
+
* minSimilarity doc above). */
|
|
69
|
+
const DEFAULT_MIN_SIMILARITY = 0.62;
|
|
70
|
+
/** Max index lines per pushed recall block (config `recallMaxItems`).
|
|
71
|
+
* 5 (was 6; lowered 2026-08-03 — slot 6 is pure padding at floor 0.62). */
|
|
72
|
+
const DEFAULT_MAX_ITEMS = 5;
|
|
67
73
|
const DEFAULT_MIN_PROMPT_LENGTH = 20;
|
|
68
|
-
/**
|
|
74
|
+
/** Default index-line title length. 100 (reverted from 150 on 2026-08-03:
|
|
75
|
+
* eval #3 §5 showed 100 vs 150 statistically identical; 100 saves ~13% tokens). */
|
|
76
|
+
const DEFAULT_TITLE_CHARS = 100;
|
|
77
|
+
/** Over-fetch multiplier: retrieve `maxItems × 3` candidates so gating + dedup
|
|
78
|
+
* still leave a full menu. Kept at 3 after maxItems 6→5 and the higher floor —
|
|
79
|
+
* permit-short is intended (returning fewer than maxItems when fewer clear the
|
|
80
|
+
* gate is correct, not a defect); raise only if blocks are persistently
|
|
81
|
+
* under-filled in production. */
|
|
69
82
|
const CANDIDATE_MULTIPLIER = 3;
|
|
70
83
|
/** First content line, de-markdowned and truncated — the index line title. */
|
|
71
|
-
function memoryTitle(content, maxLen =
|
|
84
|
+
function memoryTitle(content, maxLen = DEFAULT_TITLE_CHARS) {
|
|
72
85
|
const firstLine = content
|
|
73
86
|
.split("\n")
|
|
74
87
|
.map((l) => l.trim())
|
|
@@ -88,7 +101,15 @@ function formatDate(iso) {
|
|
|
88
101
|
const mm = String(d.getMonth() + 1).padStart(2, "0");
|
|
89
102
|
return `${dd}.${mm}.${d.getFullYear()}`;
|
|
90
103
|
}
|
|
91
|
-
|
|
104
|
+
/**
|
|
105
|
+
* Render one production index line. Exported (2026-08-02, relevance eval #v2)
|
|
106
|
+
* so the eval can measure the REAL rendered surface instead of reimplementing
|
|
107
|
+
* it — `maxLen` threads through to `memoryTitle` unchanged (default
|
|
108
|
+
* DEFAULT_TITLE_CHARS = 100, config `recallTitleChars`) so the eval's snippet-length
|
|
109
|
+
* sweep (spec §4.2) can call this SAME function at 100/150/title1sent without
|
|
110
|
+
* duplicating the date/scope/agent/type meta-line logic.
|
|
111
|
+
*/
|
|
112
|
+
function formatIndexLine(r, maxLen = DEFAULT_TITLE_CHARS) {
|
|
92
113
|
// Provenance (#202): date, scope (domain else project), ORIGIN AGENT, type.
|
|
93
114
|
// The origin agent lets a reader calibrate trust — "from my session" vs
|
|
94
115
|
// another agent/project — before fetching or acting on an entry.
|
|
@@ -100,9 +121,19 @@ function formatIndexLine(r) {
|
|
|
100
121
|
]
|
|
101
122
|
.filter(Boolean)
|
|
102
123
|
.join(", ");
|
|
103
|
-
return `- [${r.id}] ${memoryTitle(r.content)}${meta ? ` (${meta})` : ""}`;
|
|
124
|
+
return `- [${r.id}] ${memoryTitle(r.content, maxLen)}${meta ? ` (${meta})` : ""}`;
|
|
104
125
|
}
|
|
105
|
-
/**
|
|
126
|
+
/**
|
|
127
|
+
* Relevance gate: a real BM25 text match (FTS) passes unconditionally; a
|
|
128
|
+
* vector-only candidate must clear `minSimilarity`.
|
|
129
|
+
*
|
|
130
|
+
* NOTE: FTS hits BYPASS the similarity floor, so raising the floor shifts
|
|
131
|
+
* weight toward FTS-sourced entries. In practice FTS is currently inert on
|
|
132
|
+
* real prompts — eval #3 had 0 FTS rows / 2,208 (2,203 vector + 5 graph), and
|
|
133
|
+
* a 12-prompt live bedrock sample returned 96/96 vector — so the floor change
|
|
134
|
+
* is safe as measured. But FTS quality is unmeasured; if FTS starts firing
|
|
135
|
+
* (e.g. as #205's fielded-BM25 retune beds in), give it its own eval.
|
|
136
|
+
*/
|
|
106
137
|
function passesRelevanceGate(r, minSimilarity) {
|
|
107
138
|
if (r.source === "fts" || r.source === "both")
|
|
108
139
|
return true;
|
|
@@ -150,6 +181,7 @@ async function handleRecallIndex(deps, body) {
|
|
|
150
181
|
return { status: 200, body: { block: null, skipped: "short-prompt" } };
|
|
151
182
|
}
|
|
152
183
|
const maxItems = clampInt(deps.options?.maxItems, DEFAULT_MAX_ITEMS, 1, 20);
|
|
184
|
+
const titleChars = clampInt(deps.options?.titleChars, DEFAULT_TITLE_CHARS, 40, 400);
|
|
153
185
|
const minSimilarity = clampNumber(deps.options?.minSimilarity, DEFAULT_MIN_SIMILARITY, 0, 1);
|
|
154
186
|
const turn = deps.registry.beginTurn(sessionId);
|
|
155
187
|
// Optional client-side scoping (F1 + #203): project + mission_domains (soft
|
|
@@ -183,7 +215,7 @@ async function handleRecallIndex(deps, body) {
|
|
|
183
215
|
deps.registry.markShown(sessionId, ids);
|
|
184
216
|
// Exposure signal: shown_count + last_accessed refresh, NOT access_count.
|
|
185
217
|
storage.touchMemoriesShown(deps.db, ids, new Date().toISOString());
|
|
186
|
-
const lines = picked.map((r) => formatIndexLine(r));
|
|
218
|
+
const lines = picked.map((r) => formatIndexLine(r, titleChars));
|
|
187
219
|
const block = [
|
|
188
220
|
"## Memory recall (auto)",
|
|
189
221
|
// Provenance is BUILT IN (owner decision 27.07, option D; extended #202/#204):
|
package/dist/seed-lesson.d.ts
CHANGED
|
@@ -3,5 +3,5 @@
|
|
|
3
3
|
* Shared between OC plugin and CC MCP server.
|
|
4
4
|
*/
|
|
5
5
|
import type Database from "better-sqlite3";
|
|
6
|
-
export declare const SEED_LESSON = "
|
|
6
|
+
export declare const SEED_LESSON = "1% Daily Self-Improvement\n\nSelf-improvement protocol: Review your sessions daily. Scan for mistakes, gaps, user corrections, and friction. Identify 1-2 issues maximum. Apply one targeted fix to your agent instruction files (e.g. SOUL.md / MEMORY.md for Hermes profiles, CLAUDE.md for Claude Code). Never overcorrect \u2014 feedback is guidance, not emergency. One fix per issue. Never rewrite entire systems. Before changing something, ask: is this a real pattern or am I reacting to noise?\n\nThe flywheel: Sessions \u2192 Hicortex captures \u2192 Nightly consolidation \u2192 Lessons generated \u2192 Lessons injected into your context \u2192 You apply one fix \u2192 Better sessions tomorrow.\n\nUse hicortex_lessons to check for new insights. If a lesson has high confidence, apply it. If medium, observe for one more day. If nothing needs fixing today, skip \u2014 that's fine.\n\n**Severity:** critical\n**Confidence:** high\n**Generated:** seed-lesson";
|
|
7
7
|
export declare function injectSeedLesson(database: Database.Database, log?: (msg: string) => void): Promise<void>;
|
package/dist/seed-lesson.js
CHANGED
|
@@ -41,7 +41,7 @@ exports.SEED_LESSON = void 0;
|
|
|
41
41
|
exports.injectSeedLesson = injectSeedLesson;
|
|
42
42
|
const embedder_js_1 = require("./embedder.js");
|
|
43
43
|
const storage = __importStar(require("./storage.js"));
|
|
44
|
-
exports.SEED_LESSON =
|
|
44
|
+
exports.SEED_LESSON = `1% Daily Self-Improvement
|
|
45
45
|
|
|
46
46
|
Self-improvement protocol: Review your sessions daily. Scan for mistakes, gaps, user corrections, and friction. Identify 1-2 issues maximum. Apply one targeted fix to your agent instruction files (e.g. SOUL.md / MEMORY.md for Hermes profiles, CLAUDE.md for Claude Code). Never overcorrect — feedback is guidance, not emergency. One fix per issue. Never rewrite entire systems. Before changing something, ask: is this a real pattern or am I reacting to noise?
|
|
47
47
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gamaze/hicortex",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.1",
|
|
4
4
|
"description": "Self-learning memory for AI agents — experience captured automatically, distilled into lessons overnight, shared across your whole fleet. Works with Hermes, OpenClaw, Claude Code, and Pi.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
"test:watch": "vitest",
|
|
40
40
|
"eval": "node dist/eval/run-eval.js",
|
|
41
41
|
"eval:recall-sweep": "node dist/eval/recall-sweep.js",
|
|
42
|
+
"eval:relevance": "node dist/eval/relevance-eval.js",
|
|
42
43
|
"prepack": "npm run build && rm -rf ./hermes-plugin && mkdir -p ./hermes-plugin && cp -r ../../hermes-plugin/hicortex ./hermes-plugin/ && find ./hermes-plugin -name __pycache__ -type d -exec rm -rf {} + 2>/dev/null || true",
|
|
43
44
|
"prepublishOnly": "npm run build && rm -rf ./hermes-plugin && mkdir -p ./hermes-plugin && cp -r ../../hermes-plugin/hicortex ./hermes-plugin/ && find ./hermes-plugin -name __pycache__ -type d -exec rm -rf {} + 2>/dev/null || true"
|
|
44
45
|
},
|