claude-mem 13.3.0 → 13.4.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/.codex-plugin/plugin.json +1 -1
- package/dist/binaries/worker-service-v10.3.1-win-x64.exe +0 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +8 -0
- package/dist/npx-cli/index.js +439 -393
- package/dist/opencode-plugin/index.js +34 -33
- package/dist/sdk/index.d.ts +109 -0
- package/dist/sdk/index.js +183 -0
- package/openclaw/dist/index.js +13 -13
- package/openclaw/openclaw.plugin.json +1 -1
- package/openclaw/src/index.ts +30 -24
- package/package.json +8 -5
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/.mcp.json +3 -3
- package/plugin/bun.lock +163 -0
- package/plugin/modes/code.json +16 -16
- package/plugin/package.json +2 -2
- package/plugin/scripts/bun-runner.js +80 -47
- package/plugin/scripts/context-generator.cjs +81 -66
- package/plugin/scripts/mcp-server.cjs +40 -33
- package/plugin/scripts/server-beta-service.cjs +201 -147
- package/plugin/scripts/transcript-watcher.cjs +26 -0
- package/plugin/scripts/version-check.js +125 -1
- package/plugin/scripts/worker-service.cjs +393 -9735
- package/plugin/skills/smart-explore/SKILL.md +6 -3
- package/plugin/skills/standup/SKILL.md +157 -0
- package/plugin/skills/standup/agent-brief.md +47 -0
- package/plugin/skills/standup/standup.mjs +662 -0
- package/plugin/ui/viewer-bundle.js +13 -14
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude-Mem SDK - TypeScript Declarations
|
|
3
|
+
*
|
|
4
|
+
* Standalone module for external consumers to parse claude-mem observation XML
|
|
5
|
+
* and build prompts for the memory worker.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export interface ParsedObservation {
|
|
9
|
+
type: string;
|
|
10
|
+
title: string | null;
|
|
11
|
+
subtitle: string | null;
|
|
12
|
+
facts: string[];
|
|
13
|
+
narrative: string | null;
|
|
14
|
+
concepts: string[];
|
|
15
|
+
files_read: string[];
|
|
16
|
+
files_modified: string[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ParsedSummary {
|
|
20
|
+
request: string | null;
|
|
21
|
+
investigated: string | null;
|
|
22
|
+
learned: string | null;
|
|
23
|
+
completed: string | null;
|
|
24
|
+
next_steps: string | null;
|
|
25
|
+
notes: string | null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface Observation {
|
|
29
|
+
id: number;
|
|
30
|
+
tool_name: string;
|
|
31
|
+
tool_input: string;
|
|
32
|
+
tool_output: string;
|
|
33
|
+
created_at_epoch: number;
|
|
34
|
+
cwd?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ParseObservationsOptions {
|
|
38
|
+
/** Array of valid observation types. If provided, validates types against this list. */
|
|
39
|
+
validTypes?: string[];
|
|
40
|
+
/** Type to use if type is missing or invalid. Defaults to 'observation'. */
|
|
41
|
+
fallbackType?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Parse observation XML blocks from text
|
|
46
|
+
* Returns all observations found in the text
|
|
47
|
+
*
|
|
48
|
+
* @param text - The text containing observation XML blocks
|
|
49
|
+
* @param options - Optional configuration for type validation
|
|
50
|
+
* @returns Array of parsed observations
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```typescript
|
|
54
|
+
* const text = `<observation>
|
|
55
|
+
* <type>code_change</type>
|
|
56
|
+
* <title>Added login feature</title>
|
|
57
|
+
* <facts><fact>New auth module</fact></facts>
|
|
58
|
+
* </observation>`;
|
|
59
|
+
*
|
|
60
|
+
* const observations = parseObservations(text);
|
|
61
|
+
* // => [{ type: 'code_change', title: 'Added login feature', ... }]
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
export function parseObservations(
|
|
65
|
+
text: string,
|
|
66
|
+
options?: ParseObservationsOptions
|
|
67
|
+
): ParsedObservation[];
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Parse summary XML block from text
|
|
71
|
+
* Returns null if no valid summary found or if summary was skipped
|
|
72
|
+
*
|
|
73
|
+
* @param text - The text containing summary XML block
|
|
74
|
+
* @returns Parsed summary or null
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* ```typescript
|
|
78
|
+
* const text = `<summary>
|
|
79
|
+
* <request>Implement auth</request>
|
|
80
|
+
* <completed>Added JWT tokens</completed>
|
|
81
|
+
* </summary>`;
|
|
82
|
+
*
|
|
83
|
+
* const summary = parseSummary(text);
|
|
84
|
+
* // => { request: 'Implement auth', completed: 'Added JWT tokens', ... }
|
|
85
|
+
* ```
|
|
86
|
+
*/
|
|
87
|
+
export function parseSummary(text: string): ParsedSummary | null;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Build prompt to send tool observation to SDK agent
|
|
91
|
+
*
|
|
92
|
+
* @param obs - The observation object containing tool data
|
|
93
|
+
* @returns Formatted XML prompt string
|
|
94
|
+
*
|
|
95
|
+
* @example
|
|
96
|
+
* ```typescript
|
|
97
|
+
* const obs = {
|
|
98
|
+
* id: 1,
|
|
99
|
+
* tool_name: 'Read',
|
|
100
|
+
* tool_input: '{"file_path": "/src/index.ts"}',
|
|
101
|
+
* tool_output: '{"content": "..."}',
|
|
102
|
+
* created_at_epoch: Date.now()
|
|
103
|
+
* };
|
|
104
|
+
*
|
|
105
|
+
* const prompt = buildObservationPrompt(obs);
|
|
106
|
+
* // => '<observed_from_primary_session>...'
|
|
107
|
+
* ```
|
|
108
|
+
*/
|
|
109
|
+
export function buildObservationPrompt(obs: Observation): string;
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude-Mem SDK - Standalone module for external consumers
|
|
3
|
+
*
|
|
4
|
+
* This is a self-contained module that exports parsing and prompt utilities
|
|
5
|
+
* without internal dependencies on the main claude-mem codebase.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* import { parseObservations, buildObservationPrompt } from 'claude-mem/sdk';
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// ============================================================================
|
|
12
|
+
// Parser Functions
|
|
13
|
+
// ============================================================================
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Parse observation XML blocks from text
|
|
17
|
+
* Returns all observations found in the text
|
|
18
|
+
*
|
|
19
|
+
* @param {string} text - The text containing observation XML blocks
|
|
20
|
+
* @param {Object} [options] - Optional configuration
|
|
21
|
+
* @param {string[]} [options.validTypes] - Array of valid observation types. If provided, validates types.
|
|
22
|
+
* @param {string} [options.fallbackType='observation'] - Type to use if type is missing or invalid
|
|
23
|
+
* @returns {ParsedObservation[]} Array of parsed observations
|
|
24
|
+
*/
|
|
25
|
+
export function parseObservations(text, options = {}) {
|
|
26
|
+
const observations = [];
|
|
27
|
+
const { validTypes, fallbackType = 'observation' } = options;
|
|
28
|
+
|
|
29
|
+
// Match <observation>...</observation> blocks (non-greedy)
|
|
30
|
+
const observationRegex = /<observation>([\s\S]*?)<\/observation>/g;
|
|
31
|
+
|
|
32
|
+
let match;
|
|
33
|
+
while ((match = observationRegex.exec(text)) !== null) {
|
|
34
|
+
const obsContent = match[1];
|
|
35
|
+
|
|
36
|
+
// Extract all fields
|
|
37
|
+
const type = extractField(obsContent, 'type');
|
|
38
|
+
const title = extractField(obsContent, 'title');
|
|
39
|
+
const subtitle = extractField(obsContent, 'subtitle');
|
|
40
|
+
const narrative = extractField(obsContent, 'narrative');
|
|
41
|
+
const facts = extractArrayElements(obsContent, 'facts', 'fact');
|
|
42
|
+
const concepts = extractArrayElements(obsContent, 'concepts', 'concept');
|
|
43
|
+
const files_read = extractArrayElements(obsContent, 'files_read', 'file');
|
|
44
|
+
const files_modified = extractArrayElements(obsContent, 'files_modified', 'file');
|
|
45
|
+
|
|
46
|
+
// Determine final type
|
|
47
|
+
let finalType = fallbackType;
|
|
48
|
+
if (type) {
|
|
49
|
+
const trimmedType = type.trim();
|
|
50
|
+
if (validTypes) {
|
|
51
|
+
finalType = validTypes.includes(trimmedType) ? trimmedType : fallbackType;
|
|
52
|
+
} else {
|
|
53
|
+
finalType = trimmedType;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Filter out type from concepts array (types and concepts are separate dimensions)
|
|
58
|
+
const cleanedConcepts = concepts.filter(c => c !== finalType);
|
|
59
|
+
|
|
60
|
+
observations.push({
|
|
61
|
+
type: finalType,
|
|
62
|
+
title,
|
|
63
|
+
subtitle,
|
|
64
|
+
facts,
|
|
65
|
+
narrative,
|
|
66
|
+
concepts: cleanedConcepts,
|
|
67
|
+
files_read,
|
|
68
|
+
files_modified
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return observations;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Parse summary XML block from text
|
|
77
|
+
* Returns null if no valid summary found or if summary was skipped
|
|
78
|
+
*
|
|
79
|
+
* @param {string} text - The text containing summary XML block
|
|
80
|
+
* @returns {ParsedSummary|null} Parsed summary or null
|
|
81
|
+
*/
|
|
82
|
+
export function parseSummary(text) {
|
|
83
|
+
// Check for skip_summary first
|
|
84
|
+
const skipRegex = /<skip_summary\s+reason="([^"]+)"\s*\/>/;
|
|
85
|
+
const skipMatch = skipRegex.exec(text);
|
|
86
|
+
|
|
87
|
+
if (skipMatch) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Match <summary>...</summary> block (non-greedy)
|
|
92
|
+
const summaryRegex = /<summary>([\s\S]*?)<\/summary>/;
|
|
93
|
+
const summaryMatch = summaryRegex.exec(text);
|
|
94
|
+
|
|
95
|
+
if (!summaryMatch) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const summaryContent = summaryMatch[1];
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
request: extractField(summaryContent, 'request'),
|
|
103
|
+
investigated: extractField(summaryContent, 'investigated'),
|
|
104
|
+
learned: extractField(summaryContent, 'learned'),
|
|
105
|
+
completed: extractField(summaryContent, 'completed'),
|
|
106
|
+
next_steps: extractField(summaryContent, 'next_steps'),
|
|
107
|
+
notes: extractField(summaryContent, 'notes')
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Extract a simple field value from XML content
|
|
113
|
+
* Returns null for missing or empty/whitespace-only fields
|
|
114
|
+
*/
|
|
115
|
+
function extractField(content, fieldName) {
|
|
116
|
+
const regex = new RegExp(`<${fieldName}>([^<]*)</${fieldName}>`);
|
|
117
|
+
const match = regex.exec(content);
|
|
118
|
+
if (!match) return null;
|
|
119
|
+
|
|
120
|
+
const trimmed = match[1].trim();
|
|
121
|
+
return trimmed === '' ? null : trimmed;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Extract array of elements from XML content
|
|
126
|
+
*/
|
|
127
|
+
function extractArrayElements(content, arrayName, elementName) {
|
|
128
|
+
const elements = [];
|
|
129
|
+
|
|
130
|
+
// Match the array block
|
|
131
|
+
const arrayRegex = new RegExp(`<${arrayName}>(.*?)</${arrayName}>`, 's');
|
|
132
|
+
const arrayMatch = arrayRegex.exec(content);
|
|
133
|
+
|
|
134
|
+
if (!arrayMatch) {
|
|
135
|
+
return elements;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const arrayContent = arrayMatch[1];
|
|
139
|
+
|
|
140
|
+
// Extract individual elements
|
|
141
|
+
const elementRegex = new RegExp(`<${elementName}>([^<]+)</${elementName}>`, 'g');
|
|
142
|
+
let elementMatch;
|
|
143
|
+
while ((elementMatch = elementRegex.exec(arrayContent)) !== null) {
|
|
144
|
+
elements.push(elementMatch[1].trim());
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return elements;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ============================================================================
|
|
151
|
+
// Prompt Building Functions
|
|
152
|
+
// ============================================================================
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Build prompt to send tool observation to SDK agent
|
|
156
|
+
*
|
|
157
|
+
* @param {Observation} obs - The observation object containing tool data
|
|
158
|
+
* @returns {string} Formatted XML prompt string
|
|
159
|
+
*/
|
|
160
|
+
export function buildObservationPrompt(obs) {
|
|
161
|
+
// Safely parse tool_input and tool_output - they may be JSON strings
|
|
162
|
+
let toolInput;
|
|
163
|
+
let toolOutput;
|
|
164
|
+
|
|
165
|
+
try {
|
|
166
|
+
toolInput = typeof obs.tool_input === 'string' ? JSON.parse(obs.tool_input) : obs.tool_input;
|
|
167
|
+
} catch {
|
|
168
|
+
toolInput = obs.tool_input;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
toolOutput = typeof obs.tool_output === 'string' ? JSON.parse(obs.tool_output) : obs.tool_output;
|
|
173
|
+
} catch {
|
|
174
|
+
toolOutput = obs.tool_output;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return `<observed_from_primary_session>
|
|
178
|
+
<what_happened>${obs.tool_name}</what_happened>
|
|
179
|
+
<occurred_at>${new Date(obs.created_at_epoch).toISOString()}</occurred_at>${obs.cwd ? `\n <working_directory>${obs.cwd}</working_directory>` : ''}
|
|
180
|
+
<parameters>${JSON.stringify(toolInput, null, 2)}</parameters>
|
|
181
|
+
<outcome>${JSON.stringify(toolOutput, null, 2)}</outcome>
|
|
182
|
+
</observed_from_primary_session>`;
|
|
183
|
+
}
|
package/openclaw/dist/index.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
var
|
|
2
|
-
**${o}**`;return e.subtitle&&(
|
|
3
|
-
${e.subtitle}`),
|
|
1
|
+
var Y="127.0.0.1",X=["\u{1F527}","\u{1F4D0}","\u{1F50D}","\u{1F4BB}","\u{1F9EA}","\u{1F41B}","\u{1F6E1}\uFE0F","\u2601\uFE0F","\u{1F4E6}","\u{1F3AF}","\u{1F52E}","\u26A1","\u{1F30A}","\u{1F3A8}","\u{1F4CA}","\u{1F680}","\u{1F52C}","\u{1F3D7}\uFE0F","\u{1F4DD}","\u{1F3AD}"];function V(e){let s=0;for(let o=0;o<e.length;o++)s=(s<<5)-s+e.charCodeAt(o)|0;return X[Math.abs(s)%X.length]}var ee="\u{1F99E}",ne="\u2328\uFE0F",te="Claude Code Session",re="\u{1F980}";function se(e){let s=e?.primary??ee,o=e?.claudeCode??ne,a=e?.claudeCodeLabel??te,l=e?.default??re,g=e?.agents??{};return function(m){if(!m)return l;if(m.startsWith("openclaw-")){let p=m.slice(9);return p?`${g[p]||V(p)} ${p}`:`${s} openclaw`}if(m==="openclaw")return`${s} openclaw`;let b=a.trim();return b?`${o} ${b} (${m})`:`${o} ${m}`}}var q=Y;function T(e){return`http://${q}:${e}`}var oe=3,Q=3e4,y="CLOSED",j=0,J=0,$=!1;function G(e){return y==="CLOSED"?!0:y==="OPEN"?Date.now()-J>=Q?(y="HALF_OPEN",e.info("[claude-mem] Circuit breaker: probing worker connection"),$?!1:($=!0,!0)):!1:$?!1:($=!0,!0)}function z(e){y!=="CLOSED"&&e.info("[claude-mem] Worker connection restored \u2014 circuit closed"),y="CLOSED",j=0,$=!1}function M(e){$=!1,j++,(y==="HALF_OPEN"||y==="CLOSED"&&j>=oe)&&(y="OPEN",J=Date.now(),e.warn(`[claude-mem] Worker unreachable \u2014 disabling requests for ${Q/1e3}s`))}function ie(){y="CLOSED",j=0,J=0,$=!1}async function Z(e,s,o,a){if(!G(a))return null;try{let l=await fetch(`${T(e)}${s}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(o)});return l.ok?(z(a),await l.json()):(M(a),a.warn(`[claude-mem] Worker POST ${s} returned ${l.status}`),null)}catch(l){let g=l instanceof Error?l.message:String(l);return M(a),y!=="OPEN"&&a.warn(`[claude-mem] Worker POST ${s} failed: ${g}`),null}}function ae(e,s,o,a){G(a)&&fetch(`${T(e)}${s}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(o)}).then(l=>{if(!l.ok){M(a),a.warn(`[claude-mem] Worker POST ${s} returned ${l.status}`);return}z(a)}).catch(l=>{let g=l instanceof Error?l.message:String(l);M(a),y!=="OPEN"&&a.warn(`[claude-mem] Worker POST ${s} failed: ${g}`)})}async function H(e,s,o){if(!G(o))return null;try{let a=await fetch(`${T(e)}${s}`);return a.ok?(z(o),await a.text()):(M(o),o.warn(`[claude-mem] Worker GET ${s} returned ${a.status}`),null)}catch(a){let l=a instanceof Error?a.message:String(a);return M(o),y!=="OPEN"&&o.warn(`[claude-mem] Worker GET ${s} failed: ${l}`),null}}async function W(e,s,o){let a=await H(e,s,o);if(!a)return null;try{return JSON.parse(a)}catch{return o.warn(`[claude-mem] Worker GET ${s} returned non-JSON response`),null}}function ce(e,s){let o=e.title||"Untitled",l=`${s(e.project)}
|
|
2
|
+
**${o}**`;return e.subtitle&&(l+=`
|
|
3
|
+
${e.subtitle}`),l}var le={telegram:{namespace:"telegram",functionName:"sendMessageTelegram"},whatsapp:{namespace:"whatsapp",functionName:"sendMessageWhatsApp"},discord:{namespace:"discord",functionName:"sendMessageDiscord"},slack:{namespace:"slack",functionName:"sendMessageSlack"},signal:{namespace:"signal",functionName:"sendMessageSignal"},imessage:{namespace:"imessage",functionName:"sendMessageIMessage"},line:{namespace:"line",functionName:"sendMessageLine"}};async function ue(e,s,o,a){try{let l=await fetch(`https://api.telegram.org/bot${e}/sendMessage`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({chat_id:s,text:o,parse_mode:"Markdown"})});if(!l.ok){let g=await l.text();a.warn(`[claude-mem] Direct Telegram send failed (${l.status}): ${g}`)}}catch(l){let g=l instanceof Error?l.message:String(l);a.warn(`[claude-mem] Direct Telegram send error: ${g}`)}}function ge(e,s,o,a,l){if(l&&s==="telegram")return ue(l,o,a,e.logger);let g=le[s];if(!g)return e.logger.warn(`[claude-mem] Unsupported channel type: ${s}`),Promise.resolve();let v=e.runtime.channel[g.namespace];if(!v)return e.logger.warn(`[claude-mem] Channel "${s}" not available in runtime`),Promise.resolve();let m=v[g.functionName];return m?m(...s==="whatsapp"?[o,a,{verbose:!1}]:[o,a]).catch(p=>{let w=p instanceof Error?p.message:String(p);e.logger.error(`[claude-mem] Failed to send to ${s}: ${w}`)}):(e.logger.warn(`[claude-mem] Channel "${s}" has no ${g.functionName} function`),Promise.resolve())}async function de(e,s,o,a,l,g,v,m){let b=1e3,p=3e4;for(;!l.signal.aborted;){try{g("reconnecting"),e.logger.info(`[claude-mem] Connecting to SSE stream at ${T(s)}/stream`);let w=await fetch(`${T(s)}/stream`,{signal:l.signal,headers:{Accept:"text/event-stream"}});if(!w.ok)throw new Error(`SSE stream returned HTTP ${w.status}`);if(!w.body)throw new Error("SSE stream response has no body");g("connected"),b=1e3,e.logger.info("[claude-mem] Connected to SSE stream");let x=w.body.getReader(),D=new TextDecoder,_="";for(;;){let{done:R,value:P}=await x.read();if(R)break;_+=D.decode(P,{stream:!0}),_.length>1048576&&(e.logger.warn("[claude-mem] SSE buffer overflow, clearing buffer"),_="");let L=_.split(`
|
|
4
4
|
|
|
5
|
-
`);_=L.pop()||"";for(let
|
|
5
|
+
`);_=L.pop()||"";for(let B of L){let F=B.split(`
|
|
6
6
|
`).filter(S=>S.startsWith("data:")).map(S=>S.slice(5).trim());if(F.length===0)continue;let A=F.join(`
|
|
7
|
-
`);if(A)try{let S=JSON.parse(A);if(S.type==="new_observation"&&S.observation){let
|
|
8
|
-
`));let f=1e3;u.length>f&&(u=u.slice(0,f));let
|
|
9
|
-
`));break}}await
|
|
10
|
-
`)}function
|
|
11
|
-
`)}}}),e.registerCommand({name:"claude-mem-search",description:"Search Claude-Mem observations by query",acceptsArgs:!0,handler:async r=>{let n=r.args?.trim()||"";if(!n)return"Usage: /claude-mem-search <query> [limit]";let t=n.split(/\s+/),i=t[t.length-1],
|
|
12
|
-
`)}}),e.registerCommand({name:"claude-mem-recent",description:"Show recent Claude-Mem context for a project",acceptsArgs:!0,handler:async r=>{let n=r.args?.trim()||"",t=n?n.split(/\s+/):[],i=t.length>0?t[t.length-1]:"",
|
|
13
|
-
`)}}),e.registerCommand({name:"claude-mem-timeline",description:"Find best memory match and show nearby timeline events",acceptsArgs:!0,handler:async r=>{let n=r.args?.trim()||"";if(!n)return"Usage: /claude-mem-timeline <query> [depthBefore] [depthAfter]";let t=n.split(/\s+/),i=5,
|
|
14
|
-
`)}}),e.registerCommand({name:"claude_mem_status",description:"Check Claude-Mem worker health and session status",handler:async()=>{let r=await
|
|
15
|
-
`)}}catch{return{text:"Claude-Mem worker responded but returned unexpected data"}}}}),e.logger.info(`[claude-mem] OpenClaw plugin loaded \u2014 v1.0.0 (worker: ${
|
|
7
|
+
`);if(A)try{let S=JSON.parse(A);if(S.type==="new_observation"&&S.observation){let C=ce(S.observation,v);await ge(e,o,a,C,m)}}catch(S){let O=S instanceof Error?S.message:String(S);e.logger.warn(`[claude-mem] Failed to parse SSE frame: ${O}`)}}}}catch(w){if(l.signal.aborted)break;g("reconnecting");let x=w instanceof Error?w.message:String(w);e.logger.warn(`[claude-mem] SSE stream error: ${x}. Reconnecting in ${b/1e3}s`)}if(l.signal.aborted)break;await new Promise(w=>setTimeout(w,b)),b=Math.min(b*2,p)}g("disconnected")}function me(e){let s=e.pluginConfig||{},o=s.workerPort||37777;q=s.workerHost||Y;let a=s.project||"openclaw",l=se(s.observationFeed?.emojis);function g(r){return r.agentId?`openclaw-${r.agentId}`:a}let v=new Map,m=new Map,b=new Map,p=new Map,w=s.syncMemoryFile!==!1,x=new Set(s.syncMemoryFileExclude||[]);function D(r){let n=r||"default";return v.has(n)||v.set(n,`openclaw-${n}-${Date.now()}`),v.get(n)}function _(r){if(!w)return!1;let n=r?.agentId;return!(n&&x.has(n))}function R(r){let n=new Set;for(let t of[r.sessionKey,r.conversationId,r.channelId]){let i=typeof t=="string"?t.trim():"";i&&n.add(i)}return n.size===0&&n.add("default"),Array.from(n)}function P(r){let n=R(r),t=n.find(u=>m.has(u));t=t?m.get(t):n[0];let i=b.get(t);i||(i=new Set([t]),b.set(t,i));for(let u of n)i.add(u),m.set(u,t);let c=D(t);for(let u of i)v.set(u,c);return{canonicalKey:t,contentSessionId:c}}function L(r,n,t){let i=Date.now();for(let[d,f]of p)i-f>2e3&&p.delete(d);let c=`${r}::${n}::${t}`,u=p.get(c);return p.set(c,i),typeof u=="number"&&i-u<=2e3}function B(r){let n=R(r),t=n.map(c=>m.get(c)).find(Boolean)||n[0],i=b.get(t)||new Set([t,...n]);for(let c of i)m.delete(c),v.delete(c);b.delete(t),v.delete(t)}let F=6e4,A=new Map;async function S(r){let n=[a],t=r?g(r):null;t&&t!==a&&n.push(t);let i=n.join(","),c=A.get(i);if(c&&Date.now()-c.fetchedAt<F)return c.text;let u=await H(o,`/api/context/inject?projects=${encodeURIComponent(i)}`,e.logger);if(u&&u.trim().length>0){let d=u.trim();return A.set(i,{text:d,fetchedAt:Date.now()}),d}return null}async function O(r,n,t){let{contentSessionId:i}=P(r),c=g(r);if(L(i,c,n)){e.logger.info(`[claude-mem] Skipping duplicate prompt init: contentSessionId=${i} project=${c} via=${t}`);return}await Z(o,"/api/sessions/init",{contentSessionId:i,project:c,prompt:n},e.logger),e.logger.info(`[claude-mem] Session initialized via ${t}: contentSessionId=${i} project=${c}`)}e.on("session_start",async(r,n)=>{await O(n,"session start","session_start")}),e.on("message_received",async(r,n)=>{let{canonicalKey:t,contentSessionId:i}=P(n);e.logger.info(`[claude-mem] Message received \u2014 prompt capture deferred to before_agent_start: session=${t} contentSessionId=${i} hasContent=${!!r.content}`)}),e.on("after_compaction",async(r,n)=>{await O(n,"after compaction","after_compaction")}),e.on("before_agent_start",async(r,n)=>{await O(n,r.prompt||"agent run","before_agent_start")}),e.on("before_prompt_build",async(r,n)=>{if(!_(n))return;let t=await S(n);if(t)return e.logger.info(`[claude-mem] Context injected via system prompt for agent=${n.agentId??"unknown"}`),{appendSystemContext:t}}),e.on("tool_result_persist",(r,n)=>{e.logger.info(`[claude-mem] tool_result_persist fired: tool=${r.toolName??"unknown"} agent=${n.agentId??"none"} session=${n.sessionKey??"none"}`);let t=r.toolName;if(!t||t.startsWith("memory_"))return;let{canonicalKey:i,contentSessionId:c}=P(n),u="",d=r.message?.content;Array.isArray(d)&&(u=d.filter(E=>(E.type==="tool_result"||E.type==="text")&&"text"in E).map(E=>String(E.text)).join(`
|
|
8
|
+
`));let f=1e3;u.length>f&&(u=u.slice(0,f));let h=n.workspaceDir||process.cwd();n.workspaceDir||e.logger.info(`[claude-mem] tool_result_persist missing workspaceDir; using process.cwd(): session=${i} tool=${t}`),ae(o,"/api/sessions/observations",{contentSessionId:c,tool_name:t,tool_input:r.params||{},tool_response:u,cwd:h},e.logger)}),e.on("agent_end",async(r,n)=>{let{contentSessionId:t}=P(n),i="";if(Array.isArray(r.messages))for(let c=r.messages.length-1;c>=0;c--){let u=r.messages[c];if(u?.role==="assistant"){typeof u.content=="string"?i=u.content:Array.isArray(u.content)&&(i=u.content.filter(d=>d.type==="text").map(d=>d.text||"").join(`
|
|
9
|
+
`));break}}await Z(o,"/api/sessions/summarize",{contentSessionId:t,last_assistant_message:i},e.logger)}),e.on("session_end",async(r,n)=>{B(n),e.logger.info("[claude-mem] Session tracking cleaned up")}),e.on("gateway_start",async()=>{ie(),v.clear(),A.clear(),p.clear(),m.clear(),b.clear(),e.logger.info("[claude-mem] Gateway started \u2014 session tracking reset")});let C=null,I="disconnected",k=null;e.registerService({id:"claude-mem-observation-feed",start:async r=>{C&&(C.abort(),k&&(await k,k=null));let n=s.observationFeed;if(!n?.enabled){e.logger.info("[claude-mem] Observation feed disabled");return}if(!n.channel||!n.to){e.logger.warn("[claude-mem] Observation feed misconfigured \u2014 channel or target missing");return}e.logger.info(`[claude-mem] Observation feed starting \u2014 channel: ${n.channel}, target: ${n.to}`),C=new AbortController,k=de(e,o,n.channel,n.to,C,t=>{I=t},l,n.botToken)},stop:async r=>{C&&(C.abort(),C=null),k&&(await k,k=null),I="disconnected",e.logger.info("[claude-mem] Observation feed stopped \u2014 SSE connection closed")}});function U(r,n=5){return!Array.isArray(r)||r.length===0?"No results found.":r.slice(0,n).map((t,i)=>{let c=t,u=String(c.title||c.subtitle||c.text||"Untitled"),d=c.project?` [${String(c.project)}]`:"";return`${i+1}. ${u}${d}`}).join(`
|
|
10
|
+
`)}function N(r,n=10){let t=Number(r);return Number.isFinite(t)?Math.max(1,Math.min(50,Math.trunc(t))):n}e.registerCommand({name:"claude_mem_feed",description:"Show or toggle Claude-Mem observation feed status",acceptsArgs:!0,handler:async r=>{let n=s.observationFeed;if(!n)return{text:"Observation feed not configured. Add observationFeed to your plugin config."};let t=r.args?.trim();return t==="on"?(e.logger.info("[claude-mem] Feed enable requested via command"),{text:"Feed enable requested. Update observationFeed.enabled in your plugin config to persist."}):t==="off"?(e.logger.info("[claude-mem] Feed disable requested via command"),{text:"Feed disable requested. Update observationFeed.enabled in your plugin config to persist."}):{text:["Claude-Mem Observation Feed",`Enabled: ${n.enabled?"yes":"no"}`,`Channel: ${n.channel||"not set"}`,`Target: ${n.to||"not set"}`,`Connection: ${I}`].join(`
|
|
11
|
+
`)}}}),e.registerCommand({name:"claude-mem-search",description:"Search Claude-Mem observations by query",acceptsArgs:!0,handler:async r=>{let n=r.args?.trim()||"";if(!n)return"Usage: /claude-mem-search <query> [limit]";let t=n.split(/\s+/),i=t[t.length-1],c=/^\d+$/.test(i),u=c?N(i,10):10,d=c?t.slice(0,-1).join(" "):n,f=await W(o,`/api/search/observations?query=${encodeURIComponent(d)}&limit=${u}`,e.logger);if(!f)return"Claude-Mem search failed (worker unavailable or invalid response).";let h=Array.isArray(f.items)?f.items:[];return[`Claude-Mem Search: "${d}"`,U(h,u)].join(`
|
|
12
|
+
`)}}),e.registerCommand({name:"claude-mem-recent",description:"Show recent Claude-Mem context for a project",acceptsArgs:!0,handler:async r=>{let n=r.args?.trim()||"",t=n?n.split(/\s+/):[],i=t.length>0?t[t.length-1]:"",c=/^\d+$/.test(i),u=c?N(i,3):3,d=c?t.slice(0,-1).join(" "):n,f=new URLSearchParams;f.set("limit",String(u)),d&&f.set("project",d);let h=await W(o,`/api/context/recent?${f.toString()}`,e.logger);if(!h)return"Claude-Mem recent context failed (worker unavailable or invalid response).";let E=Array.isArray(h.session_summaries)?h.session_summaries:[],K=Array.isArray(h.recent_observations)?h.recent_observations:[];return["Claude-Mem Recent Context",`Project: ${d||"(auto)"}`,`Session summaries: ${E.length}`,`Recent observations: ${K.length}`,U(K,Math.min(5,K.length||5))].join(`
|
|
13
|
+
`)}}),e.registerCommand({name:"claude-mem-timeline",description:"Find best memory match and show nearby timeline events",acceptsArgs:!0,handler:async r=>{let n=r.args?.trim()||"";if(!n)return"Usage: /claude-mem-timeline <query> [depthBefore] [depthAfter]";let t=n.split(/\s+/),i=5,c=5;t.length>=2&&/^\d+$/.test(t[t.length-1])&&(i=N(t.pop(),5)),t.length>=2&&/^\d+$/.test(t[t.length-1])&&(c=N(t.pop(),5));let u=t.join(" "),d=new URLSearchParams({query:u,mode:"auto",depth_before:String(c),depth_after:String(i)}),f=await W(o,`/api/timeline/by-query?${d.toString()}`,e.logger);if(!f)return"Claude-Mem timeline lookup failed (worker unavailable or invalid response).";let h=Array.isArray(f.timeline)?f.timeline:[],E=f.anchor?String(f.anchor):"(none)";return[`Claude-Mem Timeline: "${u}"`,`Anchor: ${E}`,U(h,8)].join(`
|
|
14
|
+
`)}}),e.registerCommand({name:"claude_mem_status",description:"Check Claude-Mem worker health and session status",handler:async()=>{let r=await H(o,"/api/health",e.logger);if(!r)return{text:`Claude-Mem worker unreachable at port ${o}`};try{return{text:["Claude-Mem Worker Status",`Status: ${JSON.parse(r).status||"unknown"}`,`Port: ${o}`,`Active sessions: ${v.size}`,`Observation feed: ${I}`].join(`
|
|
15
|
+
`)}}catch{return{text:"Claude-Mem worker responded but returned unexpected data"}}}}),e.logger.info(`[claude-mem] OpenClaw plugin loaded \u2014 v1.0.0 (worker: ${q}:${o})`)}export{me as default};
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"name": "Claude-Mem (Persistent Memory)",
|
|
4
4
|
"description": "OpenClaw plugin for Claude-Mem. Records observations from embedded runner sessions and streams them to messaging channels.",
|
|
5
5
|
"kind": "memory",
|
|
6
|
-
"version": "13.
|
|
6
|
+
"version": "13.4.1",
|
|
7
7
|
"license": "Apache-2.0",
|
|
8
8
|
"author": "thedotmack",
|
|
9
9
|
"homepage": "https://claude-mem.ai",
|
package/openclaw/src/index.ts
CHANGED
|
@@ -700,28 +700,17 @@ export default function claudeMemPlugin(api: OpenClawPluginApi): void {
|
|
|
700
700
|
return null;
|
|
701
701
|
}
|
|
702
702
|
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
const { canonicalKey, contentSessionId } = rememberSessionContext(ctx);
|
|
710
|
-
api.logger.info(`[claude-mem] Message received — prompt capture deferred to before_agent_start: session=${canonicalKey} contentSessionId=${contentSessionId} hasContent=${Boolean(event.content)}`);
|
|
711
|
-
});
|
|
712
|
-
|
|
713
|
-
api.on("after_compaction", async (_event, ctx) => {
|
|
714
|
-
const { contentSessionId } = rememberSessionContext(ctx);
|
|
715
|
-
api.logger.info(`[claude-mem] Session preserved after compaction: ${contentSessionId}`);
|
|
716
|
-
});
|
|
717
|
-
|
|
718
|
-
api.on("before_agent_start", async (event, ctx) => {
|
|
703
|
+
// Centralized session-init POST. session_start, after_compaction, and
|
|
704
|
+
// before_agent_start each call this; the 2s dedup guard
|
|
705
|
+
// (shouldSkipDuplicatePromptInit) collapses the redundant inits a single
|
|
706
|
+
// user-message flow produces into one prompt record, while still ensuring a
|
|
707
|
+
// session is initialized even on flows that never reach before_agent_start.
|
|
708
|
+
async function initSessionOnce(ctx: EventContext, promptText: string, via: string): Promise<void> {
|
|
719
709
|
const { contentSessionId } = rememberSessionContext(ctx);
|
|
720
710
|
const projectName = getProjectName(ctx);
|
|
721
|
-
const promptText = event.prompt || "agent run";
|
|
722
711
|
|
|
723
712
|
if (shouldSkipDuplicatePromptInit(contentSessionId, projectName, promptText)) {
|
|
724
|
-
api.logger.info(`[claude-mem] Skipping duplicate prompt init: contentSessionId=${contentSessionId} project=${projectName}`);
|
|
713
|
+
api.logger.info(`[claude-mem] Skipping duplicate prompt init: contentSessionId=${contentSessionId} project=${projectName} via=${via}`);
|
|
725
714
|
return;
|
|
726
715
|
}
|
|
727
716
|
|
|
@@ -731,7 +720,24 @@ export default function claudeMemPlugin(api: OpenClawPluginApi): void {
|
|
|
731
720
|
prompt: promptText,
|
|
732
721
|
}, api.logger);
|
|
733
722
|
|
|
734
|
-
api.logger.info(`[claude-mem] Session initialized via
|
|
723
|
+
api.logger.info(`[claude-mem] Session initialized via ${via}: contentSessionId=${contentSessionId} project=${projectName}`);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
api.on("session_start", async (_event, ctx) => {
|
|
727
|
+
await initSessionOnce(ctx, "session start", "session_start");
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
api.on("message_received", async (event, ctx) => {
|
|
731
|
+
const { canonicalKey, contentSessionId } = rememberSessionContext(ctx);
|
|
732
|
+
api.logger.info(`[claude-mem] Message received — prompt capture deferred to before_agent_start: session=${canonicalKey} contentSessionId=${contentSessionId} hasContent=${Boolean(event.content)}`);
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
api.on("after_compaction", async (_event, ctx) => {
|
|
736
|
+
await initSessionOnce(ctx, "after compaction", "after_compaction");
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
api.on("before_agent_start", async (event, ctx) => {
|
|
740
|
+
await initSessionOnce(ctx, event.prompt || "agent run", "before_agent_start");
|
|
735
741
|
});
|
|
736
742
|
|
|
737
743
|
api.on("before_prompt_build", async (_event, ctx) => {
|
|
@@ -767,11 +773,11 @@ export default function claudeMemPlugin(api: OpenClawPluginApi): void {
|
|
|
767
773
|
toolResponseText = toolResponseText.slice(0, MAX_TOOL_RESPONSE_LENGTH);
|
|
768
774
|
}
|
|
769
775
|
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
776
|
+
// Fall back to the process cwd when the event carries no workspaceDir, so a
|
|
777
|
+
// missing ctx field never silently drops a captured observation.
|
|
778
|
+
const workspaceDir = ctx.workspaceDir || process.cwd();
|
|
779
|
+
if (!ctx.workspaceDir) {
|
|
780
|
+
api.logger.info(`[claude-mem] tool_result_persist missing workspaceDir; using process.cwd(): session=${canonicalKey} tool=${toolName}`);
|
|
775
781
|
}
|
|
776
782
|
|
|
777
783
|
workerPostFireAndForget(workerPort, "/api/sessions/observations", {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem",
|
|
3
|
-
"version": "13.
|
|
3
|
+
"version": "13.4.1",
|
|
4
4
|
"description": "Memory compression system for Claude Code - persist context across sessions",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude",
|
|
@@ -47,13 +47,12 @@
|
|
|
47
47
|
"plugin/.claude-plugin",
|
|
48
48
|
"plugin/.codex-plugin",
|
|
49
49
|
"plugin/.mcp.json",
|
|
50
|
-
"plugin/CLAUDE.md",
|
|
51
50
|
"plugin/package.json",
|
|
51
|
+
"plugin/bun.lock",
|
|
52
52
|
"plugin/hooks",
|
|
53
53
|
"plugin/modes",
|
|
54
54
|
"plugin/scripts/*.js",
|
|
55
55
|
"plugin/scripts/*.cjs",
|
|
56
|
-
"plugin/scripts/CLAUDE.md",
|
|
57
56
|
"plugin/skills",
|
|
58
57
|
"plugin/ui",
|
|
59
58
|
"openclaw"
|
|
@@ -64,7 +63,7 @@
|
|
|
64
63
|
},
|
|
65
64
|
"scripts": {
|
|
66
65
|
"dev": "npm run build-and-sync",
|
|
67
|
-
"build": "node scripts/sync-plugin-manifests.js && node scripts/build-hooks.js",
|
|
66
|
+
"build": "node scripts/sync-plugin-manifests.js && node scripts/build-hooks.js && node scripts/gen-plugin-lockfile.cjs",
|
|
68
67
|
"build-and-sync": "npm run build && npm run sync-marketplace && sleep 1 && (cd ~/.claude/plugins/marketplaces/thedotmack && npm run worker:restart)",
|
|
69
68
|
"sync-marketplace": "node scripts/sync-marketplace.cjs",
|
|
70
69
|
"sync-marketplace:force": "node scripts/sync-marketplace.cjs --force",
|
|
@@ -98,6 +97,8 @@
|
|
|
98
97
|
"cursor:uninstall": "bun plugin/scripts/worker-service.cjs cursor uninstall",
|
|
99
98
|
"cursor:status": "bun plugin/scripts/worker-service.cjs cursor status",
|
|
100
99
|
"cursor:setup": "bun plugin/scripts/worker-service.cjs cursor setup",
|
|
100
|
+
"lint:hook-io": "node scripts/check-hook-io-discipline.cjs",
|
|
101
|
+
"lint:spawn-env": "node scripts/check-spawn-env-discipline.cjs",
|
|
101
102
|
"typecheck": "tsc --noEmit && tsc --noEmit -p src/ui/viewer/tsconfig.json",
|
|
102
103
|
"typecheck:root": "tsc --noEmit",
|
|
103
104
|
"typecheck:viewer": "tsc --noEmit -p src/ui/viewer/tsconfig.json",
|
|
@@ -109,7 +110,9 @@
|
|
|
109
110
|
"test:infra": "bun test tests/infrastructure/",
|
|
110
111
|
"test:server": "bun test tests/server/",
|
|
111
112
|
"e2e:server-beta:docker": "bash scripts/e2e-server-beta-docker.sh",
|
|
112
|
-
"
|
|
113
|
+
"check:postinstall-allowlist": "node scripts/check-postinstall-allowlist.js",
|
|
114
|
+
"smoke:clean-room": "node scripts/smoke-clean-room.cjs",
|
|
115
|
+
"prepublishOnly": "npm run build && node scripts/check-postinstall-allowlist.js",
|
|
113
116
|
"release": "np",
|
|
114
117
|
"release:patch": "np patch --no-cleanup",
|
|
115
118
|
"release:minor": "np minor --no-cleanup",
|
package/plugin/.mcp.json
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"mcpServers": {
|
|
3
3
|
"mcp-search": {
|
|
4
4
|
"type": "stdio",
|
|
5
|
-
"command": "
|
|
5
|
+
"command": "node",
|
|
6
6
|
"args": [
|
|
7
|
-
"-
|
|
8
|
-
"
|
|
7
|
+
"-e",
|
|
8
|
+
"const f=require('fs'),p=require('path'),o=require('os'),c=require('child_process');const h=o.homedir();const C=process.env.CLAUDE_CONFIG_DIR||p.join(h,'.claude');const E=process.env.CLAUDE_PLUGIN_ROOT||process.env.PLUGIN_ROOT||'';const d=process.cwd();const L=x=>{try{return f.readdirSync(x).filter(n=>/^\\d/.test(n)).map(n=>p.join(x,n)).filter(z=>{try{return f.statSync(z).isDirectory()}catch{return false}}).sort((a,b)=>f.statSync(b).mtimeMs-f.statSync(a).mtimeMs)}catch{return[]}};const K=[E,p.join(d,\"plugin\"),d,...L(p.join(h,\".codex/plugins/cache/claude-mem-local/claude-mem\")),...L(p.join(h,\".codex/plugins/cache/thedotmack/claude-mem\")),...L(p.join(C,\"plugins/cache/thedotmack/claude-mem\")),p.join(C,\"plugins/marketplaces/thedotmack/plugin\")].filter(Boolean);let R=null;for(const k of K){const r=f.existsSync(p.join(k,'plugin','scripts'))?p.join(k,'plugin'):k;if(f.existsSync(p.join(r,'scripts',\"mcp-server.cjs\"))){R=r;break}}if(!R){process.stderr.write(\"claude-mem: mcp server not found\\n\");process.exit(1)}const ch=c.spawn(process.execPath,[p.join(R,'scripts',\"mcp-server.cjs\")],{stdio:'inherit'});for(const s of ['SIGTERM','SIGINT','SIGHUP'])process.on(s,()=>{try{ch.kill(s)}catch{}});ch.on('exit',(code,sig)=>{if(sig){process.removeAllListeners(sig);try{process.kill(process.pid,sig)}catch{process.exit(1)}}else process.exit(code==null?0:code)})"
|
|
9
9
|
]
|
|
10
10
|
}
|
|
11
11
|
}
|