@clawmem-ai/clawmem 0.1.18 → 0.1.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 +28 -9
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4 -0
- package/dist/src/collaboration.d.ts +49 -0
- package/dist/src/collaboration.js +69 -0
- package/dist/src/config.d.ts +21 -0
- package/dist/src/config.js +119 -0
- package/dist/src/conversation.d.ts +30 -0
- package/dist/src/conversation.js +323 -0
- package/dist/src/github-client.d.ts +269 -0
- package/dist/src/github-client.js +350 -0
- package/dist/src/keyed-async-queue.d.ts +12 -0
- package/dist/src/keyed-async-queue.js +23 -0
- package/dist/src/memory.d.ts +29 -0
- package/dist/src/memory.js +451 -0
- package/dist/src/recall-sanitize.d.ts +1 -0
- package/dist/src/recall-sanitize.js +149 -0
- package/dist/src/runtime-env.d.ts +2 -0
- package/dist/src/runtime-env.js +12 -0
- package/dist/src/service.d.ts +18 -0
- package/dist/src/service.js +3645 -0
- package/dist/src/state.d.ts +4 -0
- package/dist/src/state.js +182 -0
- package/dist/src/transcript.d.ts +3 -0
- package/dist/src/transcript.js +164 -0
- package/dist/src/types.d.ts +130 -0
- package/dist/src/types.js +1 -0
- package/dist/src/utils.d.ts +26 -0
- package/dist/src/utils.js +62 -0
- package/dist/src/yaml.d.ts +2 -0
- package/dist/src/yaml.js +81 -0
- package/openclaw.plugin.json +14 -1
- package/package.json +21 -7
- package/skills/clawmem/SKILL.md +26 -5
- package/skills/clawmem/references/collaboration.md +13 -5
- package/skills/clawmem/references/review.md +77 -0
- package/skills/clawmem/references/schema.md +44 -1
- package/index.ts +0 -6
- package/src/collaboration.test.ts +0 -71
- package/src/collaboration.ts +0 -109
- package/src/config.test.ts +0 -83
- package/src/config.ts +0 -117
- package/src/conversation.test.ts +0 -120
- package/src/conversation.ts +0 -304
- package/src/github-client.test.ts +0 -101
- package/src/github-client.ts +0 -363
- package/src/keyed-async-queue.ts +0 -26
- package/src/memory.test.ts +0 -588
- package/src/memory.ts +0 -444
- package/src/recall-sanitize.ts +0 -143
- package/src/runtime-env.ts +0 -12
- package/src/service.test.ts +0 -337
- package/src/service.ts +0 -2786
- package/src/state.test.ts +0 -119
- package/src/state.ts +0 -206
- package/src/transcript.ts +0 -186
- package/src/types.ts +0 -86
- package/src/utils.ts +0 -74
- package/src/yaml.ts +0 -88
- package/tsconfig.json +0 -15
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { PluginState } from "./types.js";
|
|
2
|
+
export declare function resolveStatePath(stateDir: string): string;
|
|
3
|
+
export declare function loadState(filePath: string): Promise<PluginState>;
|
|
4
|
+
export declare function saveState(filePath: string, state: PluginState): Promise<void>;
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { normalizeAgentId, sessionScopeKey } from "./utils.js";
|
|
4
|
+
const EMPTY_STATE = {
|
|
5
|
+
version: 4,
|
|
6
|
+
sessions: {},
|
|
7
|
+
};
|
|
8
|
+
export function resolveStatePath(stateDir) {
|
|
9
|
+
return path.join(stateDir, "clawmem", "state.json");
|
|
10
|
+
}
|
|
11
|
+
export async function loadState(filePath) {
|
|
12
|
+
try {
|
|
13
|
+
const raw = await fs.promises.readFile(filePath, "utf8");
|
|
14
|
+
return sanitizeState(JSON.parse(raw));
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
const code = error.code;
|
|
18
|
+
if (code === "ENOENT") {
|
|
19
|
+
return structuredClone(EMPTY_STATE);
|
|
20
|
+
}
|
|
21
|
+
return structuredClone(EMPTY_STATE);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export async function saveState(filePath, state) {
|
|
25
|
+
await fs.promises.mkdir(path.dirname(filePath), { recursive: true, mode: 0o700 });
|
|
26
|
+
const next = JSON.stringify(state, null, 2) + "\n";
|
|
27
|
+
const tmpPath = `${filePath}.tmp-${process.pid}-${Date.now()}`;
|
|
28
|
+
await fs.promises.writeFile(tmpPath, next, { encoding: "utf8", mode: 0o600 });
|
|
29
|
+
await fs.promises.rename(tmpPath, filePath);
|
|
30
|
+
}
|
|
31
|
+
function sanitizeState(value) {
|
|
32
|
+
if (!value || typeof value !== "object") {
|
|
33
|
+
return structuredClone(EMPTY_STATE);
|
|
34
|
+
}
|
|
35
|
+
const raw = value;
|
|
36
|
+
const sessions = raw.sessions && typeof raw.sessions === "object"
|
|
37
|
+
? raw.sessions
|
|
38
|
+
: {};
|
|
39
|
+
const migrations = {};
|
|
40
|
+
if (raw.migrations && typeof raw.migrations === "object") {
|
|
41
|
+
for (const [k, v] of Object.entries(raw.migrations)) {
|
|
42
|
+
const s = readString(v);
|
|
43
|
+
if (s)
|
|
44
|
+
migrations[k] = s;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const out = {
|
|
48
|
+
version: 4,
|
|
49
|
+
sessions: {},
|
|
50
|
+
...(Object.keys(migrations).length > 0 ? { migrations } : {}),
|
|
51
|
+
};
|
|
52
|
+
for (const [storedKey, sessionValue] of Object.entries(sessions)) {
|
|
53
|
+
if (!sessionValue || typeof sessionValue !== "object" || !storedKey.trim())
|
|
54
|
+
continue;
|
|
55
|
+
const rawSession = sessionValue;
|
|
56
|
+
const sessionId = readString(rawSession.sessionId) ?? storedKey.trim();
|
|
57
|
+
if (!sessionId)
|
|
58
|
+
continue;
|
|
59
|
+
const agentId = normalizeAgentId(readString(rawSession.agentId));
|
|
60
|
+
const lastMirroredCount = readNumber(rawSession.lastMirroredCount) ?? 0;
|
|
61
|
+
const finalizedAt = readString(rawSession.finalizedAt);
|
|
62
|
+
const derived = sanitizeDerivedState(rawSession, lastMirroredCount, finalizedAt);
|
|
63
|
+
out.sessions[sessionScopeKey(sessionId, agentId)] = {
|
|
64
|
+
sessionId,
|
|
65
|
+
sessionKey: readString(rawSession.sessionKey),
|
|
66
|
+
sessionFile: readString(rawSession.sessionFile),
|
|
67
|
+
agentId,
|
|
68
|
+
issueNumber: readNumber(rawSession.issueNumber),
|
|
69
|
+
issueTitle: readString(rawSession.issueTitle),
|
|
70
|
+
titleSource: readTitleSource(rawSession.titleSource),
|
|
71
|
+
lastMirroredCount,
|
|
72
|
+
turnCount: readNumber(rawSession.turnCount) ?? 0,
|
|
73
|
+
finalizedAt,
|
|
74
|
+
lastSummaryHash: readString(rawSession.lastSummaryHash),
|
|
75
|
+
derived,
|
|
76
|
+
createdAt: readString(rawSession.createdAt),
|
|
77
|
+
updatedAt: readString(rawSession.updatedAt),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
function sanitizeDerivedState(rawSession, lastMirroredCount, finalizedAt) {
|
|
83
|
+
const rawDerived = asRecord(rawSession.derived);
|
|
84
|
+
const rawSummary = asRecord(rawDerived?.summary);
|
|
85
|
+
const rawMemory = asRecord(rawDerived?.memory);
|
|
86
|
+
const legacySummaryStatus = readEnum(rawSession.summaryStatus, ["pending", "complete"]);
|
|
87
|
+
const legacyMemoryCursor = readNumber(rawSession.lastMemorySyncCount);
|
|
88
|
+
const summaryText = readString(rawSummary?.text);
|
|
89
|
+
const summaryTitle = readString(rawSummary?.title);
|
|
90
|
+
const summaryStatus = readTaskStatus(rawSummary?.status, summaryText || legacySummaryStatus === "complete" ? "complete" : "idle");
|
|
91
|
+
const summaryCursor = clampCursor(readNumber(rawSummary?.basedOnCursor), summaryStatus === "complete" ? lastMirroredCount : 0, lastMirroredCount);
|
|
92
|
+
const capturedCursor = clampCursor(readNumber(rawMemory?.capturedCursor)
|
|
93
|
+
?? readNumber(rawMemory?.appliedCursor)
|
|
94
|
+
?? legacyMemoryCursor, summaryStatus === "complete" ? lastMirroredCount : 0, lastMirroredCount);
|
|
95
|
+
const memoryStatus = readTaskStatus(rawMemory?.status ?? rawMemory?.extractStatus ?? rawMemory?.reconcileStatus, capturedCursor >= lastMirroredCount && lastMirroredCount > 0 ? "complete" : "idle");
|
|
96
|
+
const candidates = readMemoryCandidates(rawMemory?.candidates);
|
|
97
|
+
return {
|
|
98
|
+
summary: {
|
|
99
|
+
basedOnCursor: summaryCursor,
|
|
100
|
+
status: finalizedAt && summaryStatus === "idle" && lastMirroredCount > 0 ? "error" : summaryStatus,
|
|
101
|
+
...(summaryText ? { text: summaryText } : {}),
|
|
102
|
+
...(summaryTitle ? { title: summaryTitle } : {}),
|
|
103
|
+
...(readString(rawSummary?.lastError) ? { lastError: readString(rawSummary?.lastError) } : {}),
|
|
104
|
+
...(readString(rawSummary?.updatedAt) ? { updatedAt: readString(rawSummary?.updatedAt) } : {}),
|
|
105
|
+
},
|
|
106
|
+
memory: {
|
|
107
|
+
capturedCursor,
|
|
108
|
+
status: memoryStatus,
|
|
109
|
+
...(candidates ? { candidates } : {}),
|
|
110
|
+
...(readString(rawMemory?.lastError) ? { lastError: readString(rawMemory?.lastError) } : {}),
|
|
111
|
+
...(readString(rawMemory?.updatedAt) ? { updatedAt: readString(rawMemory?.updatedAt) } : {}),
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
function readString(value) {
|
|
116
|
+
if (typeof value !== "string")
|
|
117
|
+
return undefined;
|
|
118
|
+
const trimmed = value.trim();
|
|
119
|
+
return trimmed ? trimmed : undefined;
|
|
120
|
+
}
|
|
121
|
+
function readEnum(value, allowed) {
|
|
122
|
+
const s = readString(value);
|
|
123
|
+
return s && allowed.includes(s) ? s : undefined;
|
|
124
|
+
}
|
|
125
|
+
function readNumber(value) {
|
|
126
|
+
if (typeof value !== "number" || !Number.isFinite(value))
|
|
127
|
+
return undefined;
|
|
128
|
+
return Math.max(0, Math.floor(value));
|
|
129
|
+
}
|
|
130
|
+
function readTaskStatus(value, fallback) {
|
|
131
|
+
const status = readEnum(value, ["idle", "pending", "running", "complete", "error"]);
|
|
132
|
+
if (!status)
|
|
133
|
+
return fallback;
|
|
134
|
+
if (status === "pending" || status === "running")
|
|
135
|
+
return "idle";
|
|
136
|
+
return status;
|
|
137
|
+
}
|
|
138
|
+
function readTitleSource(value) {
|
|
139
|
+
const source = readEnum(value, ["placeholder", "digest", "llm"]);
|
|
140
|
+
if (!source)
|
|
141
|
+
return undefined;
|
|
142
|
+
return source === "digest" ? "llm" : source;
|
|
143
|
+
}
|
|
144
|
+
function readMemoryCandidates(value) {
|
|
145
|
+
if (!Array.isArray(value))
|
|
146
|
+
return undefined;
|
|
147
|
+
const out = value
|
|
148
|
+
.map((entry) => sanitizeMemoryCandidate(entry))
|
|
149
|
+
.filter((candidate) => candidate !== null);
|
|
150
|
+
return out;
|
|
151
|
+
}
|
|
152
|
+
function sanitizeMemoryCandidate(value) {
|
|
153
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
154
|
+
return null;
|
|
155
|
+
const record = value;
|
|
156
|
+
const candidateId = readString(record.candidateId);
|
|
157
|
+
const detail = readString(record.detail);
|
|
158
|
+
if (!candidateId || !detail)
|
|
159
|
+
return null;
|
|
160
|
+
const title = readString(record.title);
|
|
161
|
+
const kind = readString(record.kind);
|
|
162
|
+
const topics = Array.isArray(record.topics)
|
|
163
|
+
? record.topics.map((topic) => readString(topic)).filter((topic) => Boolean(topic))
|
|
164
|
+
: undefined;
|
|
165
|
+
const evidence = readString(record.evidence);
|
|
166
|
+
return {
|
|
167
|
+
candidateId,
|
|
168
|
+
detail,
|
|
169
|
+
...(title ? { title } : {}),
|
|
170
|
+
...(kind ? { kind } : {}),
|
|
171
|
+
...(topics && topics.length > 0 ? { topics } : {}),
|
|
172
|
+
...(evidence ? { evidence } : {}),
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
function clampCursor(value, fallback, max) {
|
|
176
|
+
if (typeof value !== "number" || !Number.isFinite(value))
|
|
177
|
+
return Math.min(max, Math.max(0, fallback));
|
|
178
|
+
return Math.min(max, Math.max(0, Math.floor(value)));
|
|
179
|
+
}
|
|
180
|
+
function asRecord(value) {
|
|
181
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
182
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
export async function readTranscriptSnapshot(filePath) {
|
|
3
|
+
const raw = await fs.promises.readFile(filePath, "utf8");
|
|
4
|
+
const lines = raw
|
|
5
|
+
.split(/\r?\n/)
|
|
6
|
+
.map((line) => line.trim())
|
|
7
|
+
.filter((line) => line.length > 0);
|
|
8
|
+
let sessionId;
|
|
9
|
+
const messages = [];
|
|
10
|
+
for (const line of lines) {
|
|
11
|
+
let parsed;
|
|
12
|
+
try {
|
|
13
|
+
parsed = JSON.parse(line);
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
const record = asRecord(parsed);
|
|
19
|
+
if (!record) {
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
if (!sessionId && record.type === "session" && typeof record.id === "string") {
|
|
23
|
+
sessionId = record.id.trim() || undefined;
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
const message = normalizeMessage(record.message ?? record);
|
|
27
|
+
if (message) {
|
|
28
|
+
messages.push(message);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return { sessionId, messages };
|
|
32
|
+
}
|
|
33
|
+
export function normalizeMessages(items) {
|
|
34
|
+
const out = [];
|
|
35
|
+
for (const item of items) {
|
|
36
|
+
const record = asRecord(item);
|
|
37
|
+
if (!record) {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
const message = normalizeMessage(record.message ?? record);
|
|
41
|
+
if (message) {
|
|
42
|
+
out.push(message);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
function normalizeMessage(value) {
|
|
48
|
+
const record = asRecord(value);
|
|
49
|
+
if (!record) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
const role = typeof record.role === "string" ? record.role : undefined;
|
|
53
|
+
if (role !== "assistant" && role !== "user") {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
if (record.tool_call_id || record.toolCallId) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
const directText = typeof record.text === "string" ? normalizeChatText(record.text) : null;
|
|
60
|
+
const text = directText ?? extractChatText(record.content);
|
|
61
|
+
if (!text) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
const timestamp = normalizeTimestamp(record.timestamp);
|
|
65
|
+
const stopReason = typeof record.stopReason === "string" ? record.stopReason : undefined;
|
|
66
|
+
return {
|
|
67
|
+
role,
|
|
68
|
+
text,
|
|
69
|
+
...(timestamp ? { timestamp } : {}),
|
|
70
|
+
...(stopReason ? { stopReason } : {}),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function extractChatText(content) {
|
|
74
|
+
if (typeof content === "string") {
|
|
75
|
+
return normalizeChatText(content) ?? "";
|
|
76
|
+
}
|
|
77
|
+
if (!Array.isArray(content)) {
|
|
78
|
+
return "";
|
|
79
|
+
}
|
|
80
|
+
const parts = [];
|
|
81
|
+
for (const block of content) {
|
|
82
|
+
if (typeof block === "string") {
|
|
83
|
+
const normalized = normalizeChatText(block);
|
|
84
|
+
if (normalized) {
|
|
85
|
+
parts.push(normalized);
|
|
86
|
+
}
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
const record = asRecord(block);
|
|
90
|
+
if (!record) {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
const type = typeof record.type === "string" ? record.type.toLowerCase() : "";
|
|
94
|
+
if (type.includes("tool")) {
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
const textCandidates = [
|
|
98
|
+
record.text,
|
|
99
|
+
record.value,
|
|
100
|
+
record.content,
|
|
101
|
+
record.outputText,
|
|
102
|
+
record.inputText,
|
|
103
|
+
];
|
|
104
|
+
for (const candidate of textCandidates) {
|
|
105
|
+
if (typeof candidate === "string") {
|
|
106
|
+
const normalized = normalizeChatText(candidate);
|
|
107
|
+
if (normalized) {
|
|
108
|
+
parts.push(normalized);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return compactText(parts);
|
|
114
|
+
}
|
|
115
|
+
function normalizeChatText(value) {
|
|
116
|
+
let trimmed = stripUntrustedMetadataPrefixes(squashWhitespace(value));
|
|
117
|
+
if (!trimmed) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
trimmed = trimmed.replace(/^\[\[\s*reply_to[^\]]*\]\]\s*/i, "").trim();
|
|
121
|
+
if (!trimmed) {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
const upper = trimmed.toUpperCase();
|
|
125
|
+
if (upper === "NO_REPLY" || upper === "HEARTBEAT_OK" || upper === "IDLE-CHAT") {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
return trimmed;
|
|
129
|
+
}
|
|
130
|
+
function normalizeTimestamp(value) {
|
|
131
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
132
|
+
return new Date(value).toISOString();
|
|
133
|
+
}
|
|
134
|
+
if (typeof value === "string") {
|
|
135
|
+
const trimmed = value.trim();
|
|
136
|
+
return trimmed || undefined;
|
|
137
|
+
}
|
|
138
|
+
return undefined;
|
|
139
|
+
}
|
|
140
|
+
function compactText(parts) {
|
|
141
|
+
return parts
|
|
142
|
+
.map((part) => (typeof part === "string" ? part.trim() : ""))
|
|
143
|
+
.filter((part) => part.length > 0)
|
|
144
|
+
.join("\n\n");
|
|
145
|
+
}
|
|
146
|
+
function squashWhitespace(value) {
|
|
147
|
+
return value.replace(/\r/g, "").replace(/\n{3,}/g, "\n\n").trim();
|
|
148
|
+
}
|
|
149
|
+
function stripUntrustedMetadataPrefixes(value) {
|
|
150
|
+
let current = value.trim();
|
|
151
|
+
for (;;) {
|
|
152
|
+
const next = current
|
|
153
|
+
.replace(/^Conversation info \(untrusted metadata\):\s*```(?:json)?\s*[\s\S]*?```\s*/i, "")
|
|
154
|
+
.replace(/^Sender \(untrusted metadata\):\s*```(?:json)?\s*[\s\S]*?```\s*/i, "")
|
|
155
|
+
.trim();
|
|
156
|
+
if (next === current) {
|
|
157
|
+
return current;
|
|
158
|
+
}
|
|
159
|
+
current = next;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
function asRecord(value) {
|
|
163
|
+
return value && typeof value === "object" ? value : null;
|
|
164
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
export type ClawMemAgentConfig = {
|
|
2
|
+
baseUrl?: string;
|
|
3
|
+
login?: string;
|
|
4
|
+
defaultRepo?: string;
|
|
5
|
+
repo?: string;
|
|
6
|
+
token?: string;
|
|
7
|
+
authScheme?: "token" | "bearer";
|
|
8
|
+
};
|
|
9
|
+
export type ClawMemPluginConfig = {
|
|
10
|
+
baseUrl: string;
|
|
11
|
+
login?: string;
|
|
12
|
+
defaultRepo?: string;
|
|
13
|
+
repo?: string;
|
|
14
|
+
token?: string;
|
|
15
|
+
authScheme: "token" | "bearer";
|
|
16
|
+
agents: Record<string, ClawMemAgentConfig>;
|
|
17
|
+
memoryRecallLimit: number;
|
|
18
|
+
memoryAutoRecallLimit: number;
|
|
19
|
+
summaryWaitTimeoutMs: number;
|
|
20
|
+
memoryExtractWaitTimeoutMs: number;
|
|
21
|
+
reviewNudgeInterval: number;
|
|
22
|
+
};
|
|
23
|
+
export type ClawMemResolvedRoute = {
|
|
24
|
+
agentId: string;
|
|
25
|
+
baseUrl: string;
|
|
26
|
+
login?: string;
|
|
27
|
+
defaultRepo?: string;
|
|
28
|
+
repo?: string;
|
|
29
|
+
token?: string;
|
|
30
|
+
authScheme: "token" | "bearer";
|
|
31
|
+
};
|
|
32
|
+
export type BootstrapIdentityResponse = {
|
|
33
|
+
token: string;
|
|
34
|
+
repo_full_name: string;
|
|
35
|
+
};
|
|
36
|
+
export type AgentRegistrationResponse = BootstrapIdentityResponse & {
|
|
37
|
+
login: string;
|
|
38
|
+
};
|
|
39
|
+
export type AnonymousSessionResponse = BootstrapIdentityResponse & {
|
|
40
|
+
owner_login: string;
|
|
41
|
+
repo_name: string;
|
|
42
|
+
};
|
|
43
|
+
export type SessionTaskStatus = "idle" | "complete" | "error";
|
|
44
|
+
export type MemoryCandidate = {
|
|
45
|
+
candidateId: string;
|
|
46
|
+
detail: string;
|
|
47
|
+
title?: string;
|
|
48
|
+
kind?: string;
|
|
49
|
+
topics?: string[];
|
|
50
|
+
evidence?: string;
|
|
51
|
+
};
|
|
52
|
+
export type SessionSummaryState = {
|
|
53
|
+
basedOnCursor: number;
|
|
54
|
+
status: SessionTaskStatus;
|
|
55
|
+
text?: string;
|
|
56
|
+
title?: string;
|
|
57
|
+
lastError?: string;
|
|
58
|
+
updatedAt?: string;
|
|
59
|
+
};
|
|
60
|
+
export type SessionMemoryState = {
|
|
61
|
+
capturedCursor: number;
|
|
62
|
+
status: SessionTaskStatus;
|
|
63
|
+
candidates?: MemoryCandidate[];
|
|
64
|
+
lastError?: string;
|
|
65
|
+
updatedAt?: string;
|
|
66
|
+
};
|
|
67
|
+
export type SessionDerivedState = {
|
|
68
|
+
summary: SessionSummaryState;
|
|
69
|
+
memory: SessionMemoryState;
|
|
70
|
+
};
|
|
71
|
+
export type SessionMirrorState = {
|
|
72
|
+
sessionId: string;
|
|
73
|
+
sessionKey?: string;
|
|
74
|
+
sessionFile?: string;
|
|
75
|
+
agentId?: string;
|
|
76
|
+
issueNumber?: number;
|
|
77
|
+
issueTitle?: string;
|
|
78
|
+
titleSource?: "placeholder" | "llm";
|
|
79
|
+
lastMirroredCount: number;
|
|
80
|
+
turnCount: number;
|
|
81
|
+
turnsSinceReview?: number;
|
|
82
|
+
finalizedAt?: string;
|
|
83
|
+
lastSummaryHash?: string;
|
|
84
|
+
derived?: SessionDerivedState;
|
|
85
|
+
createdAt?: string;
|
|
86
|
+
updatedAt?: string;
|
|
87
|
+
};
|
|
88
|
+
export type PluginState = {
|
|
89
|
+
version: 4;
|
|
90
|
+
sessions: Record<string, SessionMirrorState>;
|
|
91
|
+
migrations?: Record<string, string>;
|
|
92
|
+
};
|
|
93
|
+
export type NormalizedMessage = {
|
|
94
|
+
role: string;
|
|
95
|
+
text: string;
|
|
96
|
+
toolName?: string;
|
|
97
|
+
timestamp?: string;
|
|
98
|
+
stopReason?: string;
|
|
99
|
+
};
|
|
100
|
+
export type TranscriptSnapshot = {
|
|
101
|
+
sessionId?: string;
|
|
102
|
+
messages: NormalizedMessage[];
|
|
103
|
+
};
|
|
104
|
+
export type MemoryDraft = {
|
|
105
|
+
title?: string;
|
|
106
|
+
detail: string;
|
|
107
|
+
kind?: string;
|
|
108
|
+
topics?: string[];
|
|
109
|
+
};
|
|
110
|
+
export type MemorySchema = {
|
|
111
|
+
kinds: string[];
|
|
112
|
+
topics: string[];
|
|
113
|
+
};
|
|
114
|
+
export type MemoryListOptions = {
|
|
115
|
+
status?: "active" | "stale" | "all";
|
|
116
|
+
kind?: string;
|
|
117
|
+
topic?: string;
|
|
118
|
+
limit?: number;
|
|
119
|
+
};
|
|
120
|
+
export type ParsedMemoryIssue = {
|
|
121
|
+
issueNumber: number;
|
|
122
|
+
title: string;
|
|
123
|
+
memoryId: string;
|
|
124
|
+
memoryHash?: string;
|
|
125
|
+
date: string;
|
|
126
|
+
detail: string;
|
|
127
|
+
kind?: string;
|
|
128
|
+
topics?: string[];
|
|
129
|
+
status: "active" | "stale";
|
|
130
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { NormalizedMessage } from "./types.js";
|
|
2
|
+
export declare const DEFAULT_AGENT_ID = "main";
|
|
3
|
+
export declare const DEFAULT_BOOTSTRAP_REPO_NAME = "memory";
|
|
4
|
+
export declare function sha256(v: string): string;
|
|
5
|
+
export declare function normalizeAgentId(value: string | undefined | null): string;
|
|
6
|
+
export declare function normalizeLoginName(value: string | undefined | null): string | undefined;
|
|
7
|
+
export declare function buildAgentBootstrapRegistration(agentId: string): {
|
|
8
|
+
prefixLogin: string;
|
|
9
|
+
defaultRepoName: string;
|
|
10
|
+
};
|
|
11
|
+
export declare function sessionScopeKey(sessionId: string, agentId?: string): string;
|
|
12
|
+
export declare function inferAgentIdFromTranscriptPath(filePath: string): string | undefined;
|
|
13
|
+
export declare function subKey(s: {
|
|
14
|
+
sessionId: string;
|
|
15
|
+
agentId?: string;
|
|
16
|
+
}, suffix: string): string;
|
|
17
|
+
export declare function fmtTranscript(msgs: NormalizedMessage[]): string;
|
|
18
|
+
export declare function fmtTranscriptFrom(msgs: NormalizedMessage[], startIndex: number): string;
|
|
19
|
+
export declare function sliceTranscriptDelta(msgs: NormalizedMessage[], fromIndex: number, anchorCount?: number): {
|
|
20
|
+
anchorStart: number;
|
|
21
|
+
deltaStart: number;
|
|
22
|
+
anchorMessages: NormalizedMessage[];
|
|
23
|
+
deltaMessages: NormalizedMessage[];
|
|
24
|
+
};
|
|
25
|
+
export declare function localDate(d?: Date): string;
|
|
26
|
+
export declare function localDateTime(d: Date): string;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// Shared utility helpers used by memory.ts and conversation.ts.
|
|
2
|
+
import crypto from "node:crypto";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
export const DEFAULT_AGENT_ID = "main";
|
|
5
|
+
export const DEFAULT_BOOTSTRAP_REPO_NAME = "memory";
|
|
6
|
+
const MAX_AGENT_LOGIN_PREFIX_LEN = 32;
|
|
7
|
+
export function sha256(v) { return crypto.createHash("sha256").update(v).digest("hex"); }
|
|
8
|
+
export function normalizeAgentId(value) {
|
|
9
|
+
const trimmed = (value ?? "").trim().toLowerCase();
|
|
10
|
+
if (!trimmed)
|
|
11
|
+
return DEFAULT_AGENT_ID;
|
|
12
|
+
return trimmed.replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 64) || DEFAULT_AGENT_ID;
|
|
13
|
+
}
|
|
14
|
+
export function normalizeLoginName(value) {
|
|
15
|
+
const trimmed = (value ?? "").trim().toLowerCase();
|
|
16
|
+
return trimmed || undefined;
|
|
17
|
+
}
|
|
18
|
+
export function buildAgentBootstrapRegistration(agentId) {
|
|
19
|
+
const prefixLogin = normalizeAgentId(agentId)
|
|
20
|
+
.replace(/_/g, "-")
|
|
21
|
+
.replace(/-+/g, "-")
|
|
22
|
+
.replace(/^-+|-+$/g, "")
|
|
23
|
+
.slice(0, MAX_AGENT_LOGIN_PREFIX_LEN)
|
|
24
|
+
.replace(/-+$/g, "") || DEFAULT_AGENT_ID;
|
|
25
|
+
return { prefixLogin, defaultRepoName: DEFAULT_BOOTSTRAP_REPO_NAME };
|
|
26
|
+
}
|
|
27
|
+
export function sessionScopeKey(sessionId, agentId) {
|
|
28
|
+
return `${normalizeAgentId(agentId)}:${sessionId.trim()}`;
|
|
29
|
+
}
|
|
30
|
+
export function inferAgentIdFromTranscriptPath(filePath) {
|
|
31
|
+
const parts = path.resolve(filePath).split(path.sep);
|
|
32
|
+
const idx = parts.lastIndexOf("agents");
|
|
33
|
+
if (idx < 0 || !parts[idx + 1] || parts[idx + 2] !== "sessions")
|
|
34
|
+
return undefined;
|
|
35
|
+
return normalizeAgentId(parts[idx + 1]);
|
|
36
|
+
}
|
|
37
|
+
export function subKey(s, suffix) {
|
|
38
|
+
const san = (v) => v.replace(/[^a-zA-Z0-9_-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48) || "main";
|
|
39
|
+
return `agent:${san(s.agentId || "main")}:subagent:clawmem-${suffix}-${san(s.sessionId)}`;
|
|
40
|
+
}
|
|
41
|
+
export function fmtTranscript(msgs) {
|
|
42
|
+
return msgs.map((m, i) => `${i + 1}. ${m.role === "assistant" ? "assistant" : "user"}: ${m.text}`).join("\n\n");
|
|
43
|
+
}
|
|
44
|
+
export function fmtTranscriptFrom(msgs, startIndex) {
|
|
45
|
+
return msgs.map((m, i) => `${startIndex + i + 1}. ${m.role === "assistant" ? "assistant" : "user"}: ${m.text}`).join("\n\n");
|
|
46
|
+
}
|
|
47
|
+
export function sliceTranscriptDelta(msgs, fromIndex, anchorCount = 2) {
|
|
48
|
+
const deltaStart = Math.min(Math.max(0, Math.floor(fromIndex)), msgs.length);
|
|
49
|
+
const anchorStart = Math.max(0, deltaStart - Math.max(0, Math.floor(anchorCount)));
|
|
50
|
+
return {
|
|
51
|
+
anchorStart,
|
|
52
|
+
deltaStart,
|
|
53
|
+
anchorMessages: msgs.slice(anchorStart, deltaStart),
|
|
54
|
+
deltaMessages: msgs.slice(deltaStart),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
export function localDate(d = new Date()) {
|
|
58
|
+
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
59
|
+
}
|
|
60
|
+
export function localDateTime(d) {
|
|
61
|
+
return `${localDate(d)}T${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}:${String(d.getSeconds()).padStart(2, "0")}`;
|
|
62
|
+
}
|
package/dist/src/yaml.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
export function stringifyFlatYaml(entries) {
|
|
2
|
+
const out = [];
|
|
3
|
+
for (const [key, rawValue] of entries) {
|
|
4
|
+
const value = rawValue ?? "";
|
|
5
|
+
if (value.includes("\n")) {
|
|
6
|
+
out.push(`${key}: |-`);
|
|
7
|
+
for (const line of value.split("\n")) {
|
|
8
|
+
out.push(` ${line}`);
|
|
9
|
+
}
|
|
10
|
+
continue;
|
|
11
|
+
}
|
|
12
|
+
out.push(`${key}: ${formatScalar(value)}`);
|
|
13
|
+
}
|
|
14
|
+
return out.join("\n");
|
|
15
|
+
}
|
|
16
|
+
export function parseFlatYaml(input) {
|
|
17
|
+
const result = {};
|
|
18
|
+
const lines = input.replace(/\r/g, "").split("\n");
|
|
19
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
20
|
+
const line = lines[index] ?? "";
|
|
21
|
+
if (!line.trim()) {
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
const match = /^([A-Za-z0-9_]+):(?:\s(.*))?$/.exec(line);
|
|
25
|
+
if (!match) {
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
const key = match[1];
|
|
29
|
+
const rawValue = match[2] ?? "";
|
|
30
|
+
if (rawValue === "|-" || rawValue === "|") {
|
|
31
|
+
const block = [];
|
|
32
|
+
let cursor = index + 1;
|
|
33
|
+
while (cursor < lines.length) {
|
|
34
|
+
const blockLine = lines[cursor] ?? "";
|
|
35
|
+
if (blockLine.startsWith(" ")) {
|
|
36
|
+
block.push(blockLine.slice(2));
|
|
37
|
+
cursor += 1;
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (!blockLine.trim()) {
|
|
41
|
+
block.push("");
|
|
42
|
+
cursor += 1;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
result[key] = block.join("\n");
|
|
48
|
+
index = cursor - 1;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
result[key] = parseScalar(rawValue.trim());
|
|
52
|
+
}
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
function formatScalar(value) {
|
|
56
|
+
if (value.length === 0) {
|
|
57
|
+
return '""';
|
|
58
|
+
}
|
|
59
|
+
if (/^[A-Za-z0-9_./:@ -]+$/.test(value) && !looksLikeYamlKeyword(value)) {
|
|
60
|
+
return value;
|
|
61
|
+
}
|
|
62
|
+
return JSON.stringify(value);
|
|
63
|
+
}
|
|
64
|
+
function parseScalar(value) {
|
|
65
|
+
if (!value) {
|
|
66
|
+
return "";
|
|
67
|
+
}
|
|
68
|
+
if (value.startsWith('"') && value.endsWith('"')) {
|
|
69
|
+
try {
|
|
70
|
+
return JSON.parse(value);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return value.slice(1, -1);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return value;
|
|
77
|
+
}
|
|
78
|
+
function looksLikeYamlKeyword(value) {
|
|
79
|
+
const lowered = value.trim().toLowerCase();
|
|
80
|
+
return lowered === "null" || lowered === "true" || lowered === "false" || lowered === "~";
|
|
81
|
+
}
|