@fuxi-ailabs/crowdmemory-codex-plugin 0.0.0-stage → 0.1.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/.agents/plugins/marketplace.json +5 -0
- package/.codex-plugin/plugin.json +8 -0
- package/.mcp.json +8 -0
- package/LICENSE +201 -0
- package/README.md +99 -2
- package/contract/SNAPSHOT.json +17 -0
- package/contract/error-signature-vectors.json +143 -0
- package/contract/event-id-vectors.json +128 -0
- package/contract/memory-agent-ingress-v1.schema.json +2521 -0
- package/hooks/hooks.json +12 -0
- package/lib/core/client.js +273 -0
- package/lib/core/mask.js +76 -0
- package/lib/core/memory-tools.js +171 -0
- package/lib/core/recall.js +30 -0
- package/lib/core/redact.js +51 -0
- package/lib/core/replay.js +46 -0
- package/lib/core/signature.js +45 -0
- package/lib/core/state.js +167 -0
- package/lib/core/util.js +60 -0
- package/lib/hook.js +26 -0
- package/lib/mcp.js +94 -0
- package/lib/memory.js +240 -0
- package/lib/tools.js +114 -0
- package/package.json +34 -4
package/lib/memory.js
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/** Codex event translation; storage, ranking, trust and wire format stay in the adapter core / Runtime. */
|
|
2
|
+
import { basename, join, relative, isAbsolute } from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { createRequire } from 'node:module';
|
|
5
|
+
import { RuntimeClient } from './core/client.js';
|
|
6
|
+
import { StateStore } from './core/state.js';
|
|
7
|
+
import { codePoints, utf8Tail, utf8Head } from './core/util.js';
|
|
8
|
+
import { redactQueryText } from './core/redact.js';
|
|
9
|
+
import { errorSignature } from './core/signature.js';
|
|
10
|
+
import { maskedExit } from './core/mask.js';
|
|
11
|
+
import { replayCommand } from './core/replay.js';
|
|
12
|
+
import { fenceRecall, injectOf } from './core/recall.js';
|
|
13
|
+
import { searchBody, searchReport, readReport } from './core/memory-tools.js';
|
|
14
|
+
export const ADAPTER_VERSION = createRequire(import.meta.url)('../package.json').version;
|
|
15
|
+
export const SESSION_ARG = 'crowdmemory_session';
|
|
16
|
+
export const OWN_TOOL = /^mcp__crowdmemory__memory_(?:search|read|note)$/u;
|
|
17
|
+
const text = (v) => typeof v === 'string' ? v : '';
|
|
18
|
+
const off = (v) => /^(?:0|false|off|no)$/iu.test(text(v));
|
|
19
|
+
const object = (v) => v !== null && typeof v === 'object' && !Array.isArray(v) ? v : {};
|
|
20
|
+
export function configFromEnv(env = process.env) {
|
|
21
|
+
const home = env['HOME'] || homedir();
|
|
22
|
+
return {
|
|
23
|
+
endpointFile: env['CROWDMEMORY_ENDPOINT_FILE'] || join(home, '.crowdmemory/state/endpoint.json'),
|
|
24
|
+
// PLUGIN_DATA is hook-only. Both the hook and MCP process must resolve the same fallback directory.
|
|
25
|
+
stateDir: env['CROWDMEMORY_CODEX_STATE'] || join(env['CODEX_HOME'] || join(home, '.codex'), 'crowdmemory/sessions'),
|
|
26
|
+
...(env['CROWDMEMORY_PROJECT'] ? { project: env['CROWDMEMORY_PROJECT'] } : {}),
|
|
27
|
+
profile: env['CROWDMEMORY_PROFILE'] || 'session-default',
|
|
28
|
+
inject: !off(env['CROWDMEMORY_INJECT']), notes: !off(env['CROWDMEMORY_NOTES']),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export function contextOf(event) {
|
|
32
|
+
const session = text(event['session_id']);
|
|
33
|
+
if (!session || session.length > 256)
|
|
34
|
+
return undefined;
|
|
35
|
+
const agent = text(event['agent_id']);
|
|
36
|
+
if (agent.length > 128 || (agent && `${session}/${agent}`.length > 256))
|
|
37
|
+
return undefined;
|
|
38
|
+
return { ref: agent ? `${session}/${agent}` : session, parentRef: agent ? session : null,
|
|
39
|
+
...(text(event['cwd']) ? { cwd: text(event['cwd']) } : {}) };
|
|
40
|
+
}
|
|
41
|
+
/** A running unified-exec result is not a completed command or a successful test. */
|
|
42
|
+
export function shellResult(response) {
|
|
43
|
+
const obj = object(response);
|
|
44
|
+
const output = typeof response === 'string' ? response : text(obj['stdout']) + '\n' + text(obj['stderr']);
|
|
45
|
+
const exit = obj['exit_code'];
|
|
46
|
+
if (typeof exit === 'number' && Number.isInteger(exit))
|
|
47
|
+
return { output, exit };
|
|
48
|
+
// CLI 0.164.0 supplies only stdout here, even for a nonzero exit. Its content is not exit metadata.
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
export class CodexMemory {
|
|
52
|
+
config;
|
|
53
|
+
client;
|
|
54
|
+
store;
|
|
55
|
+
constructor(config) {
|
|
56
|
+
this.config = config;
|
|
57
|
+
this.client = new RuntimeClient({ endpointFile: config.endpointFile, adapterId: 'codex', timeoutMs: { 'session.end': 1200 } });
|
|
58
|
+
this.store = new StateStore(config.stateDir);
|
|
59
|
+
}
|
|
60
|
+
fresh(ctx) { return () => ({ ref: ctx.ref, parentRef: ctx.parentRef, ...(ctx.cwd ? { cwd: ctx.cwd } : {}), seq: 0, step: 0 }); }
|
|
61
|
+
async start(ctx) {
|
|
62
|
+
return this.store.update(ctx.ref, async (r) => {
|
|
63
|
+
if (r.ended)
|
|
64
|
+
return undefined;
|
|
65
|
+
if (r.handle)
|
|
66
|
+
return r.handle;
|
|
67
|
+
if ((r.retryAt ?? 0) > Date.now())
|
|
68
|
+
return undefined;
|
|
69
|
+
const reply = await this.client.send({ kind: 'session.start', sessionRef: ctx.ref, parentRef: ctx.parentRef,
|
|
70
|
+
nativeKey: `start:${++r.seq}`, payload: {
|
|
71
|
+
project: codePoints(this.config.project || basename(ctx.cwd || '') || 'workspace', 256),
|
|
72
|
+
profile_ref: this.config.profile, adapter_version: ADAPTER_VERSION,
|
|
73
|
+
capabilities: ['session_binding', ...(this.config.inject ? ['step_injection'] : []), 'tool_capture', 'subagent_capture',
|
|
74
|
+
'query_tools', ...(this.config.notes ? ['note_tool'] : [])],
|
|
75
|
+
} });
|
|
76
|
+
if (reply.ok && typeof reply.result['session_handle'] === 'string') {
|
|
77
|
+
r.handle = reply.result['session_handle'];
|
|
78
|
+
const disabled = reply.result['disabled_kinds'];
|
|
79
|
+
r.disabled = Array.isArray(disabled) ? disabled.filter((v) => typeof v === 'string') : [];
|
|
80
|
+
return r.handle;
|
|
81
|
+
}
|
|
82
|
+
r.retryAt = Date.now() + 5000;
|
|
83
|
+
return undefined;
|
|
84
|
+
}, this.fresh(ctx));
|
|
85
|
+
}
|
|
86
|
+
async forward(ctx, kind, body, key) {
|
|
87
|
+
if (!ctx)
|
|
88
|
+
return { ok: false, code: 'session_unbound', message: 'Codex PreToolUse binding required' };
|
|
89
|
+
const handle = await this.start(ctx);
|
|
90
|
+
if (!handle)
|
|
91
|
+
return { ok: false, code: 'runtime_unavailable', message: 'memory session unavailable' };
|
|
92
|
+
const seq = await this.store.update(ctx.ref, r => !r.ended && !r.disabled?.includes(kind) ? ++r.seq : undefined);
|
|
93
|
+
if (!seq)
|
|
94
|
+
return { ok: false, code: 'session_unavailable', message: 'session ended or event disabled' };
|
|
95
|
+
const reply = await this.client.send({ kind, sessionRef: ctx.ref, parentRef: ctx.parentRef,
|
|
96
|
+
payload: { session_handle: handle, ...body }, nativeKey: `${key}:${seq}` });
|
|
97
|
+
if (reply.ok && kind === 'query' && body['op'] === 'read')
|
|
98
|
+
await this.store.update(ctx.ref, r => { r.memoryShown = true; });
|
|
99
|
+
return reply;
|
|
100
|
+
}
|
|
101
|
+
async lastFailureFacts(ctx) {
|
|
102
|
+
const r = ctx ? await this.store.read(ctx.ref) : undefined;
|
|
103
|
+
return r?.lastFailure ? { ...r.lastFailure } : undefined;
|
|
104
|
+
}
|
|
105
|
+
async recall(ctx, query) {
|
|
106
|
+
const step = await this.store.update(ctx.ref, r => { r.firstStepDone = true; return r.step; }, this.fresh(ctx));
|
|
107
|
+
const reply = await this.forward(ctx, 'step.before', { step: step ?? 0,
|
|
108
|
+
...(query ? { query_text: redactQueryText(query) } : {}) }, `step:${step}`);
|
|
109
|
+
if (!reply.ok || !this.config.inject)
|
|
110
|
+
return undefined;
|
|
111
|
+
const inject = injectOf(reply.result);
|
|
112
|
+
if (!inject)
|
|
113
|
+
return undefined;
|
|
114
|
+
const context = fenceRecall(utf8Head(inject.text, 14000), 'crowdmemory-codex-plugin');
|
|
115
|
+
await this.store.update(ctx.ref, r => { r.memoryShown = true; r.lastInject = context; });
|
|
116
|
+
return context;
|
|
117
|
+
}
|
|
118
|
+
/** A stdout-only hook has no trustworthy outcome. Search/read on its text without inventing tool.result.ok. */
|
|
119
|
+
async recallOutput(ctx, stdout) {
|
|
120
|
+
if (!this.config.inject || !stdout.trim())
|
|
121
|
+
return undefined;
|
|
122
|
+
const query = redactQueryText(utf8Tail(stdout, 4000));
|
|
123
|
+
const reply = await this.forward(ctx, 'query', searchBody(query, 2, await this.lastFailureFacts(ctx)), 'output.search');
|
|
124
|
+
if (!reply.ok)
|
|
125
|
+
return undefined;
|
|
126
|
+
const hits = Array.isArray(reply.result['hits']) ? reply.result['hits'] : [];
|
|
127
|
+
if (!hits.length)
|
|
128
|
+
return undefined;
|
|
129
|
+
const parts = [searchReport(reply)];
|
|
130
|
+
for (const hit of hits.slice(0, 2)) {
|
|
131
|
+
const ref = text(object(hit)['record_ref']);
|
|
132
|
+
if (!ref)
|
|
133
|
+
continue;
|
|
134
|
+
const record = await this.forward(ctx, 'query', { op: 'read', ref }, `output.read:${ref}`);
|
|
135
|
+
if (record.ok)
|
|
136
|
+
parts.push(readReport(record, ref));
|
|
137
|
+
}
|
|
138
|
+
const context = fenceRecall(utf8Head(parts.join('\n\n'), 14000), 'crowdmemory-codex-plugin');
|
|
139
|
+
await this.store.update(ctx.ref, r => { r.memoryShown = true; r.lastInject = context; });
|
|
140
|
+
return context;
|
|
141
|
+
}
|
|
142
|
+
async end(ctx) {
|
|
143
|
+
const data = await this.store.update(ctx.ref, r => {
|
|
144
|
+
if (r.ended)
|
|
145
|
+
return undefined;
|
|
146
|
+
r.ended = true;
|
|
147
|
+
return r.handle ? { handle: r.handle, seq: ++r.seq } : undefined;
|
|
148
|
+
});
|
|
149
|
+
if (data)
|
|
150
|
+
await this.client.send({ kind: 'session.end', sessionRef: ctx.ref, parentRef: ctx.parentRef,
|
|
151
|
+
nativeKey: `end:${data.seq}`, payload: { session_handle: data.handle, reason: 'other' } });
|
|
152
|
+
}
|
|
153
|
+
async hook(event) {
|
|
154
|
+
const ctx = contextOf(event);
|
|
155
|
+
if (!ctx)
|
|
156
|
+
return undefined;
|
|
157
|
+
const name = text(event['hook_event_name']);
|
|
158
|
+
const output = (context) => context ? { hookSpecificOutput: { hookEventName: name, additionalContext: context } } : undefined;
|
|
159
|
+
if (name === 'PreToolUse' && OWN_TOOL.test(text(event['tool_name']))) {
|
|
160
|
+
// Codex requires allow together with updatedInput. Only our own memory tools are matched.
|
|
161
|
+
return { hookSpecificOutput: { hookEventName: name, permissionDecision: 'allow', updatedInput: {
|
|
162
|
+
...object(event['tool_input']), [SESSION_ARG]: { session_ref: ctx.ref, parent_ref: ctx.parentRef, cwd: ctx.cwd },
|
|
163
|
+
} } };
|
|
164
|
+
}
|
|
165
|
+
if (name === 'SessionStart') {
|
|
166
|
+
const record = await this.store.read(ctx.ref);
|
|
167
|
+
if (text(event['source']) === 'compact')
|
|
168
|
+
return output(record?.lastInject);
|
|
169
|
+
if (record?.ended)
|
|
170
|
+
await this.store.update(ctx.ref, r => { delete r.ended; delete r.handle; delete r.retryAt; delete r.firstStepDone; delete r.lastFailure; delete r.lastInject; delete r.memoryShown; });
|
|
171
|
+
await this.start(ctx);
|
|
172
|
+
await this.store.prune();
|
|
173
|
+
}
|
|
174
|
+
else if (name === 'UserPromptSubmit') {
|
|
175
|
+
if (!(await this.store.read(ctx.ref))?.firstStepDone)
|
|
176
|
+
return output(await this.recall(ctx, text(event['prompt'])));
|
|
177
|
+
}
|
|
178
|
+
else if (name === 'SubagentStart') {
|
|
179
|
+
await this.start(ctx);
|
|
180
|
+
if (ctx.parentRef) {
|
|
181
|
+
await this.store.update(ctx.parentRef, r => { r.children = [...new Set([...(r.children ?? []), ctx.ref])].slice(-64); });
|
|
182
|
+
}
|
|
183
|
+
// Codex does not provide a stable subagent prompt field; do not assign a parent's queued prompt by FIFO.
|
|
184
|
+
return output(await this.recall(ctx));
|
|
185
|
+
}
|
|
186
|
+
else if (name === 'SubagentStop') {
|
|
187
|
+
if (ctx.parentRef) {
|
|
188
|
+
await this.forward({ ...ctx, ref: ctx.parentRef, parentRef: null }, 'subagent.end', { child_ref: ctx.ref, stop_reason: 'other' }, `child:${ctx.ref}`);
|
|
189
|
+
}
|
|
190
|
+
await this.end(ctx);
|
|
191
|
+
}
|
|
192
|
+
else if (name === 'SessionEnd') {
|
|
193
|
+
const record = await this.store.read(ctx.ref);
|
|
194
|
+
await Promise.all((record?.children ?? []).map(ref => this.end({ ref, parentRef: ctx.ref })));
|
|
195
|
+
await this.end(ctx);
|
|
196
|
+
}
|
|
197
|
+
else if (name === 'PostToolUse') {
|
|
198
|
+
const tool = text(event['tool_name']);
|
|
199
|
+
if (OWN_TOOL.test(tool))
|
|
200
|
+
return undefined;
|
|
201
|
+
const args = object(event['tool_input']);
|
|
202
|
+
const result = tool === 'Bash' ? shellResult(event['tool_response']) : undefined;
|
|
203
|
+
if (tool === 'Bash' && !result && typeof event['tool_response'] === 'string') {
|
|
204
|
+
await this.start(ctx);
|
|
205
|
+
await this.store.update(ctx.ref, r => { r.step++; });
|
|
206
|
+
return output(await this.recallOutput(ctx, event['tool_response']));
|
|
207
|
+
}
|
|
208
|
+
if (result) {
|
|
209
|
+
const command = text(args['command']);
|
|
210
|
+
const masked = maskedExit(command, result.output, '');
|
|
211
|
+
const exit = masked?.exitCode ?? result.exit;
|
|
212
|
+
const r = await this.store.read(ctx.ref);
|
|
213
|
+
const replay = replayCommand(command, ctx.cwd);
|
|
214
|
+
if (exit !== 0 || r?.memoryShown) {
|
|
215
|
+
const tail = utf8Tail(masked?.stdout || result.output, 4096);
|
|
216
|
+
const facts = { tool_name: 'bash', error_tail: tail, error_signature: errorSignature(tail) ?? null };
|
|
217
|
+
await this.forward(ctx, 'tool.result', { tool_name: 'bash', ok: exit === 0, exit_code: exit,
|
|
218
|
+
call_ref: text(event['tool_use_id']), ...(replay ? { command: replay } : {}),
|
|
219
|
+
...(exit !== 0 ? { error_tail: facts.error_tail, error_signature: facts.error_signature } : {}) }, `tool:${text(event['tool_use_id'])}`);
|
|
220
|
+
await this.store.update(ctx.ref, current => { current.step++; if (exit !== 0)
|
|
221
|
+
current.lastFailure = facts; });
|
|
222
|
+
if (exit !== 0)
|
|
223
|
+
return output(await this.recall(ctx));
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
else if (tool === 'apply_patch') {
|
|
227
|
+
const response = text(event['tool_response']);
|
|
228
|
+
if (!response.startsWith('Success.'))
|
|
229
|
+
return undefined;
|
|
230
|
+
for (const match of response.matchAll(/^[AMD] (.+)$/gmu)) {
|
|
231
|
+
const path = match[1];
|
|
232
|
+
const rel = ctx.cwd && isAbsolute(path) ? relative(ctx.cwd, path) : path;
|
|
233
|
+
await this.forward(ctx, 'tool.result', { tool_name: 'apply_patch', ok: true, path: codePoints(rel, 1024),
|
|
234
|
+
call_ref: text(event['tool_use_id']) }, `patch:${text(event['tool_use_id'])}:${path}`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return undefined;
|
|
239
|
+
}
|
|
240
|
+
}
|
package/lib/tools.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* memory_search / memory_read / memory_note for Codex, served over MCP. The tool texts, payloads and reports
|
|
3
|
+
* come from the adapter core (`core/memory-tools.ts`, the same as the DSH adapter's); this file only writes them as
|
|
4
|
+
* MCP JSON Schemas and adds the calling session, which the plugin's PreToolUse hook puts into the arguments.
|
|
5
|
+
*/
|
|
6
|
+
import { SESSION_ARG } from './memory.js';
|
|
7
|
+
import { FIX_KINDS, NOTE_RELATIONS, NOTE_SCOPES, NOTE_TYPES, TOOL_TEXT, noteBody, noteReport, readRef, readReport, searchBody, searchReport, } from './core/memory-tools.js';
|
|
8
|
+
import { codePoints } from './core/util.js';
|
|
9
|
+
export { NOTE_RELATIONS, NOTE_SCOPES, NOTE_TYPES, fixOf } from './core/memory-tools.js';
|
|
10
|
+
const isRecord = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
11
|
+
/** The calling session as the PreToolUse hook put it into the arguments. */
|
|
12
|
+
export function sessionOf(args) {
|
|
13
|
+
const value = args[SESSION_ARG];
|
|
14
|
+
if (!isRecord(value) || typeof value['session_ref'] !== 'string' || value['session_ref'] === '')
|
|
15
|
+
return undefined;
|
|
16
|
+
const parent = typeof value['parent_ref'] === 'string' && value['parent_ref'] !== '' ? value['parent_ref'] : null;
|
|
17
|
+
const cwd = typeof value['cwd'] === 'string' && value['cwd'] !== '' ? value['cwd'] : undefined;
|
|
18
|
+
return { ref: codePoints(value['session_ref'], 256), parentRef: parent === null ? null : codePoints(parent, 256), ...(cwd === undefined ? {} : { cwd }) };
|
|
19
|
+
}
|
|
20
|
+
const SESSION_SCHEMA = { type: 'object', description: 'Filled in by the Crowd Memory plugin. Leave it out.' };
|
|
21
|
+
/** MCP validates nothing for us: the note's required fields are checked here (DSH's tool layer does it there). */
|
|
22
|
+
function noteArgs(args) {
|
|
23
|
+
const content = isRecord(args['content']) ? args['content'] : {};
|
|
24
|
+
const fields = ['what', 'why', 'where', 'learned'];
|
|
25
|
+
if (typeof args['title'] !== 'string' || !NOTE_TYPES.includes(args['type']) || !NOTE_SCOPES.includes(args['scope'])
|
|
26
|
+
|| fields.some(key => typeof content[key] !== 'string'))
|
|
27
|
+
return undefined;
|
|
28
|
+
return {
|
|
29
|
+
title: args['title'], type: args['type'], scope: args['scope'],
|
|
30
|
+
content: Object.fromEntries(fields.map(key => [key, content[key]])),
|
|
31
|
+
topic_key: typeof args['topic_key'] === 'string' ? args['topic_key'] : undefined,
|
|
32
|
+
evidence_refs: Array.isArray(args['evidence_refs']) ? args['evidence_refs'].filter((ref) => typeof ref === 'string') : undefined,
|
|
33
|
+
relates_to: Array.isArray(args['relates_to']) ? args['relates_to'] : undefined,
|
|
34
|
+
fix: args['fix'],
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
export function memoryTools(memory) {
|
|
38
|
+
const tools = [{
|
|
39
|
+
name: 'memory_search',
|
|
40
|
+
description: TOOL_TEXT.memory_search.description,
|
|
41
|
+
inputSchema: {
|
|
42
|
+
type: 'object',
|
|
43
|
+
properties: {
|
|
44
|
+
query: { type: 'string', description: TOOL_TEXT.memory_search.query },
|
|
45
|
+
limit: { type: 'integer', description: TOOL_TEXT.memory_search.limit },
|
|
46
|
+
[SESSION_ARG]: SESSION_SCHEMA,
|
|
47
|
+
},
|
|
48
|
+
required: ['query'],
|
|
49
|
+
},
|
|
50
|
+
async execute(args, callKey) {
|
|
51
|
+
const session = sessionOf(args);
|
|
52
|
+
const reply = await memory.forward(session, 'query', searchBody(args['query'], args['limit'], await memory.lastFailureFacts(session)), `memory_search:${callKey}`);
|
|
53
|
+
return searchReport(reply);
|
|
54
|
+
},
|
|
55
|
+
}, {
|
|
56
|
+
name: 'memory_read',
|
|
57
|
+
description: TOOL_TEXT.memory_read.description,
|
|
58
|
+
inputSchema: {
|
|
59
|
+
type: 'object',
|
|
60
|
+
properties: { ref: { type: 'string', description: TOOL_TEXT.memory_read.ref }, [SESSION_ARG]: SESSION_SCHEMA },
|
|
61
|
+
required: ['ref'],
|
|
62
|
+
},
|
|
63
|
+
async execute(args, callKey) {
|
|
64
|
+
const ref = readRef(args['ref']);
|
|
65
|
+
return readReport(await memory.forward(sessionOf(args), 'query', { op: 'read', ref }, `memory_read:${callKey}`), ref);
|
|
66
|
+
},
|
|
67
|
+
}];
|
|
68
|
+
if (memory.config.notes) {
|
|
69
|
+
tools.push({
|
|
70
|
+
name: 'memory_note',
|
|
71
|
+
description: TOOL_TEXT.memory_note.description,
|
|
72
|
+
inputSchema: {
|
|
73
|
+
type: 'object',
|
|
74
|
+
properties: {
|
|
75
|
+
title: { type: 'string', description: TOOL_TEXT.memory_note.title },
|
|
76
|
+
type: { type: 'string', enum: [...NOTE_TYPES] },
|
|
77
|
+
scope: { type: 'string', enum: [...NOTE_SCOPES], description: TOOL_TEXT.memory_note.scope },
|
|
78
|
+
topic_key: { type: 'string', description: TOOL_TEXT.memory_note.topic_key },
|
|
79
|
+
content: {
|
|
80
|
+
type: 'object', additionalProperties: false,
|
|
81
|
+
properties: { what: { type: 'string' }, why: { type: 'string' }, where: { type: 'string' }, learned: { type: 'string' } },
|
|
82
|
+
required: ['what', 'why', 'where', 'learned'],
|
|
83
|
+
},
|
|
84
|
+
evidence_refs: { type: 'array', items: { type: 'string' } },
|
|
85
|
+
relates_to: {
|
|
86
|
+
type: 'array',
|
|
87
|
+
items: {
|
|
88
|
+
type: 'object', additionalProperties: false,
|
|
89
|
+
properties: { record_ref: { type: 'string' }, relation: { type: 'string', enum: [...NOTE_RELATIONS] } },
|
|
90
|
+
required: ['record_ref', 'relation'],
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
fix: {
|
|
94
|
+
type: 'object', additionalProperties: false, description: TOOL_TEXT.memory_note.fix,
|
|
95
|
+
properties: {
|
|
96
|
+
kind: { type: 'string', enum: [...FIX_KINDS] },
|
|
97
|
+
command: { type: 'string' }, diff: { type: 'string' }, path: { type: 'string' }, content: { type: 'string' },
|
|
98
|
+
},
|
|
99
|
+
required: ['kind'],
|
|
100
|
+
},
|
|
101
|
+
[SESSION_ARG]: SESSION_SCHEMA,
|
|
102
|
+
},
|
|
103
|
+
required: ['title', 'type', 'scope', 'content'],
|
|
104
|
+
},
|
|
105
|
+
async execute(args, callKey) {
|
|
106
|
+
const note = noteArgs(args);
|
|
107
|
+
if (note === undefined)
|
|
108
|
+
return 'memory request refused (schema_invalid): memory_note needs title, type, scope and content.what/why/where/learned';
|
|
109
|
+
return noteReport(await memory.forward(sessionOf(args), 'note', noteBody(note), `memory_note:${callKey}`));
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
return tools;
|
|
114
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,36 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fuxi-ailabs/crowdmemory-codex-plugin",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Codex hooks and MCP adapter for the local Crowd Memory Runtime",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"author": "cacheline <cacheline999@gmail.com>",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"engines": {
|
|
9
|
+
"node": ">=22.18.0"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
".codex-plugin/plugin.json",
|
|
13
|
+
".agents/plugins/marketplace.json",
|
|
14
|
+
"hooks/hooks.json",
|
|
15
|
+
".mcp.json",
|
|
16
|
+
"lib/**/*.js",
|
|
17
|
+
"contract/**",
|
|
18
|
+
"README.md",
|
|
19
|
+
"LICENSE"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsc -p tsconfig.json",
|
|
23
|
+
"typecheck": "tsc -p tsconfig.test.json",
|
|
24
|
+
"test": "node --import tsx --test tests/*.test.ts"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@types/node": "22.20.0",
|
|
28
|
+
"ajv": "8.17.1",
|
|
29
|
+
"typescript": "6.0.3",
|
|
30
|
+
"tsx": "4.22.4"
|
|
31
|
+
},
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public"
|
|
34
|
+
},
|
|
35
|
+
"packageManager": "pnpm@12.8.1+sha512.f64ba907507f5ceafe06c8d38e6052d0179444580ec1279ddd5bfc11cb48aa8a2644b66598e07e761da84872a9fc57d5f902b87fa49d024198d558612aabbe45"
|
|
36
|
+
}
|