ralph-mem 0.1.0
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/LICENSE +21 -0
- package/README.md +232 -0
- package/dist/chunk-41rc1bhg.js +1116 -0
- package/dist/chunk-c3a91ngd.js +2734 -0
- package/dist/chunk-kga64hvg.js +90 -0
- package/dist/chunk-ns0dgdnb.js +21 -0
- package/dist/chunk-v8anyhk1.js +103 -0
- package/dist/chunk-w40c0y00.js +36 -0
- package/dist/hooks/post-tool-use.js +155 -0
- package/dist/hooks/session-end.js +89 -0
- package/dist/hooks/session-start.js +95 -0
- package/dist/hooks/user-prompt-submit.js +234 -0
- package/dist/index.js +64 -0
- package/dist/skills/mem-forget.js +192 -0
- package/dist/skills/mem-inject.js +130 -0
- package/dist/skills/mem-search.js +303 -0
- package/dist/skills/mem-status.js +200 -0
- package/dist/skills/ralph-config.js +404 -0
- package/dist/skills/ralph.js +654 -0
- package/package.json +64 -0
- package/plugin.json +51 -0
|
@@ -0,0 +1,654 @@
|
|
|
1
|
+
import {
|
|
2
|
+
loadConfig
|
|
3
|
+
} from "../chunk-c3a91ngd.js";
|
|
4
|
+
import {
|
|
5
|
+
createDBClient
|
|
6
|
+
} from "../chunk-41rc1bhg.js";
|
|
7
|
+
import {
|
|
8
|
+
ensureProjectDirs,
|
|
9
|
+
getProjectDBPath,
|
|
10
|
+
getProjectDataDir
|
|
11
|
+
} from "../chunk-w40c0y00.js";
|
|
12
|
+
import"../chunk-ns0dgdnb.js";
|
|
13
|
+
|
|
14
|
+
// src/features/ralph/engine.ts
|
|
15
|
+
function sleep(ms) {
|
|
16
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
17
|
+
}
|
|
18
|
+
function createLoopEngine(projectPath, sessionId, options) {
|
|
19
|
+
const config = options?.config ? { ...loadConfig(projectPath), ...options.config } : loadConfig(projectPath);
|
|
20
|
+
ensureProjectDirs(projectPath);
|
|
21
|
+
const dbPath = getProjectDBPath(projectPath);
|
|
22
|
+
const client = options?.client ?? createDBClient(dbPath);
|
|
23
|
+
let currentLoopRun = null;
|
|
24
|
+
let stopRequested = false;
|
|
25
|
+
let iterationCallback = null;
|
|
26
|
+
let completeCallback = null;
|
|
27
|
+
let iterationStartCallback = null;
|
|
28
|
+
let iterationEndCallback = null;
|
|
29
|
+
let currentIteration = 0;
|
|
30
|
+
function endLoopRun(status) {
|
|
31
|
+
if (!currentLoopRun)
|
|
32
|
+
return;
|
|
33
|
+
const now = new Date().toISOString();
|
|
34
|
+
client.updateLoopRun(currentLoopRun.id, {
|
|
35
|
+
status,
|
|
36
|
+
ended_at: now
|
|
37
|
+
});
|
|
38
|
+
currentLoopRun = client.getLoopRun(currentLoopRun.id);
|
|
39
|
+
}
|
|
40
|
+
function createLoopSummary(loopRunId, task, result) {
|
|
41
|
+
const statusText = result.reason === "success" ? "성공" : result.reason === "max_iterations" ? "최대 반복 도달" : result.reason === "stopped" ? "중단됨" : "오류";
|
|
42
|
+
const content = `Ralph Loop 완료
|
|
43
|
+
태스크: ${task}
|
|
44
|
+
상태: ${statusText}
|
|
45
|
+
반복: ${result.iterations}회
|
|
46
|
+
${result.error ? `오류: ${result.error}` : ""}`.trim();
|
|
47
|
+
client.createObservation({
|
|
48
|
+
session_id: sessionId,
|
|
49
|
+
type: result.success ? "success" : "note",
|
|
50
|
+
content,
|
|
51
|
+
importance: result.success ? 0.9 : 0.7,
|
|
52
|
+
loop_run_id: loopRunId,
|
|
53
|
+
iteration: result.iterations
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
isRunning() {
|
|
58
|
+
return currentLoopRun !== null && currentLoopRun.status === "running";
|
|
59
|
+
},
|
|
60
|
+
getCurrentRun() {
|
|
61
|
+
if (!currentLoopRun)
|
|
62
|
+
return null;
|
|
63
|
+
return client.getLoopRun(currentLoopRun.id);
|
|
64
|
+
},
|
|
65
|
+
getLoopContext() {
|
|
66
|
+
if (!currentLoopRun || currentLoopRun.status !== "running") {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
runId: currentLoopRun.id,
|
|
71
|
+
iteration: currentIteration
|
|
72
|
+
};
|
|
73
|
+
},
|
|
74
|
+
async start(task, loopOptions) {
|
|
75
|
+
const activeRun = client.getActiveLoopRun(sessionId);
|
|
76
|
+
if (activeRun) {
|
|
77
|
+
throw new Error(`Loop already running: ${activeRun.id}. Stop it first with stop().`);
|
|
78
|
+
}
|
|
79
|
+
stopRequested = false;
|
|
80
|
+
const criteria = loopOptions?.criteria ?? config.ralph.success_criteria;
|
|
81
|
+
const maxIterations = loopOptions?.maxIterations ?? config.ralph.max_iterations;
|
|
82
|
+
const cooldownMs = loopOptions?.cooldownMs ?? config.ralph.cooldown_ms;
|
|
83
|
+
currentLoopRun = client.createLoopRun({
|
|
84
|
+
session_id: sessionId,
|
|
85
|
+
task,
|
|
86
|
+
criteria: JSON.stringify(criteria),
|
|
87
|
+
max_iterations: maxIterations
|
|
88
|
+
});
|
|
89
|
+
const loopRunId = currentLoopRun.id;
|
|
90
|
+
let iteration = 0;
|
|
91
|
+
let lastError;
|
|
92
|
+
try {
|
|
93
|
+
while (iteration < maxIterations) {
|
|
94
|
+
if (stopRequested) {
|
|
95
|
+
endLoopRun("stopped");
|
|
96
|
+
const result2 = {
|
|
97
|
+
success: false,
|
|
98
|
+
iterations: iteration,
|
|
99
|
+
reason: "stopped",
|
|
100
|
+
loopRunId
|
|
101
|
+
};
|
|
102
|
+
createLoopSummary(loopRunId, task, result2);
|
|
103
|
+
if (completeCallback)
|
|
104
|
+
completeCallback(result2);
|
|
105
|
+
return result2;
|
|
106
|
+
}
|
|
107
|
+
iteration++;
|
|
108
|
+
currentIteration = iteration;
|
|
109
|
+
client.updateLoopRun(loopRunId, { iterations: iteration });
|
|
110
|
+
const iterationContext = {
|
|
111
|
+
iteration,
|
|
112
|
+
task,
|
|
113
|
+
loopRunId
|
|
114
|
+
};
|
|
115
|
+
if (iterationStartCallback) {
|
|
116
|
+
iterationStartCallback(iterationContext);
|
|
117
|
+
}
|
|
118
|
+
if (!iterationCallback) {
|
|
119
|
+
throw new Error("No iteration callback set. Call onIteration() before start().");
|
|
120
|
+
}
|
|
121
|
+
const iterationResult = await iterationCallback(iterationContext);
|
|
122
|
+
if (iterationEndCallback) {
|
|
123
|
+
iterationEndCallback(iterationContext, iterationResult);
|
|
124
|
+
}
|
|
125
|
+
if (iterationResult.success) {
|
|
126
|
+
endLoopRun("success");
|
|
127
|
+
const result2 = {
|
|
128
|
+
success: true,
|
|
129
|
+
iterations: iteration,
|
|
130
|
+
reason: "success",
|
|
131
|
+
loopRunId
|
|
132
|
+
};
|
|
133
|
+
createLoopSummary(loopRunId, task, result2);
|
|
134
|
+
if (completeCallback)
|
|
135
|
+
completeCallback(result2);
|
|
136
|
+
return result2;
|
|
137
|
+
}
|
|
138
|
+
if (iterationResult.error) {
|
|
139
|
+
lastError = iterationResult.error;
|
|
140
|
+
}
|
|
141
|
+
if (iteration < maxIterations && !stopRequested) {
|
|
142
|
+
await sleep(cooldownMs);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
endLoopRun("failed");
|
|
146
|
+
const result = {
|
|
147
|
+
success: false,
|
|
148
|
+
iterations: iteration,
|
|
149
|
+
reason: "max_iterations",
|
|
150
|
+
loopRunId,
|
|
151
|
+
error: lastError
|
|
152
|
+
};
|
|
153
|
+
createLoopSummary(loopRunId, task, result);
|
|
154
|
+
if (completeCallback)
|
|
155
|
+
completeCallback(result);
|
|
156
|
+
return result;
|
|
157
|
+
} catch (error) {
|
|
158
|
+
endLoopRun("failed");
|
|
159
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
160
|
+
const result = {
|
|
161
|
+
success: false,
|
|
162
|
+
iterations: iteration,
|
|
163
|
+
reason: "error",
|
|
164
|
+
loopRunId,
|
|
165
|
+
error: errorMessage
|
|
166
|
+
};
|
|
167
|
+
createLoopSummary(loopRunId, task, result);
|
|
168
|
+
if (completeCallback)
|
|
169
|
+
completeCallback(result);
|
|
170
|
+
return result;
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
async stop() {
|
|
174
|
+
stopRequested = true;
|
|
175
|
+
if (currentLoopRun && currentLoopRun.status === "running") {
|
|
176
|
+
endLoopRun("stopped");
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
onIteration(callback) {
|
|
180
|
+
iterationCallback = callback;
|
|
181
|
+
},
|
|
182
|
+
onComplete(callback) {
|
|
183
|
+
completeCallback = callback;
|
|
184
|
+
},
|
|
185
|
+
onIterationStart(callback) {
|
|
186
|
+
iterationStartCallback = callback;
|
|
187
|
+
},
|
|
188
|
+
onIterationEnd(callback) {
|
|
189
|
+
iterationEndCallback = callback;
|
|
190
|
+
},
|
|
191
|
+
close() {
|
|
192
|
+
if (!options?.client) {
|
|
193
|
+
client.close();
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// src/features/ralph/snapshot.ts
|
|
200
|
+
import { execSync } from "node:child_process";
|
|
201
|
+
import {
|
|
202
|
+
copyFileSync,
|
|
203
|
+
existsSync,
|
|
204
|
+
mkdirSync,
|
|
205
|
+
readFileSync,
|
|
206
|
+
readdirSync,
|
|
207
|
+
rmSync,
|
|
208
|
+
writeFileSync
|
|
209
|
+
} from "node:fs";
|
|
210
|
+
import { dirname, join } from "node:path";
|
|
211
|
+
function getModifiedFiles(projectPath) {
|
|
212
|
+
try {
|
|
213
|
+
const gitDir = join(projectPath, ".git");
|
|
214
|
+
if (!existsSync(gitDir)) {
|
|
215
|
+
return [];
|
|
216
|
+
}
|
|
217
|
+
const staged = execSync("git diff --cached --name-only", {
|
|
218
|
+
cwd: projectPath,
|
|
219
|
+
encoding: "utf-8"
|
|
220
|
+
}).trim();
|
|
221
|
+
const unstaged = execSync("git diff --name-only", {
|
|
222
|
+
cwd: projectPath,
|
|
223
|
+
encoding: "utf-8"
|
|
224
|
+
}).trim();
|
|
225
|
+
const untracked = execSync("git ls-files --others --exclude-standard", {
|
|
226
|
+
cwd: projectPath,
|
|
227
|
+
encoding: "utf-8"
|
|
228
|
+
}).trim();
|
|
229
|
+
const files = new Set;
|
|
230
|
+
for (const line of staged.split(`
|
|
231
|
+
`)) {
|
|
232
|
+
if (line.trim())
|
|
233
|
+
files.add(line.trim());
|
|
234
|
+
}
|
|
235
|
+
for (const line of unstaged.split(`
|
|
236
|
+
`)) {
|
|
237
|
+
if (line.trim())
|
|
238
|
+
files.add(line.trim());
|
|
239
|
+
}
|
|
240
|
+
for (const line of untracked.split(`
|
|
241
|
+
`)) {
|
|
242
|
+
if (line.trim())
|
|
243
|
+
files.add(line.trim());
|
|
244
|
+
}
|
|
245
|
+
return Array.from(files).filter((file) => existsSync(join(projectPath, file)));
|
|
246
|
+
} catch {
|
|
247
|
+
return [];
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
function getSnapshotsDir(projectPath) {
|
|
251
|
+
return join(getProjectDataDir(projectPath), "snapshots");
|
|
252
|
+
}
|
|
253
|
+
function createSnapshotManager(projectPath) {
|
|
254
|
+
const snapshotsDir = getSnapshotsDir(projectPath);
|
|
255
|
+
return {
|
|
256
|
+
async create(runId) {
|
|
257
|
+
const snapshotPath = join(snapshotsDir, runId);
|
|
258
|
+
mkdirSync(snapshotPath, { recursive: true });
|
|
259
|
+
const modifiedFiles = getModifiedFiles(projectPath);
|
|
260
|
+
for (const file of modifiedFiles) {
|
|
261
|
+
const srcPath = join(projectPath, file);
|
|
262
|
+
const destPath = join(snapshotPath, file);
|
|
263
|
+
mkdirSync(dirname(destPath), { recursive: true });
|
|
264
|
+
copyFileSync(srcPath, destPath);
|
|
265
|
+
}
|
|
266
|
+
const metadata = {
|
|
267
|
+
runId,
|
|
268
|
+
createdAt: new Date().toISOString(),
|
|
269
|
+
files: modifiedFiles
|
|
270
|
+
};
|
|
271
|
+
writeFileSync(join(snapshotPath, ".snapshot-meta.json"), JSON.stringify(metadata, null, 2));
|
|
272
|
+
return snapshotPath;
|
|
273
|
+
},
|
|
274
|
+
async restore(snapshotPath) {
|
|
275
|
+
if (!existsSync(snapshotPath)) {
|
|
276
|
+
throw new Error(`Snapshot not found: ${snapshotPath}`);
|
|
277
|
+
}
|
|
278
|
+
const metaPath = join(snapshotPath, ".snapshot-meta.json");
|
|
279
|
+
if (!existsSync(metaPath)) {
|
|
280
|
+
throw new Error(`Invalid snapshot: missing metadata at ${snapshotPath}`);
|
|
281
|
+
}
|
|
282
|
+
const metadata = JSON.parse(readFileSync(metaPath, "utf-8"));
|
|
283
|
+
const files = metadata.files || [];
|
|
284
|
+
for (const file of files) {
|
|
285
|
+
const srcPath = join(snapshotPath, file);
|
|
286
|
+
const destPath = join(projectPath, file);
|
|
287
|
+
if (existsSync(srcPath)) {
|
|
288
|
+
mkdirSync(dirname(destPath), { recursive: true });
|
|
289
|
+
copyFileSync(srcPath, destPath);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
},
|
|
293
|
+
async delete(snapshotPath) {
|
|
294
|
+
if (existsSync(snapshotPath)) {
|
|
295
|
+
rmSync(snapshotPath, { recursive: true });
|
|
296
|
+
}
|
|
297
|
+
},
|
|
298
|
+
async list() {
|
|
299
|
+
if (!existsSync(snapshotsDir)) {
|
|
300
|
+
return [];
|
|
301
|
+
}
|
|
302
|
+
const entries = readdirSync(snapshotsDir);
|
|
303
|
+
const snapshots = [];
|
|
304
|
+
for (const entry of entries) {
|
|
305
|
+
const snapshotPath = join(snapshotsDir, entry);
|
|
306
|
+
const metaPath = join(snapshotPath, ".snapshot-meta.json");
|
|
307
|
+
if (existsSync(metaPath)) {
|
|
308
|
+
try {
|
|
309
|
+
const metadata = JSON.parse(readFileSync(metaPath, "utf-8"));
|
|
310
|
+
const files = metadata.files || [];
|
|
311
|
+
snapshots.push({
|
|
312
|
+
runId: metadata.runId || entry,
|
|
313
|
+
path: snapshotPath,
|
|
314
|
+
createdAt: new Date(metadata.createdAt),
|
|
315
|
+
fileCount: files.length
|
|
316
|
+
});
|
|
317
|
+
} catch {}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return snapshots.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
|
|
321
|
+
},
|
|
322
|
+
async cleanup(maxAge) {
|
|
323
|
+
const snapshots = await this.list();
|
|
324
|
+
const cutoff = Date.now() - maxAge;
|
|
325
|
+
let deletedCount = 0;
|
|
326
|
+
for (const snapshot of snapshots) {
|
|
327
|
+
if (snapshot.createdAt.getTime() < cutoff) {
|
|
328
|
+
await this.delete(snapshot.path);
|
|
329
|
+
deletedCount++;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return deletedCount;
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
async function restoreRunSnapshot(projectPath, runId) {
|
|
337
|
+
const manager = createSnapshotManager(projectPath);
|
|
338
|
+
const snapshotPath = join(getSnapshotsDir(projectPath), runId);
|
|
339
|
+
return manager.restore(snapshotPath);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// src/skills/ralph.ts
|
|
343
|
+
var CRITERIA_COMMANDS = {
|
|
344
|
+
test_pass: "npm test",
|
|
345
|
+
build_success: "npm run build",
|
|
346
|
+
lint_clean: "npm run lint",
|
|
347
|
+
type_check: "npx tsc --noEmit",
|
|
348
|
+
custom: "(custom)"
|
|
349
|
+
};
|
|
350
|
+
function parseStartArgs(argsString) {
|
|
351
|
+
const args = {
|
|
352
|
+
task: ""
|
|
353
|
+
};
|
|
354
|
+
const tokens = [];
|
|
355
|
+
let current = "";
|
|
356
|
+
let inQuotes = false;
|
|
357
|
+
let quoteChar = "";
|
|
358
|
+
for (const char of argsString) {
|
|
359
|
+
if ((char === '"' || char === "'") && !inQuotes) {
|
|
360
|
+
inQuotes = true;
|
|
361
|
+
quoteChar = char;
|
|
362
|
+
} else if (char === quoteChar && inQuotes) {
|
|
363
|
+
inQuotes = false;
|
|
364
|
+
quoteChar = "";
|
|
365
|
+
} else if (char === " " && !inQuotes) {
|
|
366
|
+
if (current) {
|
|
367
|
+
tokens.push(current);
|
|
368
|
+
current = "";
|
|
369
|
+
}
|
|
370
|
+
} else {
|
|
371
|
+
current += char;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
if (current) {
|
|
375
|
+
tokens.push(current);
|
|
376
|
+
}
|
|
377
|
+
let i = 0;
|
|
378
|
+
while (i < tokens.length) {
|
|
379
|
+
const token = tokens[i];
|
|
380
|
+
if (token === "--criteria" && i + 1 < tokens.length) {
|
|
381
|
+
const criteriaType = tokens[i + 1].toLowerCase();
|
|
382
|
+
if (isValidCriteriaType(criteriaType)) {
|
|
383
|
+
args.criteria = criteriaType;
|
|
384
|
+
}
|
|
385
|
+
i += 2;
|
|
386
|
+
} else if (token === "--max-iterations" && i + 1 < tokens.length) {
|
|
387
|
+
args.maxIterations = Number.parseInt(tokens[i + 1], 10);
|
|
388
|
+
i += 2;
|
|
389
|
+
} else if (token === "--cooldown" && i + 1 < tokens.length) {
|
|
390
|
+
args.cooldownMs = Number.parseInt(tokens[i + 1], 10);
|
|
391
|
+
i += 2;
|
|
392
|
+
} else if (token === "--no-snapshot") {
|
|
393
|
+
args.noSnapshot = true;
|
|
394
|
+
i++;
|
|
395
|
+
} else if (!token.startsWith("--") && !args.task) {
|
|
396
|
+
args.task = token;
|
|
397
|
+
i++;
|
|
398
|
+
} else {
|
|
399
|
+
i++;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
return args;
|
|
403
|
+
}
|
|
404
|
+
function isValidCriteriaType(type) {
|
|
405
|
+
return [
|
|
406
|
+
"test_pass",
|
|
407
|
+
"build_success",
|
|
408
|
+
"lint_clean",
|
|
409
|
+
"type_check",
|
|
410
|
+
"custom"
|
|
411
|
+
].includes(type);
|
|
412
|
+
}
|
|
413
|
+
function formatStartMessage(loopRunId, task, criteria, maxIterations) {
|
|
414
|
+
const command = CRITERIA_COMMANDS[criteria];
|
|
415
|
+
return `\uD83D\uDE80 Ralph Loop 시작
|
|
416
|
+
|
|
417
|
+
태스크: ${task}
|
|
418
|
+
기준: ${criteria} (${command})
|
|
419
|
+
최대 반복: ${maxIterations}
|
|
420
|
+
|
|
421
|
+
Loop ID: ${loopRunId}
|
|
422
|
+
중단: /ralph stop`;
|
|
423
|
+
}
|
|
424
|
+
function formatStopMessage(loopRunId, reason, rolledBack) {
|
|
425
|
+
const rollbackMsg = rolledBack ? `
|
|
426
|
+
파일이 롤백되었습니다.` : "";
|
|
427
|
+
return `⏹️ Ralph Loop 중단
|
|
428
|
+
|
|
429
|
+
Loop ID: ${loopRunId}
|
|
430
|
+
이유: ${reason}${rollbackMsg}`;
|
|
431
|
+
}
|
|
432
|
+
function formatStatusMessage(isRunning, run) {
|
|
433
|
+
if (!isRunning || !run) {
|
|
434
|
+
return `\uD83D\uDCCA Ralph Loop 상태: 실행 중인 Loop 없음
|
|
435
|
+
|
|
436
|
+
시작: /ralph start "태스크 설명"`;
|
|
437
|
+
}
|
|
438
|
+
const elapsed = Math.floor((Date.now() - run.startedAt.getTime()) / 1000);
|
|
439
|
+
const minutes = Math.floor(elapsed / 60);
|
|
440
|
+
const seconds = elapsed % 60;
|
|
441
|
+
return `\uD83D\uDCCA Ralph Loop 상태: 실행 중
|
|
442
|
+
|
|
443
|
+
Loop ID: ${run.id}
|
|
444
|
+
태스크: ${run.task}
|
|
445
|
+
반복: ${run.iterations}/${run.maxIterations}
|
|
446
|
+
경과: ${minutes}분 ${seconds}초
|
|
447
|
+
|
|
448
|
+
중단: /ralph stop`;
|
|
449
|
+
}
|
|
450
|
+
function formatHistoryMessage(history) {
|
|
451
|
+
if (history.length === 0) {
|
|
452
|
+
return "\uD83D\uDCCB Ralph Loop 이력: 이력 없음";
|
|
453
|
+
}
|
|
454
|
+
const rows = history.map((entry) => {
|
|
455
|
+
const taskShort = entry.task.length > 20 ? `${entry.task.slice(0, 17)}...` : entry.task;
|
|
456
|
+
return `│ ${entry.id.padEnd(14)} │ ${taskShort.padEnd(20)} │ ${entry.status.padEnd(7)} │ ${String(entry.iterations).padStart(4)} │`;
|
|
457
|
+
});
|
|
458
|
+
return `\uD83D\uDCCB 최근 Ralph Loop 이력
|
|
459
|
+
|
|
460
|
+
┌────────────────┬──────────────────────┬─────────┬──────┐
|
|
461
|
+
│ ID │ 태스크 │ 상태 │ 반복 │
|
|
462
|
+
├────────────────┼──────────────────────┼─────────┼──────┤
|
|
463
|
+
${rows.join(`
|
|
464
|
+
`)}
|
|
465
|
+
└────────────────┴──────────────────────┴─────────┴──────┘`;
|
|
466
|
+
}
|
|
467
|
+
function createRalphSkill(context) {
|
|
468
|
+
const { projectPath, sessionId } = context;
|
|
469
|
+
const config = context.config ?? loadConfig(projectPath);
|
|
470
|
+
let engine = context.engine;
|
|
471
|
+
const currentSnapshotPath = null;
|
|
472
|
+
function getOrCreateEngine() {
|
|
473
|
+
if (!engine) {
|
|
474
|
+
engine = createLoopEngine(projectPath, sessionId, {
|
|
475
|
+
client: context.client,
|
|
476
|
+
config
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
return engine;
|
|
480
|
+
}
|
|
481
|
+
return {
|
|
482
|
+
async start(args) {
|
|
483
|
+
if (!args.task) {
|
|
484
|
+
return {
|
|
485
|
+
success: false,
|
|
486
|
+
message: "",
|
|
487
|
+
error: '태스크 설명이 필요합니다. 사용법: /ralph start "태스크 설명"'
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
const eng = getOrCreateEngine();
|
|
491
|
+
if (eng.isRunning()) {
|
|
492
|
+
const currentRun = eng.getCurrentRun();
|
|
493
|
+
return {
|
|
494
|
+
success: false,
|
|
495
|
+
message: "",
|
|
496
|
+
error: `이미 Loop가 실행 중입니다. (ID: ${currentRun?.id})
|
|
497
|
+
중단: /ralph stop`
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
const criteriaType = args.criteria ?? "test_pass";
|
|
501
|
+
const criteria = [{ type: criteriaType }];
|
|
502
|
+
const maxIterations = args.maxIterations ?? config.ralph?.max_iterations ?? 10;
|
|
503
|
+
const cooldownMs = args.cooldownMs ?? config.ralph?.cooldown_ms ?? 1000;
|
|
504
|
+
if (!args.noSnapshot) {
|
|
505
|
+
try {} catch {}
|
|
506
|
+
}
|
|
507
|
+
try {
|
|
508
|
+
const loopRun = eng.getCurrentRun();
|
|
509
|
+
const message = formatStartMessage(loopRun?.id ?? "pending", args.task, criteriaType, maxIterations);
|
|
510
|
+
return {
|
|
511
|
+
success: true,
|
|
512
|
+
loopRunId: loopRun?.id,
|
|
513
|
+
message
|
|
514
|
+
};
|
|
515
|
+
} catch (error) {
|
|
516
|
+
return {
|
|
517
|
+
success: false,
|
|
518
|
+
message: "",
|
|
519
|
+
error: error instanceof Error ? error.message : String(error)
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
},
|
|
523
|
+
async stop(args = {}) {
|
|
524
|
+
const eng = getOrCreateEngine();
|
|
525
|
+
if (!eng.isRunning()) {
|
|
526
|
+
return {
|
|
527
|
+
success: false,
|
|
528
|
+
message: "",
|
|
529
|
+
error: "실행 중인 Loop가 없습니다."
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
const currentRun = eng.getCurrentRun();
|
|
533
|
+
const loopRunId = currentRun?.id ?? "unknown";
|
|
534
|
+
try {
|
|
535
|
+
await eng.stop();
|
|
536
|
+
let rolledBack = false;
|
|
537
|
+
if (args.rollback && currentSnapshotPath) {
|
|
538
|
+
try {
|
|
539
|
+
await restoreRunSnapshot(projectPath, loopRunId);
|
|
540
|
+
rolledBack = true;
|
|
541
|
+
} catch {}
|
|
542
|
+
}
|
|
543
|
+
const message = formatStopMessage(loopRunId, "사용자 중단", rolledBack);
|
|
544
|
+
return {
|
|
545
|
+
success: true,
|
|
546
|
+
message
|
|
547
|
+
};
|
|
548
|
+
} catch (error) {
|
|
549
|
+
return {
|
|
550
|
+
success: false,
|
|
551
|
+
message: "",
|
|
552
|
+
error: error instanceof Error ? error.message : String(error)
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
},
|
|
556
|
+
async status(args = {}) {
|
|
557
|
+
const eng = getOrCreateEngine();
|
|
558
|
+
const isRunning = eng.isRunning();
|
|
559
|
+
const currentRun = eng.getCurrentRun();
|
|
560
|
+
let run;
|
|
561
|
+
if (currentRun) {
|
|
562
|
+
run = {
|
|
563
|
+
id: currentRun.id,
|
|
564
|
+
task: currentRun.task,
|
|
565
|
+
iterations: currentRun.iterations,
|
|
566
|
+
maxIterations: currentRun.max_iterations,
|
|
567
|
+
startedAt: new Date(currentRun.started_at)
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
let history;
|
|
571
|
+
if (args.history && context.client) {
|
|
572
|
+
const loopRuns = context.client.listLoopRuns(sessionId, 10);
|
|
573
|
+
history = loopRuns.map((lr) => ({
|
|
574
|
+
id: lr.id,
|
|
575
|
+
task: lr.task,
|
|
576
|
+
status: lr.status,
|
|
577
|
+
iterations: lr.iterations,
|
|
578
|
+
startedAt: new Date(lr.started_at),
|
|
579
|
+
endedAt: lr.ended_at ? new Date(lr.ended_at) : undefined
|
|
580
|
+
}));
|
|
581
|
+
}
|
|
582
|
+
const message = args.history && history ? formatHistoryMessage(history) : formatStatusMessage(isRunning, run);
|
|
583
|
+
return {
|
|
584
|
+
isRunning,
|
|
585
|
+
currentRun: run,
|
|
586
|
+
history,
|
|
587
|
+
message
|
|
588
|
+
};
|
|
589
|
+
},
|
|
590
|
+
getEngine() {
|
|
591
|
+
return getOrCreateEngine();
|
|
592
|
+
},
|
|
593
|
+
close() {
|
|
594
|
+
if (engine) {
|
|
595
|
+
engine.close();
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
async function executeRalphCommand(command, argsString, context) {
|
|
601
|
+
const skill = createRalphSkill(context);
|
|
602
|
+
try {
|
|
603
|
+
switch (command.toLowerCase()) {
|
|
604
|
+
case "start": {
|
|
605
|
+
const args = parseStartArgs(argsString);
|
|
606
|
+
const result = await skill.start(args);
|
|
607
|
+
if (!result.success) {
|
|
608
|
+
return `❌ ${result.error}`;
|
|
609
|
+
}
|
|
610
|
+
return result.message;
|
|
611
|
+
}
|
|
612
|
+
case "stop": {
|
|
613
|
+
const rollback = argsString.includes("--rollback");
|
|
614
|
+
const result = await skill.stop({ rollback });
|
|
615
|
+
if (!result.success) {
|
|
616
|
+
return `❌ ${result.error}`;
|
|
617
|
+
}
|
|
618
|
+
return result.message;
|
|
619
|
+
}
|
|
620
|
+
case "status": {
|
|
621
|
+
const history = argsString.includes("--history");
|
|
622
|
+
const result = await skill.status({ history });
|
|
623
|
+
return result.message;
|
|
624
|
+
}
|
|
625
|
+
default:
|
|
626
|
+
return `❌ 알 수 없는 명령: ${command}
|
|
627
|
+
|
|
628
|
+
사용 가능한 명령:
|
|
629
|
+
/ralph start "태스크" [--criteria type] [--max-iterations n]
|
|
630
|
+
/ralph stop [--rollback]
|
|
631
|
+
/ralph status [--history]`;
|
|
632
|
+
}
|
|
633
|
+
} finally {
|
|
634
|
+
skill.close();
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
async function ralphSkill(_input) {
|
|
638
|
+
return {
|
|
639
|
+
success: false,
|
|
640
|
+
message: "Use createRalphSkill() for full functionality"
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
export {
|
|
644
|
+
ralphSkill,
|
|
645
|
+
parseStartArgs,
|
|
646
|
+
formatStopMessage,
|
|
647
|
+
formatStatusMessage,
|
|
648
|
+
formatStartMessage,
|
|
649
|
+
formatHistoryMessage,
|
|
650
|
+
executeRalphCommand,
|
|
651
|
+
createRalphSkill
|
|
652
|
+
};
|
|
653
|
+
|
|
654
|
+
export { ralphSkill };
|
package/package.json
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ralph-mem",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Persistent context management plugin for Claude Code with Ralph Loop",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist",
|
|
10
|
+
"plugin.json",
|
|
11
|
+
"prompts"
|
|
12
|
+
],
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+https://github.com/roboco-io/ralph-mem.git"
|
|
16
|
+
},
|
|
17
|
+
"bugs": {
|
|
18
|
+
"url": "https://github.com/roboco-io/ralph-mem/issues"
|
|
19
|
+
},
|
|
20
|
+
"homepage": "https://github.com/roboco-io/ralph-mem#readme",
|
|
21
|
+
"scripts": {
|
|
22
|
+
"dev": "bun run --watch src/index.ts",
|
|
23
|
+
"build": "bun run scripts/build.ts",
|
|
24
|
+
"test": "vitest run",
|
|
25
|
+
"test:watch": "vitest",
|
|
26
|
+
"test:coverage": "vitest run --coverage",
|
|
27
|
+
"lint": "bunx @biomejs/biome check src/",
|
|
28
|
+
"lint:fix": "bunx @biomejs/biome check --write src/",
|
|
29
|
+
"typecheck": "tsc --noEmit",
|
|
30
|
+
"prepare": "husky"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@xenova/transformers": "^2.17.2",
|
|
34
|
+
"better-sqlite3": "^12.6.2",
|
|
35
|
+
"js-yaml": "^4.1.1",
|
|
36
|
+
"nanoid": "^5.0.7"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@biomejs/biome": "^1.9.0",
|
|
40
|
+
"@types/better-sqlite3": "^7.6.13",
|
|
41
|
+
"@types/bun": "^1.1.0",
|
|
42
|
+
"@types/js-yaml": "^4.0.9",
|
|
43
|
+
"@types/node": "^22.0.0",
|
|
44
|
+
"@vitest/coverage-v8": "^2.0.0",
|
|
45
|
+
"husky": "^9.1.7",
|
|
46
|
+
"lint-staged": "^16.2.7",
|
|
47
|
+
"typescript": "^5.6.0",
|
|
48
|
+
"vitest": "^2.0.0"
|
|
49
|
+
},
|
|
50
|
+
"keywords": [
|
|
51
|
+
"claude-code",
|
|
52
|
+
"plugin",
|
|
53
|
+
"memory",
|
|
54
|
+
"context",
|
|
55
|
+
"ralph-loop"
|
|
56
|
+
],
|
|
57
|
+
"author": "",
|
|
58
|
+
"license": "MIT",
|
|
59
|
+
"lint-staged": {
|
|
60
|
+
"*.{ts,tsx}": [
|
|
61
|
+
"bunx @biomejs/biome check --write"
|
|
62
|
+
]
|
|
63
|
+
}
|
|
64
|
+
}
|