@lumenflow/cli 2.2.2 → 2.3.2
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 +147 -57
- package/dist/__tests__/agent-log-issue.test.js +56 -0
- package/dist/__tests__/cli-entry-point.test.js +66 -17
- package/dist/__tests__/cli-subprocess.test.js +25 -0
- package/dist/__tests__/init.test.js +298 -0
- package/dist/__tests__/initiative-plan.test.js +340 -0
- package/dist/__tests__/mem-cleanup-execution.test.js +19 -0
- package/dist/__tests__/merge-block.test.js +220 -0
- package/dist/__tests__/release.test.js +61 -0
- package/dist/__tests__/safe-git.test.js +191 -0
- package/dist/__tests__/state-doctor.test.js +274 -0
- package/dist/__tests__/wu-done.test.js +36 -0
- package/dist/__tests__/wu-edit.test.js +119 -0
- package/dist/__tests__/wu-prep.test.js +108 -0
- package/dist/agent-issues-query.js +4 -3
- package/dist/agent-log-issue.js +25 -4
- package/dist/backlog-prune.js +5 -4
- package/dist/cli-entry-point.js +11 -1
- package/dist/doctor.js +368 -0
- package/dist/flow-bottlenecks.js +6 -5
- package/dist/flow-report.js +4 -3
- package/dist/gates.js +356 -101
- package/dist/guard-locked.js +4 -3
- package/dist/guard-worktree-commit.js +4 -3
- package/dist/init.js +517 -86
- package/dist/initiative-add-wu.js +4 -3
- package/dist/initiative-bulk-assign-wus.js +8 -5
- package/dist/initiative-create.js +73 -37
- package/dist/initiative-edit.js +37 -21
- package/dist/initiative-list.js +4 -3
- package/dist/initiative-plan.js +337 -0
- package/dist/initiative-status.js +4 -3
- package/dist/lane-health.js +377 -0
- package/dist/lane-suggest.js +382 -0
- package/dist/mem-checkpoint.js +2 -2
- package/dist/mem-cleanup.js +2 -2
- package/dist/mem-context.js +306 -0
- package/dist/mem-create.js +2 -2
- package/dist/mem-delete.js +293 -0
- package/dist/mem-inbox.js +2 -2
- package/dist/mem-index.js +211 -0
- package/dist/mem-init.js +1 -1
- package/dist/mem-profile.js +207 -0
- package/dist/mem-promote.js +254 -0
- package/dist/mem-ready.js +2 -2
- package/dist/mem-signal.js +2 -2
- package/dist/mem-start.js +2 -2
- package/dist/mem-summarize.js +2 -2
- package/dist/mem-triage.js +2 -2
- package/dist/merge-block.js +222 -0
- package/dist/metrics-cli.js +7 -4
- package/dist/metrics-snapshot.js +4 -3
- package/dist/orchestrate-initiative.js +10 -4
- package/dist/orchestrate-monitor.js +379 -31
- package/dist/release.js +69 -29
- package/dist/signal-cleanup.js +296 -0
- package/dist/spawn-list.js +6 -5
- package/dist/state-bootstrap.js +5 -4
- package/dist/state-cleanup.js +360 -0
- package/dist/state-doctor-fix.js +196 -0
- package/dist/state-doctor.js +501 -0
- package/dist/validate-agent-skills.js +4 -3
- package/dist/validate-agent-sync.js +4 -3
- package/dist/validate-backlog-sync.js +4 -3
- package/dist/validate-skills-spec.js +4 -3
- package/dist/validate.js +4 -3
- package/dist/wu-block.js +3 -3
- package/dist/wu-claim.js +208 -98
- package/dist/wu-cleanup.js +5 -4
- package/dist/wu-create.js +71 -46
- package/dist/wu-delete.js +88 -60
- package/dist/wu-deps.js +6 -5
- package/dist/wu-done-check.js +34 -0
- package/dist/wu-done.js +39 -12
- package/dist/wu-edit.js +63 -28
- package/dist/wu-infer-lane.js +7 -6
- package/dist/wu-preflight.js +23 -81
- package/dist/wu-prep.js +125 -0
- package/dist/wu-prune.js +4 -3
- package/dist/wu-recover.js +88 -22
- package/dist/wu-repair.js +7 -6
- package/dist/wu-spawn.js +226 -270
- package/dist/wu-status.js +4 -3
- package/dist/wu-unblock.js +5 -5
- package/dist/wu-unlock-lane.js +4 -3
- package/dist/wu-validate.js +5 -4
- package/package.json +16 -7
- package/templates/core/.lumenflow/constraints.md.template +192 -0
- package/templates/core/.lumenflow/rules/git-safety.md.template +27 -0
- package/templates/core/.lumenflow/rules/wu-workflow.md.template +48 -0
- package/templates/core/AGENTS.md.template +60 -0
- package/templates/core/LUMENFLOW.md.template +255 -0
- package/templates/core/UPGRADING.md.template +121 -0
- package/templates/core/ai/onboarding/agent-safety-card.md.template +106 -0
- package/templates/core/ai/onboarding/first-wu-mistakes.md.template +198 -0
- package/templates/core/ai/onboarding/quick-ref-commands.md.template +186 -0
- package/templates/core/ai/onboarding/release-process.md.template +362 -0
- package/templates/core/ai/onboarding/troubleshooting-wu-done.md.template +159 -0
- package/templates/core/ai/onboarding/wu-create-checklist.md.template +117 -0
- package/templates/vendors/aider/.aider.conf.yml.template +27 -0
- package/templates/vendors/claude/.claude/CLAUDE.md.template +52 -0
- package/templates/vendors/claude/.claude/settings.json.template +49 -0
- package/templates/vendors/claude/.claude/skills/bug-classification/SKILL.md.template +192 -0
- package/templates/vendors/claude/.claude/skills/code-quality/SKILL.md.template +152 -0
- package/templates/vendors/claude/.claude/skills/context-management/SKILL.md.template +155 -0
- package/templates/vendors/claude/.claude/skills/execution-memory/SKILL.md.template +304 -0
- package/templates/vendors/claude/.claude/skills/frontend-design/SKILL.md.template +131 -0
- package/templates/vendors/claude/.claude/skills/initiative-management/SKILL.md.template +164 -0
- package/templates/vendors/claude/.claude/skills/library-first/SKILL.md.template +98 -0
- package/templates/vendors/claude/.claude/skills/lumenflow-gates/SKILL.md.template +87 -0
- package/templates/vendors/claude/.claude/skills/multi-agent-coordination/SKILL.md.template +84 -0
- package/templates/vendors/claude/.claude/skills/ops-maintenance/SKILL.md.template +254 -0
- package/templates/vendors/claude/.claude/skills/orchestration/SKILL.md.template +189 -0
- package/templates/vendors/claude/.claude/skills/tdd-workflow/SKILL.md.template +139 -0
- package/templates/vendors/claude/.claude/skills/worktree-discipline/SKILL.md.template +138 -0
- package/templates/vendors/claude/.claude/skills/wu-lifecycle/SKILL.md.template +106 -0
- package/templates/vendors/cline/.clinerules.template +53 -0
- package/templates/vendors/cursor/.cursor/rules/lumenflow.md.template +34 -0
- package/templates/vendors/cursor/.cursor/rules.md.template +28 -0
- package/templates/vendors/windsurf/.windsurf/rules/lumenflow.md.template +34 -0
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/* eslint-disable no-console -- CLI tool requires console output */
|
|
3
|
+
/**
|
|
4
|
+
* Memory Context CLI (WU-1234, WU-1292)
|
|
5
|
+
*
|
|
6
|
+
* Generate deterministic, formatted context injection blocks for wu:spawn prompts.
|
|
7
|
+
* Outputs structured markdown with sections for project profile, summaries,
|
|
8
|
+
* WU context, and discoveries.
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* pnpm mem:context --wu WU-XXXX [options]
|
|
12
|
+
*
|
|
13
|
+
* Options:
|
|
14
|
+
* --max-size <bytes> Maximum context size in bytes (default: 4096)
|
|
15
|
+
* --spawn-context-max-size <bytes> Alias for --max-size (for config parity)
|
|
16
|
+
* --lane <lane> Filter project memories by lane (WU-1292)
|
|
17
|
+
* --max-recent-summaries <n> Limit recent summaries included (WU-1292)
|
|
18
|
+
* --max-project-nodes <n> Limit project nodes included (WU-1292)
|
|
19
|
+
* --format <json|human> Output format (default: human)
|
|
20
|
+
* --quiet Suppress header/footer output
|
|
21
|
+
*
|
|
22
|
+
* Includes audit logging to .lumenflow/telemetry/tools.ndjson.
|
|
23
|
+
*
|
|
24
|
+
* @see {@link packages/@lumenflow/memory/src/mem-context-core.ts} - Core logic
|
|
25
|
+
* @see {@link packages/@lumenflow/cli/__tests__/mem-context.test.ts} - Tests
|
|
26
|
+
*/
|
|
27
|
+
import fs from 'node:fs/promises';
|
|
28
|
+
import path from 'node:path';
|
|
29
|
+
import { generateContext } from '@lumenflow/memory/dist/mem-context-core.js';
|
|
30
|
+
import { createWUParser, WU_OPTIONS } from '@lumenflow/core/dist/arg-parser.js';
|
|
31
|
+
import { EXIT_CODES, LUMENFLOW_PATHS } from '@lumenflow/core/dist/wu-constants.js';
|
|
32
|
+
/**
|
|
33
|
+
* Log prefix for mem:context output
|
|
34
|
+
*/
|
|
35
|
+
const LOG_PREFIX = '[mem:context]';
|
|
36
|
+
/**
|
|
37
|
+
* Tool name for audit logging
|
|
38
|
+
*/
|
|
39
|
+
const TOOL_NAME = 'mem:context';
|
|
40
|
+
/**
|
|
41
|
+
* Valid output formats
|
|
42
|
+
*/
|
|
43
|
+
const VALID_FORMATS = ['json', 'human'];
|
|
44
|
+
/**
|
|
45
|
+
* CLI argument options specific to mem:context
|
|
46
|
+
*/
|
|
47
|
+
const CLI_OPTIONS = {
|
|
48
|
+
maxSize: {
|
|
49
|
+
name: 'maxSize',
|
|
50
|
+
flags: '-m, --max-size <bytes>',
|
|
51
|
+
description: 'Maximum context size in bytes (default: 4096)',
|
|
52
|
+
},
|
|
53
|
+
spawnContextMaxSize: {
|
|
54
|
+
name: 'spawnContextMaxSize',
|
|
55
|
+
flags: '--spawn-context-max-size <bytes>',
|
|
56
|
+
description: 'Alias for --max-size (for config parity with spawn_context_max_size)',
|
|
57
|
+
},
|
|
58
|
+
lane: {
|
|
59
|
+
name: 'lane',
|
|
60
|
+
flags: '-l, --lane <lane>',
|
|
61
|
+
description: 'Filter project memories by lane (e.g., "Framework: CLI")',
|
|
62
|
+
},
|
|
63
|
+
maxRecentSummaries: {
|
|
64
|
+
name: 'maxRecentSummaries',
|
|
65
|
+
flags: '--max-recent-summaries <count>',
|
|
66
|
+
description: 'Maximum number of recent summaries to include (default: 5)',
|
|
67
|
+
},
|
|
68
|
+
maxProjectNodes: {
|
|
69
|
+
name: 'maxProjectNodes',
|
|
70
|
+
flags: '--max-project-nodes <count>',
|
|
71
|
+
description: 'Maximum number of project nodes to include (default: 10)',
|
|
72
|
+
},
|
|
73
|
+
format: {
|
|
74
|
+
name: 'format',
|
|
75
|
+
flags: '-f, --format <format>',
|
|
76
|
+
description: 'Output format: json or human (default: human)',
|
|
77
|
+
},
|
|
78
|
+
baseDir: {
|
|
79
|
+
name: 'baseDir',
|
|
80
|
+
flags: '-d, --base-dir <path>',
|
|
81
|
+
description: 'Base directory (defaults to current directory)',
|
|
82
|
+
},
|
|
83
|
+
quiet: {
|
|
84
|
+
name: 'quiet',
|
|
85
|
+
flags: '-q, --quiet',
|
|
86
|
+
description: 'Suppress header/footer output, only show context block',
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* Write audit log entry for tool execution
|
|
91
|
+
*
|
|
92
|
+
* @param baseDir - Base directory
|
|
93
|
+
* @param entry - Audit log entry
|
|
94
|
+
*/
|
|
95
|
+
async function writeAuditLog(baseDir, entry) {
|
|
96
|
+
try {
|
|
97
|
+
const logPath = path.join(baseDir, LUMENFLOW_PATHS.AUDIT_LOG);
|
|
98
|
+
const logDir = path.dirname(logPath);
|
|
99
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool creates known directory
|
|
100
|
+
await fs.mkdir(logDir, { recursive: true });
|
|
101
|
+
const line = `${JSON.stringify(entry)}\n`;
|
|
102
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool writes audit log
|
|
103
|
+
await fs.appendFile(logPath, line, 'utf-8');
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
// Audit logging is non-fatal - silently ignore errors
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Validate and parse a positive integer argument
|
|
111
|
+
*
|
|
112
|
+
* @param value - Raw argument value
|
|
113
|
+
* @param optionName - Name of the option for error messages
|
|
114
|
+
* @returns Parsed integer value
|
|
115
|
+
* @throws If value is invalid
|
|
116
|
+
*/
|
|
117
|
+
function parsePositiveInt(value, optionName) {
|
|
118
|
+
if (!value) {
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
const parsed = parseInt(value, 10);
|
|
122
|
+
if (isNaN(parsed) || parsed <= 0) {
|
|
123
|
+
throw new Error(`Invalid ${optionName} value: "${value}". Must be a positive integer.`);
|
|
124
|
+
}
|
|
125
|
+
return parsed;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* WU-1292: Validate lane argument
|
|
129
|
+
*
|
|
130
|
+
* @param lane - Lane argument
|
|
131
|
+
* @returns Validated lane
|
|
132
|
+
* @throws If lane is empty string
|
|
133
|
+
*/
|
|
134
|
+
function validateLane(lane) {
|
|
135
|
+
if (lane === undefined) {
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
138
|
+
if (lane === '') {
|
|
139
|
+
throw new Error('Invalid --lane value: lane cannot be empty.');
|
|
140
|
+
}
|
|
141
|
+
return lane;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Validate format argument
|
|
145
|
+
*
|
|
146
|
+
* @param format - Format argument
|
|
147
|
+
* @returns Validated format
|
|
148
|
+
* @throws If format is invalid
|
|
149
|
+
*/
|
|
150
|
+
function validateFormat(format) {
|
|
151
|
+
if (!format) {
|
|
152
|
+
return 'human';
|
|
153
|
+
}
|
|
154
|
+
if (!VALID_FORMATS.includes(format)) {
|
|
155
|
+
throw new Error(`Invalid --format value: "${format}". Valid formats: ${VALID_FORMATS.join(', ')}`);
|
|
156
|
+
}
|
|
157
|
+
return format;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Print output in human-readable format
|
|
161
|
+
*
|
|
162
|
+
* @param result - Generate context result
|
|
163
|
+
* @param wuId - WU ID
|
|
164
|
+
* @param quiet - Whether to suppress headers
|
|
165
|
+
*/
|
|
166
|
+
function printHumanFormat(result, wuId, quiet) {
|
|
167
|
+
if (!quiet) {
|
|
168
|
+
console.log(`${LOG_PREFIX} Context for ${wuId}:`);
|
|
169
|
+
console.log('');
|
|
170
|
+
}
|
|
171
|
+
if (result.contextBlock === '') {
|
|
172
|
+
if (!quiet) {
|
|
173
|
+
console.log(' (no memories found - empty context block)');
|
|
174
|
+
console.log('');
|
|
175
|
+
}
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
console.log(result.contextBlock);
|
|
179
|
+
if (!quiet) {
|
|
180
|
+
console.log('');
|
|
181
|
+
console.log(`${LOG_PREFIX} ${result.stats.totalNodes} node(s) included${result.stats.truncated ? ' (truncated)' : ''}`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Print output in JSON format
|
|
186
|
+
*
|
|
187
|
+
* @param result - Generate context result
|
|
188
|
+
* @param wuId - WU ID
|
|
189
|
+
*/
|
|
190
|
+
function printJsonFormat(result, wuId) {
|
|
191
|
+
const output = {
|
|
192
|
+
wuId,
|
|
193
|
+
contextBlock: result.contextBlock,
|
|
194
|
+
stats: result.stats,
|
|
195
|
+
};
|
|
196
|
+
console.log(JSON.stringify(output, null, 2));
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Main CLI entry point
|
|
200
|
+
*/
|
|
201
|
+
async function main() {
|
|
202
|
+
const args = createWUParser({
|
|
203
|
+
name: 'mem-context',
|
|
204
|
+
description: 'Generate context injection block for wu:spawn prompts',
|
|
205
|
+
options: [
|
|
206
|
+
WU_OPTIONS.wu,
|
|
207
|
+
CLI_OPTIONS.maxSize,
|
|
208
|
+
CLI_OPTIONS.spawnContextMaxSize,
|
|
209
|
+
CLI_OPTIONS.lane,
|
|
210
|
+
CLI_OPTIONS.maxRecentSummaries,
|
|
211
|
+
CLI_OPTIONS.maxProjectNodes,
|
|
212
|
+
CLI_OPTIONS.format,
|
|
213
|
+
CLI_OPTIONS.baseDir,
|
|
214
|
+
CLI_OPTIONS.quiet,
|
|
215
|
+
],
|
|
216
|
+
required: ['wu'],
|
|
217
|
+
});
|
|
218
|
+
const baseDir = args.baseDir || process.cwd();
|
|
219
|
+
const startedAt = new Date().toISOString();
|
|
220
|
+
const startTime = Date.now();
|
|
221
|
+
let maxSize;
|
|
222
|
+
let lane;
|
|
223
|
+
let maxRecentSummaries;
|
|
224
|
+
let maxProjectNodes;
|
|
225
|
+
let format;
|
|
226
|
+
// Validate arguments
|
|
227
|
+
try {
|
|
228
|
+
// WU-1292: --spawn-context-max-size is an alias for --max-size (for config parity)
|
|
229
|
+
// If both are provided, --spawn-context-max-size takes precedence
|
|
230
|
+
const maxSizeArg = args.spawnContextMaxSize || args.maxSize;
|
|
231
|
+
maxSize = parsePositiveInt(maxSizeArg, '--max-size');
|
|
232
|
+
lane = validateLane(args.lane);
|
|
233
|
+
maxRecentSummaries = parsePositiveInt(args.maxRecentSummaries, '--max-recent-summaries');
|
|
234
|
+
maxProjectNodes = parsePositiveInt(args.maxProjectNodes, '--max-project-nodes');
|
|
235
|
+
format = validateFormat(args.format);
|
|
236
|
+
}
|
|
237
|
+
catch (err) {
|
|
238
|
+
const error = err;
|
|
239
|
+
console.error(`${LOG_PREFIX} Error: ${error.message}`);
|
|
240
|
+
process.exit(EXIT_CODES.ERROR);
|
|
241
|
+
}
|
|
242
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Core returns any type due to loose tsconfig
|
|
243
|
+
let result;
|
|
244
|
+
let error = null;
|
|
245
|
+
try {
|
|
246
|
+
result = await generateContext(baseDir, {
|
|
247
|
+
wuId: args.wu,
|
|
248
|
+
maxSize,
|
|
249
|
+
lane,
|
|
250
|
+
maxRecentSummaries,
|
|
251
|
+
maxProjectNodes,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
catch (err) {
|
|
255
|
+
const e = err;
|
|
256
|
+
error = e.message;
|
|
257
|
+
result = {
|
|
258
|
+
success: false,
|
|
259
|
+
contextBlock: '',
|
|
260
|
+
stats: { totalNodes: 0, byType: {}, truncated: false, size: 0 },
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
// Type assertion for type safety
|
|
264
|
+
const typedResult = result;
|
|
265
|
+
const durationMs = Date.now() - startTime;
|
|
266
|
+
// Write audit log entry
|
|
267
|
+
await writeAuditLog(baseDir, {
|
|
268
|
+
tool: TOOL_NAME,
|
|
269
|
+
status: error ? 'failed' : 'success',
|
|
270
|
+
startedAt,
|
|
271
|
+
completedAt: new Date().toISOString(),
|
|
272
|
+
durationMs,
|
|
273
|
+
input: {
|
|
274
|
+
baseDir,
|
|
275
|
+
wuId: args.wu,
|
|
276
|
+
maxSize,
|
|
277
|
+
lane,
|
|
278
|
+
maxRecentSummaries,
|
|
279
|
+
maxProjectNodes,
|
|
280
|
+
format,
|
|
281
|
+
quiet: args.quiet,
|
|
282
|
+
},
|
|
283
|
+
output: typedResult.success
|
|
284
|
+
? {
|
|
285
|
+
contextSize: typedResult.contextBlock.length,
|
|
286
|
+
stats: typedResult.stats,
|
|
287
|
+
}
|
|
288
|
+
: null,
|
|
289
|
+
error: error ? { message: error } : null,
|
|
290
|
+
});
|
|
291
|
+
if (error) {
|
|
292
|
+
console.error(`${LOG_PREFIX} Error: ${error}`);
|
|
293
|
+
process.exit(EXIT_CODES.ERROR);
|
|
294
|
+
}
|
|
295
|
+
// Print output based on format
|
|
296
|
+
if (format === 'json') {
|
|
297
|
+
printJsonFormat(typedResult, args.wu);
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
printHumanFormat(typedResult, args.wu, !!args.quiet);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
main().catch((e) => {
|
|
304
|
+
console.error(`${LOG_PREFIX} ${e.message}`);
|
|
305
|
+
process.exit(EXIT_CODES.ERROR);
|
|
306
|
+
});
|
package/dist/mem-create.js
CHANGED
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
* Usage:
|
|
12
12
|
* pnpm mem:create 'title' [--type <type>] [--discovered-from <id>] [--wu <id>] [--quiet]
|
|
13
13
|
*
|
|
14
|
-
* @see {@link
|
|
15
|
-
* @see {@link
|
|
14
|
+
* @see {@link packages/@lumenflow/cli/src/lib/mem-create-core.ts} - Core logic
|
|
15
|
+
* @see {@link packages/@lumenflow/cli/src/__tests__/mem-create.test.ts} - Tests
|
|
16
16
|
*/
|
|
17
17
|
import fs from 'node:fs/promises';
|
|
18
18
|
import path from 'node:path';
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/* eslint-disable no-console -- CLI command uses console for status output */
|
|
3
|
+
/**
|
|
4
|
+
* Memory Delete CLI (WU-1284)
|
|
5
|
+
*
|
|
6
|
+
* Delete or archive memory nodes using soft-delete pattern.
|
|
7
|
+
* Respects append-only pattern by marking nodes with metadata.status=deleted.
|
|
8
|
+
*
|
|
9
|
+
* Features:
|
|
10
|
+
* - Delete by node ID(s)
|
|
11
|
+
* - Bulk delete via --tag filter
|
|
12
|
+
* - Bulk delete via --older-than filter
|
|
13
|
+
* - Dry-run preview mode
|
|
14
|
+
*
|
|
15
|
+
* Usage:
|
|
16
|
+
* pnpm mem:delete <node-id> # Delete single node
|
|
17
|
+
* pnpm mem:delete <id1> <id2> # Delete multiple nodes
|
|
18
|
+
* pnpm mem:delete --tag obsolete # Delete by tag
|
|
19
|
+
* pnpm mem:delete --older-than 30d # Delete old nodes
|
|
20
|
+
* pnpm mem:delete --tag old --older-than 7d # Combined filters
|
|
21
|
+
* pnpm mem:delete <id> --dry-run # Preview only
|
|
22
|
+
*
|
|
23
|
+
* @see {@link packages/@lumenflow/memory/src/mem-delete-core.ts} - Core logic
|
|
24
|
+
* @see {@link packages/@lumenflow/memory/__tests__/mem-delete.test.ts} - Tests
|
|
25
|
+
*/
|
|
26
|
+
import fs from 'node:fs/promises';
|
|
27
|
+
import path from 'node:path';
|
|
28
|
+
import { deleteMemoryNodes } from '@lumenflow/memory/dist/mem-delete-core.js';
|
|
29
|
+
import { createWUParser } from '@lumenflow/core/dist/arg-parser.js';
|
|
30
|
+
import { EXIT_CODES, LUMENFLOW_PATHS } from '@lumenflow/core/dist/wu-constants.js';
|
|
31
|
+
/**
|
|
32
|
+
* Log prefix for mem:delete output
|
|
33
|
+
*/
|
|
34
|
+
const LOG_PREFIX = '[mem:delete]';
|
|
35
|
+
/**
|
|
36
|
+
* Tool name for audit logging
|
|
37
|
+
*/
|
|
38
|
+
const TOOL_NAME = 'mem:delete';
|
|
39
|
+
/**
|
|
40
|
+
* CLI argument options specific to mem:delete
|
|
41
|
+
*/
|
|
42
|
+
const CLI_OPTIONS = {
|
|
43
|
+
dryRun: {
|
|
44
|
+
name: 'dryRun',
|
|
45
|
+
flags: '--dry-run',
|
|
46
|
+
description: 'Preview deletion without making changes',
|
|
47
|
+
},
|
|
48
|
+
tag: {
|
|
49
|
+
name: 'tag',
|
|
50
|
+
flags: '--tag <tag>',
|
|
51
|
+
description: 'Delete all nodes matching this tag',
|
|
52
|
+
},
|
|
53
|
+
olderThan: {
|
|
54
|
+
name: 'olderThan',
|
|
55
|
+
flags: '--older-than <duration>',
|
|
56
|
+
description: 'Delete nodes older than duration (e.g., 30d, 7d, 24h, 2w)',
|
|
57
|
+
},
|
|
58
|
+
json: {
|
|
59
|
+
name: 'json',
|
|
60
|
+
flags: '--json',
|
|
61
|
+
description: 'Output as JSON',
|
|
62
|
+
},
|
|
63
|
+
quiet: {
|
|
64
|
+
name: 'quiet',
|
|
65
|
+
flags: '-q, --quiet',
|
|
66
|
+
description: 'Suppress output except errors',
|
|
67
|
+
},
|
|
68
|
+
baseDir: {
|
|
69
|
+
name: 'baseDir',
|
|
70
|
+
flags: '-b, --base-dir <path>',
|
|
71
|
+
description: 'Base directory (defaults to current directory)',
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* Write audit log entry for tool execution
|
|
76
|
+
*
|
|
77
|
+
* @param baseDir - Base directory
|
|
78
|
+
* @param entry - Audit log entry
|
|
79
|
+
*/
|
|
80
|
+
async function writeAuditLog(baseDir, entry) {
|
|
81
|
+
try {
|
|
82
|
+
const logPath = path.join(baseDir, LUMENFLOW_PATHS.AUDIT_LOG);
|
|
83
|
+
const logDir = path.dirname(logPath);
|
|
84
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool creates known directory
|
|
85
|
+
await fs.mkdir(logDir, { recursive: true });
|
|
86
|
+
const line = `${JSON.stringify(entry)}\n`;
|
|
87
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool writes audit log
|
|
88
|
+
await fs.appendFile(logPath, line, 'utf-8');
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
// Audit logging is non-fatal - silently ignore errors
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Parse CLI arguments and extract node IDs from positional args
|
|
96
|
+
*
|
|
97
|
+
* @returns Parsed args and node IDs
|
|
98
|
+
*/
|
|
99
|
+
function parseArguments() {
|
|
100
|
+
const args = createWUParser({
|
|
101
|
+
name: 'mem-delete',
|
|
102
|
+
description: 'Delete memory nodes (soft delete via metadata.status=deleted)',
|
|
103
|
+
options: [
|
|
104
|
+
CLI_OPTIONS.dryRun,
|
|
105
|
+
CLI_OPTIONS.tag,
|
|
106
|
+
CLI_OPTIONS.olderThan,
|
|
107
|
+
CLI_OPTIONS.json,
|
|
108
|
+
CLI_OPTIONS.quiet,
|
|
109
|
+
CLI_OPTIONS.baseDir,
|
|
110
|
+
],
|
|
111
|
+
required: [],
|
|
112
|
+
allowPositionalId: true,
|
|
113
|
+
});
|
|
114
|
+
// Extract positional arguments as node IDs
|
|
115
|
+
const nodeIds = [];
|
|
116
|
+
const argv = process.argv.slice(2).filter((arg) => arg !== '--');
|
|
117
|
+
for (const arg of argv) {
|
|
118
|
+
// Skip flags and their values
|
|
119
|
+
if (arg.startsWith('-')) {
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
// Check if this arg is a value for a flag (by checking previous arg)
|
|
123
|
+
const prevArg = argv[argv.indexOf(arg) - 1];
|
|
124
|
+
if (prevArg &&
|
|
125
|
+
prevArg.startsWith('-') &&
|
|
126
|
+
!prevArg.startsWith('--dry-run') &&
|
|
127
|
+
!prevArg.startsWith('--json') &&
|
|
128
|
+
!prevArg.startsWith('-q')) {
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
// Node IDs start with 'mem-'
|
|
132
|
+
if (arg.startsWith('mem-')) {
|
|
133
|
+
nodeIds.push(arg);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return { args, nodeIds };
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Display limit for ID lists
|
|
140
|
+
*/
|
|
141
|
+
const DISPLAY_LIMITS = {
|
|
142
|
+
DELETED_IDS: 10,
|
|
143
|
+
SKIPPED_IDS: 5,
|
|
144
|
+
};
|
|
145
|
+
/**
|
|
146
|
+
* Print a list of IDs with limit
|
|
147
|
+
*/
|
|
148
|
+
function printIdList(ids, limit, label) {
|
|
149
|
+
console.log('');
|
|
150
|
+
console.log(label);
|
|
151
|
+
const displayIds = ids.slice(0, limit);
|
|
152
|
+
for (const id of displayIds) {
|
|
153
|
+
console.log(` - ${id}`);
|
|
154
|
+
}
|
|
155
|
+
if (ids.length > limit) {
|
|
156
|
+
console.log(` ... and ${ids.length - limit} more`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Print errors list
|
|
161
|
+
*/
|
|
162
|
+
function printErrors(errors) {
|
|
163
|
+
if (errors.length === 0)
|
|
164
|
+
return;
|
|
165
|
+
console.log('');
|
|
166
|
+
console.log('Errors:');
|
|
167
|
+
for (const error of errors) {
|
|
168
|
+
console.log(` - ${error}`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Print deletion result to console
|
|
173
|
+
*
|
|
174
|
+
* @param result - Deletion result
|
|
175
|
+
* @param quiet - Suppress verbose output
|
|
176
|
+
*/
|
|
177
|
+
function printResult(result, quiet) {
|
|
178
|
+
const statusMsg = result.dryRun
|
|
179
|
+
? `${LOG_PREFIX} Dry-run: Would delete ${result.deletedCount} node(s)`
|
|
180
|
+
: `${LOG_PREFIX} Deletion complete`;
|
|
181
|
+
console.log(statusMsg);
|
|
182
|
+
if (quiet) {
|
|
183
|
+
console.log(`${result.deletedCount} deleted, ${result.skippedIds.length} skipped`);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
console.log('');
|
|
187
|
+
console.log('Summary:');
|
|
188
|
+
console.log(` Deleted: ${result.deletedCount} node(s)`);
|
|
189
|
+
console.log(` Skipped: ${result.skippedIds.length} node(s)`);
|
|
190
|
+
if (result.deletedIds.length > 0) {
|
|
191
|
+
const label = result.dryRun ? 'Would Delete:' : 'Deleted:';
|
|
192
|
+
printIdList(result.deletedIds, DISPLAY_LIMITS.DELETED_IDS, label);
|
|
193
|
+
}
|
|
194
|
+
if (result.skippedIds.length > 0) {
|
|
195
|
+
printIdList(result.skippedIds, DISPLAY_LIMITS.SKIPPED_IDS, 'Skipped (already deleted):');
|
|
196
|
+
}
|
|
197
|
+
printErrors(result.errors);
|
|
198
|
+
if (result.dryRun) {
|
|
199
|
+
console.log('');
|
|
200
|
+
console.log('To execute, run without --dry-run');
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Build delete options from CLI arguments
|
|
205
|
+
*/
|
|
206
|
+
function buildDeleteOptions(args, nodeIds) {
|
|
207
|
+
const options = {
|
|
208
|
+
dryRun: args.dryRun,
|
|
209
|
+
};
|
|
210
|
+
if (nodeIds.length > 0) {
|
|
211
|
+
options.nodeIds = nodeIds;
|
|
212
|
+
}
|
|
213
|
+
if (args.tag) {
|
|
214
|
+
options.tag = args.tag;
|
|
215
|
+
}
|
|
216
|
+
if (args.olderThan) {
|
|
217
|
+
options.olderThan = args.olderThan;
|
|
218
|
+
}
|
|
219
|
+
return options;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Print usage help and exit with error
|
|
223
|
+
*/
|
|
224
|
+
function printUsageAndExit() {
|
|
225
|
+
console.error(`${LOG_PREFIX} Error: At least one filter is required`);
|
|
226
|
+
console.error('');
|
|
227
|
+
console.error('Usage:');
|
|
228
|
+
console.error(' pnpm mem:delete <node-id> Delete single node by ID');
|
|
229
|
+
console.error(' pnpm mem:delete <id1> <id2> Delete multiple nodes');
|
|
230
|
+
console.error(' pnpm mem:delete --tag <tag> Delete all nodes with tag');
|
|
231
|
+
console.error(' pnpm mem:delete --older-than 30d Delete nodes older than 30 days');
|
|
232
|
+
console.error(' pnpm mem:delete --dry-run Preview deletion');
|
|
233
|
+
process.exit(EXIT_CODES.ERROR);
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Main CLI entry point
|
|
237
|
+
*/
|
|
238
|
+
async function main() {
|
|
239
|
+
const { args, nodeIds } = parseArguments();
|
|
240
|
+
const baseDir = args.baseDir || process.cwd();
|
|
241
|
+
const startedAt = new Date().toISOString();
|
|
242
|
+
const startTime = Date.now();
|
|
243
|
+
const deleteOptions = buildDeleteOptions(args, nodeIds);
|
|
244
|
+
// Check if any filter is provided
|
|
245
|
+
if (!deleteOptions.nodeIds && !deleteOptions.tag && !deleteOptions.olderThan) {
|
|
246
|
+
printUsageAndExit();
|
|
247
|
+
}
|
|
248
|
+
let result = null;
|
|
249
|
+
let error = null;
|
|
250
|
+
try {
|
|
251
|
+
result = await deleteMemoryNodes(baseDir, deleteOptions);
|
|
252
|
+
}
|
|
253
|
+
catch (err) {
|
|
254
|
+
error = err instanceof Error ? err.message : String(err);
|
|
255
|
+
}
|
|
256
|
+
const durationMs = Date.now() - startTime;
|
|
257
|
+
await writeAuditLog(baseDir, {
|
|
258
|
+
tool: TOOL_NAME,
|
|
259
|
+
status: error ? 'failed' : 'success',
|
|
260
|
+
startedAt,
|
|
261
|
+
completedAt: new Date().toISOString(),
|
|
262
|
+
durationMs,
|
|
263
|
+
input: { baseDir, nodeIds, tag: args.tag, olderThan: args.olderThan, dryRun: args.dryRun },
|
|
264
|
+
output: result
|
|
265
|
+
? {
|
|
266
|
+
success: result.success,
|
|
267
|
+
deletedCount: result.deletedCount,
|
|
268
|
+
deletedIds: result.deletedIds,
|
|
269
|
+
skippedCount: result.skippedIds.length,
|
|
270
|
+
dryRun: result.dryRun,
|
|
271
|
+
}
|
|
272
|
+
: null,
|
|
273
|
+
error: error ? { message: error } : null,
|
|
274
|
+
});
|
|
275
|
+
if (error) {
|
|
276
|
+
console.error(`${LOG_PREFIX} Error: ${error}`);
|
|
277
|
+
process.exit(EXIT_CODES.ERROR);
|
|
278
|
+
}
|
|
279
|
+
if (!result) {
|
|
280
|
+
console.error(`${LOG_PREFIX} Error: No result from delete operation`);
|
|
281
|
+
process.exit(EXIT_CODES.ERROR);
|
|
282
|
+
}
|
|
283
|
+
if (args.json) {
|
|
284
|
+
console.log(JSON.stringify(result, null, 2));
|
|
285
|
+
process.exit(result.success ? EXIT_CODES.SUCCESS : EXIT_CODES.ERROR);
|
|
286
|
+
}
|
|
287
|
+
printResult(result, args.quiet);
|
|
288
|
+
process.exit(result.success ? EXIT_CODES.SUCCESS : EXIT_CODES.ERROR);
|
|
289
|
+
}
|
|
290
|
+
main().catch((e) => {
|
|
291
|
+
console.error(`${LOG_PREFIX} ${e instanceof Error ? e.message : String(e)}`);
|
|
292
|
+
process.exit(EXIT_CODES.ERROR);
|
|
293
|
+
});
|
package/dist/mem-inbox.js
CHANGED
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
* Usage:
|
|
15
15
|
* pnpm mem:inbox [--lane <name>] [--wu <id>] [--since <time>] [--watch]
|
|
16
16
|
*
|
|
17
|
-
* @see {@link
|
|
18
|
-
* @see {@link
|
|
17
|
+
* @see {@link packages/@lumenflow/cli/src/lib/mem-signal-core.ts} - Core logic
|
|
18
|
+
* @see {@link packages/@lumenflow/cli/src/__tests__/mem-inbox.test.ts} - Tests
|
|
19
19
|
*/
|
|
20
20
|
import fs from 'node:fs/promises';
|
|
21
21
|
import path from 'node:path';
|