@yeseh/cortex-cli 0.6.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/README.md +144 -0
- package/dist/category/commands/create.d.ts +44 -0
- package/dist/category/commands/create.d.ts.map +1 -0
- package/dist/category/commands/create.spec.d.ts +7 -0
- package/dist/category/commands/create.spec.d.ts.map +1 -0
- package/dist/category/index.d.ts +19 -0
- package/dist/category/index.d.ts.map +1 -0
- package/dist/commands/init.d.ts +58 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.spec.d.ts +2 -0
- package/dist/commands/init.spec.d.ts.map +1 -0
- package/dist/context.d.ts +18 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.spec.d.ts +2 -0
- package/dist/context.spec.d.ts.map +1 -0
- package/dist/create-cli-command.d.ts +23 -0
- package/dist/create-cli-command.d.ts.map +1 -0
- package/dist/create-cli-command.spec.d.ts +10 -0
- package/dist/create-cli-command.spec.d.ts.map +1 -0
- package/dist/errors.d.ts +57 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.spec.d.ts +2 -0
- package/dist/errors.spec.d.ts.map +1 -0
- package/dist/input.d.ts +42 -0
- package/dist/input.d.ts.map +1 -0
- package/dist/input.spec.d.ts +2 -0
- package/dist/input.spec.d.ts.map +1 -0
- package/dist/memory/commands/add.d.ts +62 -0
- package/dist/memory/commands/add.d.ts.map +1 -0
- package/dist/memory/commands/add.spec.d.ts +7 -0
- package/dist/memory/commands/add.spec.d.ts.map +1 -0
- package/dist/memory/commands/definitions.spec.d.ts +10 -0
- package/dist/memory/commands/definitions.spec.d.ts.map +1 -0
- package/dist/memory/commands/handlers.spec.d.ts +2 -0
- package/dist/memory/commands/handlers.spec.d.ts.map +1 -0
- package/dist/memory/commands/list.d.ts +119 -0
- package/dist/memory/commands/list.d.ts.map +1 -0
- package/dist/memory/commands/list.spec.d.ts +2 -0
- package/dist/memory/commands/list.spec.d.ts.map +1 -0
- package/dist/memory/commands/move.d.ts +42 -0
- package/dist/memory/commands/move.d.ts.map +1 -0
- package/dist/memory/commands/move.spec.d.ts +2 -0
- package/dist/memory/commands/move.spec.d.ts.map +1 -0
- package/dist/memory/commands/remove.d.ts +41 -0
- package/dist/memory/commands/remove.d.ts.map +1 -0
- package/dist/memory/commands/remove.spec.d.ts +2 -0
- package/dist/memory/commands/remove.spec.d.ts.map +1 -0
- package/dist/memory/commands/show.d.ts +81 -0
- package/dist/memory/commands/show.d.ts.map +1 -0
- package/dist/memory/commands/show.spec.d.ts +2 -0
- package/dist/memory/commands/show.spec.d.ts.map +1 -0
- package/dist/memory/commands/test-helpers.spec.d.ts +19 -0
- package/dist/memory/commands/test-helpers.spec.d.ts.map +1 -0
- package/dist/memory/commands/update.d.ts +73 -0
- package/dist/memory/commands/update.d.ts.map +1 -0
- package/dist/memory/commands/update.spec.d.ts +2 -0
- package/dist/memory/commands/update.spec.d.ts.map +1 -0
- package/dist/memory/index.d.ts +29 -0
- package/dist/memory/index.d.ts.map +1 -0
- package/dist/memory/index.spec.d.ts +10 -0
- package/dist/memory/index.spec.d.ts.map +1 -0
- package/dist/memory/parsing.d.ts +3 -0
- package/dist/memory/parsing.d.ts.map +1 -0
- package/dist/memory/parsing.spec.d.ts +7 -0
- package/dist/memory/parsing.spec.d.ts.map +1 -0
- package/dist/output.d.ts +87 -0
- package/dist/output.d.ts.map +1 -0
- package/dist/output.spec.d.ts +2 -0
- package/dist/output.spec.d.ts.map +1 -0
- package/dist/paths.d.ts +27 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.spec.d.ts +7 -0
- package/dist/paths.spec.d.ts.map +1 -0
- package/dist/program.d.ts +41 -0
- package/dist/program.d.ts.map +1 -0
- package/dist/program.spec.d.ts +11 -0
- package/dist/program.spec.d.ts.map +1 -0
- package/dist/run.d.ts +7 -0
- package/dist/run.d.ts.map +1 -0
- package/dist/run.spec.d.ts +12 -0
- package/dist/run.spec.d.ts.map +1 -0
- package/dist/store/commands/add.d.ts +73 -0
- package/dist/store/commands/add.d.ts.map +1 -0
- package/dist/store/commands/add.spec.d.ts +17 -0
- package/dist/store/commands/add.spec.d.ts.map +1 -0
- package/dist/store/commands/init.d.ts +75 -0
- package/dist/store/commands/init.d.ts.map +1 -0
- package/dist/store/commands/init.spec.d.ts +7 -0
- package/dist/store/commands/init.spec.d.ts.map +1 -0
- package/dist/store/commands/list.d.ts +62 -0
- package/dist/store/commands/list.d.ts.map +1 -0
- package/dist/store/commands/list.spec.d.ts +7 -0
- package/dist/store/commands/list.spec.d.ts.map +1 -0
- package/dist/store/commands/prune.d.ts +92 -0
- package/dist/store/commands/prune.d.ts.map +1 -0
- package/dist/store/commands/prune.spec.d.ts +7 -0
- package/dist/store/commands/prune.spec.d.ts.map +1 -0
- package/dist/store/commands/reindexs.d.ts +54 -0
- package/dist/store/commands/reindexs.d.ts.map +1 -0
- package/dist/store/commands/reindexs.spec.d.ts +7 -0
- package/dist/store/commands/reindexs.spec.d.ts.map +1 -0
- package/dist/store/commands/remove.d.ts +63 -0
- package/dist/store/commands/remove.d.ts.map +1 -0
- package/dist/store/commands/remove.spec.d.ts +17 -0
- package/dist/store/commands/remove.spec.d.ts.map +1 -0
- package/dist/store/index.d.ts +32 -0
- package/dist/store/index.d.ts.map +1 -0
- package/dist/store/index.spec.d.ts +9 -0
- package/dist/store/index.spec.d.ts.map +1 -0
- package/dist/store/utils/resolve-store-name.d.ts +30 -0
- package/dist/store/utils/resolve-store-name.d.ts.map +1 -0
- package/dist/store/utils/resolve-store-name.spec.d.ts +2 -0
- package/dist/store/utils/resolve-store-name.spec.d.ts.map +1 -0
- package/dist/test-helpers.spec.d.ts +224 -0
- package/dist/test-helpers.spec.d.ts.map +1 -0
- package/dist/tests/cli.integration.spec.d.ts +11 -0
- package/dist/tests/cli.integration.spec.d.ts.map +1 -0
- package/dist/toon.d.ts +197 -0
- package/dist/toon.d.ts.map +1 -0
- package/dist/toon.spec.d.ts +9 -0
- package/dist/toon.spec.d.ts.map +1 -0
- package/dist/utils/git.d.ts +20 -0
- package/dist/utils/git.d.ts.map +1 -0
- package/dist/utils/git.spec.d.ts +7 -0
- package/dist/utils/git.spec.d.ts.map +1 -0
- package/package.json +45 -0
- package/src/category/commands/create.spec.ts +139 -0
- package/src/category/commands/create.ts +115 -0
- package/src/category/index.ts +24 -0
- package/src/commands/init.spec.ts +203 -0
- package/src/commands/init.ts +301 -0
- package/src/context.spec.ts +60 -0
- package/src/context.ts +175 -0
- package/src/errors.spec.ts +264 -0
- package/src/errors.ts +105 -0
- package/src/memory/commands/add.spec.ts +169 -0
- package/src/memory/commands/add.ts +157 -0
- package/src/memory/commands/definitions.spec.ts +80 -0
- package/src/memory/commands/list.spec.ts +123 -0
- package/src/memory/commands/list.ts +268 -0
- package/src/memory/commands/move.spec.ts +85 -0
- package/src/memory/commands/move.ts +115 -0
- package/src/memory/commands/remove.spec.ts +79 -0
- package/src/memory/commands/remove.ts +104 -0
- package/src/memory/commands/show.spec.ts +71 -0
- package/src/memory/commands/show.ts +164 -0
- package/src/memory/commands/test-helpers.spec.ts +127 -0
- package/src/memory/commands/update.spec.ts +86 -0
- package/src/memory/commands/update.ts +229 -0
- package/src/memory/index.spec.ts +59 -0
- package/src/memory/index.ts +44 -0
- package/src/memory/parsing.spec.ts +105 -0
- package/src/memory/parsing.ts +22 -0
- package/src/observability.spec.ts +139 -0
- package/src/observability.ts +63 -0
- package/src/output.spec.ts +835 -0
- package/src/output.ts +119 -0
- package/src/program.spec.ts +46 -0
- package/src/program.ts +75 -0
- package/src/run.spec.ts +31 -0
- package/src/run.ts +9 -0
- package/src/store/commands/add.spec.ts +131 -0
- package/src/store/commands/add.ts +231 -0
- package/src/store/commands/init.spec.ts +236 -0
- package/src/store/commands/init.ts +256 -0
- package/src/store/commands/list.spec.ts +175 -0
- package/src/store/commands/list.ts +102 -0
- package/src/store/commands/prune.spec.ts +120 -0
- package/src/store/commands/prune.ts +152 -0
- package/src/store/commands/reindexs.spec.ts +94 -0
- package/src/store/commands/reindexs.ts +96 -0
- package/src/store/commands/remove.spec.ts +97 -0
- package/src/store/commands/remove.ts +189 -0
- package/src/store/index.spec.ts +60 -0
- package/src/store/index.ts +49 -0
- package/src/store/utils/resolve-store-name.spec.ts +62 -0
- package/src/store/utils/resolve-store-name.ts +79 -0
- package/src/test-helpers.spec.ts +430 -0
- package/src/tests/cli.integration.spec.ts +1170 -0
- package/src/toon.spec.ts +183 -0
- package/src/toon.ts +462 -0
- package/src/utils/git.spec.ts +95 -0
- package/src/utils/git.ts +51 -0
- package/src/utils/input.spec.ts +326 -0
- package/src/utils/input.ts +145 -0
- package/src/utils/paths.spec.ts +235 -0
- package/src/utils/paths.ts +75 -0
- package/src/utils/prompts.spec.ts +23 -0
- package/src/utils/prompts.ts +88 -0
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory update command implementation using Commander.js.
|
|
3
|
+
*
|
|
4
|
+
* Updates an existing memory at the specified path with new content, tags,
|
|
5
|
+
* or expiration date.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```bash
|
|
9
|
+
* # Update memory content inline
|
|
10
|
+
* cortex memory update project/tech-stack --content "Updated stack: TypeScript"
|
|
11
|
+
*
|
|
12
|
+
* # Update memory content from a file
|
|
13
|
+
* cortex memory update project/notes --file ./updated-notes.md
|
|
14
|
+
*
|
|
15
|
+
* # Update tags
|
|
16
|
+
* cortex memory update project/tech-stack --tags "typescript,nodejs,updated"
|
|
17
|
+
*
|
|
18
|
+
* # Update expiration date
|
|
19
|
+
* cortex memory update project/temp --expires-at "2026-12-31T00:00:00Z"
|
|
20
|
+
*
|
|
21
|
+
* # Clear expiration date
|
|
22
|
+
* cortex memory update project/temp --no-expires-at
|
|
23
|
+
*
|
|
24
|
+
* # Update from a specific store
|
|
25
|
+
* cortex memory --store work update project/notes --content "New content"
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { Command } from '@commander-js/extra-typings';
|
|
30
|
+
import { throwCliError } from '../../errors.ts';
|
|
31
|
+
import { MemoryPath, type CortexContext, type UpdateMemoryInput } from '@yeseh/cortex-core';
|
|
32
|
+
import { resolveInput as resolveCliContent } from '../../utils/input.ts';
|
|
33
|
+
import { parseExpiresAt, parseTags } from '../parsing.ts';
|
|
34
|
+
import { createCliCommandContext } from '../../context.ts';
|
|
35
|
+
import { serializeOutput, type OutputFormat } from '../../output.ts';
|
|
36
|
+
|
|
37
|
+
/** Options parsed by Commander for the update command */
|
|
38
|
+
export interface UpdateCommandOptions {
|
|
39
|
+
content?: string;
|
|
40
|
+
file?: string;
|
|
41
|
+
tags?: string[];
|
|
42
|
+
/**
|
|
43
|
+
* Expiration date from Commander.js option parsing.
|
|
44
|
+
* - `string` — ISO 8601 date provided via `--expires-at <date>`
|
|
45
|
+
* - `false` — expiration cleared via `--no-expires-at` negation flag
|
|
46
|
+
* - `undefined` (omitted) — keep the existing value unchanged
|
|
47
|
+
*/
|
|
48
|
+
expiresAt?: string | false;
|
|
49
|
+
citation?: string[];
|
|
50
|
+
format?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const parseUpdateExpiresAt = (raw?: string | false): Date | null | undefined => {
|
|
54
|
+
if (raw === false) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (!raw) {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return parseExpiresAt(raw);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const resolveUpdateContent = async (
|
|
66
|
+
ctx: CortexContext,
|
|
67
|
+
options: UpdateCommandOptions
|
|
68
|
+
): Promise<string | null> => {
|
|
69
|
+
if (options.content === undefined && options.file === undefined) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const content = await resolveCliContent({
|
|
74
|
+
content: options.content,
|
|
75
|
+
filePath: options.file,
|
|
76
|
+
stream: ctx.stdin,
|
|
77
|
+
// `memory update` does not read stdin.
|
|
78
|
+
stdinRequested: false,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
if (!content.ok()) {
|
|
82
|
+
throwCliError(content.error);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (!content.value.content) {
|
|
86
|
+
throwCliError({
|
|
87
|
+
code: 'MISSING_CONTENT',
|
|
88
|
+
message: 'Memory content is required via --content or --file.',
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return content.value.content;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const buildUpdates = (
|
|
96
|
+
content: string | null,
|
|
97
|
+
tags: string[] | undefined,
|
|
98
|
+
expiresAt: Date | null | undefined,
|
|
99
|
+
citations: string[] | undefined
|
|
100
|
+
): UpdateMemoryInput => {
|
|
101
|
+
const updates: UpdateMemoryInput = {};
|
|
102
|
+
if (content !== null) {
|
|
103
|
+
updates.content = content;
|
|
104
|
+
}
|
|
105
|
+
if (tags !== undefined) {
|
|
106
|
+
updates.tags = tags;
|
|
107
|
+
}
|
|
108
|
+
if (expiresAt !== undefined) {
|
|
109
|
+
updates.expiresAt = expiresAt;
|
|
110
|
+
}
|
|
111
|
+
if (citations !== undefined) {
|
|
112
|
+
updates.citations = citations;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (Object.keys(updates).length === 0) {
|
|
116
|
+
throwCliError({
|
|
117
|
+
code: 'INVALID_ARGUMENTS',
|
|
118
|
+
message:
|
|
119
|
+
'No updates provided. Use --content, --file, --tags, --citation, or expiry flags.',
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return updates;
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Handler for the memory update command.
|
|
128
|
+
* Exported for direct testing without Commander parsing.
|
|
129
|
+
*
|
|
130
|
+
* @param ctx - CLI context containing Cortex client and streams
|
|
131
|
+
* @param storeName - Optional store name from parent command
|
|
132
|
+
* @param path - Memory path to update (e.g., "project/tech-stack")
|
|
133
|
+
* @param options - Command options from Commander
|
|
134
|
+
*/
|
|
135
|
+
export async function handleUpdate(
|
|
136
|
+
ctx: CortexContext,
|
|
137
|
+
storeName: string | undefined,
|
|
138
|
+
path: string,
|
|
139
|
+
options: UpdateCommandOptions
|
|
140
|
+
): Promise<void> {
|
|
141
|
+
const pathResult = MemoryPath.fromString(path);
|
|
142
|
+
if (!pathResult.ok()) {
|
|
143
|
+
throwCliError(pathResult.error);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const content = await resolveUpdateContent(ctx, options);
|
|
147
|
+
const tags = options.tags === undefined ? undefined : parseTags(options.tags);
|
|
148
|
+
const expiresAt = parseUpdateExpiresAt(options.expiresAt);
|
|
149
|
+
const updates = buildUpdates(content, tags, expiresAt, options.citation);
|
|
150
|
+
|
|
151
|
+
const storeResult = ctx.cortex.getStore(storeName ?? 'global');
|
|
152
|
+
if (!storeResult.ok()) {
|
|
153
|
+
throwCliError(storeResult.error);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const store = storeResult.value;
|
|
157
|
+
const rootResult = store.root();
|
|
158
|
+
if (!rootResult.ok()) {
|
|
159
|
+
throwCliError(rootResult.error);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const categoryResult = pathResult.value.category.isRoot
|
|
163
|
+
? rootResult
|
|
164
|
+
: rootResult.value.getCategory(pathResult.value.category.toString());
|
|
165
|
+
if (!categoryResult.ok()) {
|
|
166
|
+
throwCliError(categoryResult.error);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const memoryClient = categoryResult.value.getMemory(pathResult.value.slug.toString());
|
|
170
|
+
const updateResult = await memoryClient.update(updates);
|
|
171
|
+
if (!updateResult.ok()) {
|
|
172
|
+
throwCliError(updateResult.error);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const memory = updateResult.value;
|
|
176
|
+
const stdout = ctx.stdout ?? process.stdout;
|
|
177
|
+
|
|
178
|
+
const rawFormat = options.format;
|
|
179
|
+
if (!rawFormat) {
|
|
180
|
+
stdout.write(`Updated memory ${memory.path.toString()}.\n`);
|
|
181
|
+
} else {
|
|
182
|
+
const format = rawFormat as OutputFormat;
|
|
183
|
+
const serialized = serializeOutput(
|
|
184
|
+
{
|
|
185
|
+
kind: 'memory',
|
|
186
|
+
value: {
|
|
187
|
+
path: memory.path.toString(),
|
|
188
|
+
metadata: memory.metadata,
|
|
189
|
+
content: memory.content,
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
format
|
|
193
|
+
);
|
|
194
|
+
if (!serialized.ok()) {
|
|
195
|
+
throwCliError({ code: 'SERIALIZE_FAILED', message: serialized.error.message });
|
|
196
|
+
}
|
|
197
|
+
stdout.write(serialized.value + '\n');
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* The `memory update` subcommand.
|
|
203
|
+
*
|
|
204
|
+
* Updates an existing memory at the specified path. Can update:
|
|
205
|
+
* - Content via `--content` flag for inline text or `--file` to read from a file
|
|
206
|
+
* - Tags via `--tags` flag (replaces existing tags)
|
|
207
|
+
* - Expiration via `--expires-at` or `--no-expires-at`
|
|
208
|
+
*
|
|
209
|
+
* The `--store` option is inherited from the parent `memory` command.
|
|
210
|
+
*/
|
|
211
|
+
export const updateCommand = new Command('update')
|
|
212
|
+
.description('Update an existing memory')
|
|
213
|
+
.argument('<path>', 'Memory path to update')
|
|
214
|
+
.option('-c, --content <text>', 'New memory content as inline text')
|
|
215
|
+
.option('-f, --file <filepath>', 'Read new content from a file')
|
|
216
|
+
.option('-t, --tags <value...>', 'Tags (can be repeated or comma-separated, replaces existing)')
|
|
217
|
+
.option('-e, --expires-at <date>', 'New expiration date (ISO 8601)')
|
|
218
|
+
.option('--no-expires-at', 'Remove expiration date')
|
|
219
|
+
.option('--citation <value...>', 'Citation references (replaces existing)')
|
|
220
|
+
.option('-o, --format <format>', 'Output format (yaml, json, toon)')
|
|
221
|
+
.action(async (path, options, command) => {
|
|
222
|
+
const parentOpts = command.parent?.opts() as { store?: string } | undefined;
|
|
223
|
+
const context = await createCliCommandContext();
|
|
224
|
+
if (!context.ok()) {
|
|
225
|
+
throwCliError(context.error);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
await handleUpdate(context.value, parentOpts?.store, path, options);
|
|
229
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for memory command group wiring.
|
|
3
|
+
*
|
|
4
|
+
* Verifies that the `memory` command group is correctly configured with
|
|
5
|
+
* the expected name, description, options, and subcommands.
|
|
6
|
+
*
|
|
7
|
+
* @module cli/memory/index.spec
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, it, expect } from 'bun:test';
|
|
11
|
+
|
|
12
|
+
import { memoryCommand } from './index.ts';
|
|
13
|
+
|
|
14
|
+
describe('memoryCommand', () => {
|
|
15
|
+
it('should have name "memory"', () => {
|
|
16
|
+
expect(memoryCommand.name()).toBe('memory');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('should have description', () => {
|
|
20
|
+
expect(memoryCommand.description()).toBeTruthy();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should have --store option', () => {
|
|
24
|
+
const options = memoryCommand.options;
|
|
25
|
+
const storeOption = options.find((o) => o.long === '--store');
|
|
26
|
+
expect(storeOption).toBeDefined();
|
|
27
|
+
expect(storeOption?.short).toBe('-s');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should have "add" subcommand registered', () => {
|
|
31
|
+
const names = memoryCommand.commands.map((c) => c.name());
|
|
32
|
+
expect(names).toContain('add');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should have "show" subcommand registered', () => {
|
|
36
|
+
const names = memoryCommand.commands.map((c) => c.name());
|
|
37
|
+
expect(names).toContain('show');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should have "update" subcommand registered', () => {
|
|
41
|
+
const names = memoryCommand.commands.map((c) => c.name());
|
|
42
|
+
expect(names).toContain('update');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should have "remove" subcommand registered', () => {
|
|
46
|
+
const names = memoryCommand.commands.map((c) => c.name());
|
|
47
|
+
expect(names).toContain('remove');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should have "move" subcommand registered', () => {
|
|
51
|
+
const names = memoryCommand.commands.map((c) => c.name());
|
|
52
|
+
expect(names).toContain('move');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should have "list" subcommand registered', () => {
|
|
56
|
+
const names = memoryCommand.commands.map((c) => c.name());
|
|
57
|
+
expect(names).toContain('list');
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory command group for the CLI.
|
|
3
|
+
*
|
|
4
|
+
* This module defines the `memory` command group, which provides operations
|
|
5
|
+
* for managing memories in the Cortex memory system. The `--store` option
|
|
6
|
+
* is defined at the group level and inherited by all subcommands.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```bash
|
|
10
|
+
* # Use default store
|
|
11
|
+
* cortex memory add project/notes --content "Hello"
|
|
12
|
+
*
|
|
13
|
+
* # Use specific store
|
|
14
|
+
* cortex memory --store my-store add project/notes --content "Hello"
|
|
15
|
+
* cortex memory -s my-store list
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { Command } from '@commander-js/extra-typings';
|
|
20
|
+
|
|
21
|
+
import { addCommand } from './commands/add.ts';
|
|
22
|
+
import { showCommand } from './commands/show.ts';
|
|
23
|
+
import { updateCommand } from './commands/update.ts';
|
|
24
|
+
import { removeCommand } from './commands/remove.ts';
|
|
25
|
+
import { moveCommand } from './commands/move.ts';
|
|
26
|
+
import { listCommand } from './commands/list.ts';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The `memory` command group.
|
|
30
|
+
*
|
|
31
|
+
* Provides memory management operations. The `--store` option allows
|
|
32
|
+
* targeting a specific named store instead of the default store.
|
|
33
|
+
* This option is inherited by all subcommands.
|
|
34
|
+
*/
|
|
35
|
+
export const memoryCommand = new Command('memory')
|
|
36
|
+
.description('Memory operations')
|
|
37
|
+
.option('-s, --store <name>', 'Use a specific named store');
|
|
38
|
+
|
|
39
|
+
memoryCommand.addCommand(addCommand);
|
|
40
|
+
memoryCommand.addCommand(showCommand);
|
|
41
|
+
memoryCommand.addCommand(updateCommand);
|
|
42
|
+
memoryCommand.addCommand(removeCommand);
|
|
43
|
+
memoryCommand.addCommand(moveCommand);
|
|
44
|
+
memoryCommand.addCommand(listCommand);
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for memory/parsing.ts
|
|
3
|
+
*
|
|
4
|
+
* @module cli/memory/parsing.spec
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect } from 'bun:test';
|
|
8
|
+
import { parseTags, parseExpiresAt } from './parsing';
|
|
9
|
+
import { expectInvalidArgumentError } from '../test-helpers.spec';
|
|
10
|
+
|
|
11
|
+
// ============================================================================
|
|
12
|
+
// parseTags
|
|
13
|
+
// ============================================================================
|
|
14
|
+
|
|
15
|
+
describe('parseTags', () => {
|
|
16
|
+
it('should return empty array for undefined input', () => {
|
|
17
|
+
expect(parseTags(undefined)).toEqual([]);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('should return empty array for empty array input', () => {
|
|
21
|
+
expect(parseTags([])).toEqual([]);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('should split comma-separated tags from a single string', () => {
|
|
25
|
+
expect(parseTags(['foo,bar,baz'])).toEqual([
|
|
26
|
+
'foo',
|
|
27
|
+
'bar',
|
|
28
|
+
'baz',
|
|
29
|
+
]);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should handle multiple string entries in the array', () => {
|
|
33
|
+
expect(parseTags([
|
|
34
|
+
'foo', 'bar',
|
|
35
|
+
])).toEqual([
|
|
36
|
+
'foo', 'bar',
|
|
37
|
+
]);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should trim whitespace from each tag', () => {
|
|
41
|
+
expect(parseTags([' foo , bar , baz '])).toEqual([
|
|
42
|
+
'foo',
|
|
43
|
+
'bar',
|
|
44
|
+
'baz',
|
|
45
|
+
]);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should filter empty strings after splitting and trimming', () => {
|
|
49
|
+
expect(parseTags([
|
|
50
|
+
',,,', ' , ',
|
|
51
|
+
])).toEqual([]);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should preserve duplicate tags without deduplication', () => {
|
|
55
|
+
expect(parseTags([
|
|
56
|
+
'foo,foo', 'foo',
|
|
57
|
+
])).toEqual([
|
|
58
|
+
'foo',
|
|
59
|
+
'foo',
|
|
60
|
+
'foo',
|
|
61
|
+
]);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// ============================================================================
|
|
66
|
+
// parseExpiresAt
|
|
67
|
+
// ============================================================================
|
|
68
|
+
|
|
69
|
+
describe('parseExpiresAt', () => {
|
|
70
|
+
it('should return undefined for undefined input', () => {
|
|
71
|
+
expect(parseExpiresAt(undefined)).toBeUndefined();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should return undefined for empty string', () => {
|
|
75
|
+
expect(parseExpiresAt('')).toBeUndefined();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('should return a Date for valid ISO 8601 string', () => {
|
|
79
|
+
const result = parseExpiresAt('2025-12-31T23:59:59.000Z');
|
|
80
|
+
expect(result).toBeInstanceOf(Date);
|
|
81
|
+
expect(result!.toISOString()).toBe('2025-12-31T23:59:59.000Z');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should return a Date for valid date-only string "2025-12-31"', () => {
|
|
85
|
+
const result = parseExpiresAt('2025-12-31');
|
|
86
|
+
expect(result).toBeInstanceOf(Date);
|
|
87
|
+
expect(result!.getFullYear()).toBe(2025);
|
|
88
|
+
expect(result!.getMonth()).toBe(11); // 0-indexed December
|
|
89
|
+
expect(result!.getDate()).toBe(31);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('should throw InvalidArgumentError for invalid date string "not-a-date"', async () => {
|
|
93
|
+
await expectInvalidArgumentError(
|
|
94
|
+
() => parseExpiresAt('not-a-date'),
|
|
95
|
+
'Invalid expiration date format',
|
|
96
|
+
);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('should throw InvalidArgumentError for garbage string "abc123"', async () => {
|
|
100
|
+
await expectInvalidArgumentError(
|
|
101
|
+
() => parseExpiresAt('abc123'),
|
|
102
|
+
'Invalid expiration date format',
|
|
103
|
+
);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { throwCliError } from '../errors';
|
|
2
|
+
|
|
3
|
+
export const parseTags = (raw?: string[]): string[] =>
|
|
4
|
+
raw
|
|
5
|
+
? raw
|
|
6
|
+
.flatMap((tag) => tag.split(','))
|
|
7
|
+
.map((tag) => tag.trim())
|
|
8
|
+
.filter(Boolean)
|
|
9
|
+
: [];
|
|
10
|
+
|
|
11
|
+
export const parseExpiresAt = (raw?: string): Date | undefined => {
|
|
12
|
+
if (!raw) {
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const parsed = new Date(raw);
|
|
17
|
+
if (Number.isNaN(parsed.getTime())) {
|
|
18
|
+
throwCliError({ code: 'INVALID_ARGUMENTS', message: 'Invalid expiration date format' });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return parsed;
|
|
22
|
+
};
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'bun:test';
|
|
2
|
+
import { createCliLogger } from './observability.ts';
|
|
3
|
+
|
|
4
|
+
describe('createCliLogger', () => {
|
|
5
|
+
let stderrLines: string[];
|
|
6
|
+
let origWrite: typeof process.stderr.write;
|
|
7
|
+
let origDebugEnv: string | undefined;
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
stderrLines = [];
|
|
11
|
+
origWrite = process.stderr.write.bind(process.stderr);
|
|
12
|
+
origDebugEnv = process.env.DEBUG;
|
|
13
|
+
process.stderr.write = ((chunk: string | Uint8Array) => {
|
|
14
|
+
stderrLines.push(typeof chunk === 'string' ? chunk : Buffer.from(chunk).toString());
|
|
15
|
+
return true;
|
|
16
|
+
}) as any;
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
process.stderr.write = origWrite;
|
|
21
|
+
if (origDebugEnv === undefined) {
|
|
22
|
+
delete process.env.DEBUG;
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
process.env.DEBUG = origDebugEnv;
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe('info()', () => {
|
|
30
|
+
it('should write info log to stderr as JSON', () => {
|
|
31
|
+
const logger = createCliLogger();
|
|
32
|
+
logger.info('hello world');
|
|
33
|
+
expect(stderrLines.length).toBe(1);
|
|
34
|
+
const parsed = JSON.parse(stderrLines[0]!);
|
|
35
|
+
expect(parsed.level).toBe('info');
|
|
36
|
+
expect(parsed.msg).toBe('hello world');
|
|
37
|
+
expect(parsed.ts).toBeDefined();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should include metadata in the log line', () => {
|
|
41
|
+
const logger = createCliLogger();
|
|
42
|
+
logger.info('test', { store: 'default', count: 5 });
|
|
43
|
+
const parsed = JSON.parse(stderrLines[0]!);
|
|
44
|
+
expect(parsed.store).toBe('default');
|
|
45
|
+
expect(parsed.count).toBe(5);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe('warn()', () => {
|
|
50
|
+
it('should write warn log to stderr', () => {
|
|
51
|
+
const logger = createCliLogger();
|
|
52
|
+
logger.warn('warning message');
|
|
53
|
+
expect(stderrLines.length).toBe(1);
|
|
54
|
+
const parsed = JSON.parse(stderrLines[0]!);
|
|
55
|
+
expect(parsed.level).toBe('warn');
|
|
56
|
+
expect(parsed.msg).toBe('warning message');
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe('error()', () => {
|
|
61
|
+
it('should write error log with Error object details', () => {
|
|
62
|
+
const logger = createCliLogger();
|
|
63
|
+
logger.error('something failed', new Error('boom'));
|
|
64
|
+
expect(stderrLines.length).toBe(1);
|
|
65
|
+
const parsed = JSON.parse(stderrLines[0]!);
|
|
66
|
+
expect(parsed.level).toBe('error');
|
|
67
|
+
expect(parsed.msg).toBe('something failed');
|
|
68
|
+
expect(parsed.error).toBe('boom');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('should handle string error argument', () => {
|
|
72
|
+
const logger = createCliLogger();
|
|
73
|
+
logger.error('failed', 'string error');
|
|
74
|
+
const parsed = JSON.parse(stderrLines[0]!);
|
|
75
|
+
expect(parsed.error).toBe('string error');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('should handle missing error argument', () => {
|
|
79
|
+
const logger = createCliLogger();
|
|
80
|
+
logger.error('failed');
|
|
81
|
+
expect(stderrLines.length).toBe(1);
|
|
82
|
+
const parsed = JSON.parse(stderrLines[0]!);
|
|
83
|
+
expect(parsed.error).toBeUndefined();
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe('debug()', () => {
|
|
88
|
+
it('should suppress debug output when DEBUG env is not set', () => {
|
|
89
|
+
delete process.env.DEBUG;
|
|
90
|
+
const logger = createCliLogger();
|
|
91
|
+
logger.debug('debug message');
|
|
92
|
+
expect(stderrLines.length).toBe(0);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should write debug output when DEBUG=cortex', () => {
|
|
96
|
+
process.env.DEBUG = 'cortex';
|
|
97
|
+
const logger = createCliLogger();
|
|
98
|
+
logger.debug('debug message');
|
|
99
|
+
expect(stderrLines.length).toBe(1);
|
|
100
|
+
const parsed = JSON.parse(stderrLines[0]!);
|
|
101
|
+
expect(parsed.level).toBe('debug');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('should write debug output when DEBUG includes cortex alongside other values', () => {
|
|
105
|
+
process.env.DEBUG = 'express,cortex,http';
|
|
106
|
+
const logger = createCliLogger();
|
|
107
|
+
logger.debug('debug message');
|
|
108
|
+
expect(stderrLines.length).toBe(1);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('should suppress debug when DEBUG is set to a different value', () => {
|
|
112
|
+
process.env.DEBUG = 'express';
|
|
113
|
+
const logger = createCliLogger();
|
|
114
|
+
logger.debug('debug message');
|
|
115
|
+
expect(stderrLines.length).toBe(0);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe('Logger interface compliance', () => {
|
|
120
|
+
it('should not write to stdout', () => {
|
|
121
|
+
const stdoutLines: string[] = [];
|
|
122
|
+
const origStdoutWrite = process.stdout.write.bind(process.stdout);
|
|
123
|
+
process.stdout.write = ((chunk: string | Uint8Array) => {
|
|
124
|
+
stdoutLines.push(typeof chunk === 'string' ? chunk : Buffer.from(chunk).toString());
|
|
125
|
+
return true;
|
|
126
|
+
}) as any;
|
|
127
|
+
try {
|
|
128
|
+
const logger = createCliLogger();
|
|
129
|
+
logger.info('test');
|
|
130
|
+
logger.warn('test');
|
|
131
|
+
logger.error('test');
|
|
132
|
+
expect(stdoutLines.length).toBe(0);
|
|
133
|
+
}
|
|
134
|
+
finally {
|
|
135
|
+
process.stdout.write = origStdoutWrite;
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI observability — plain ConsoleLogger writing structured JSON to stderr.
|
|
3
|
+
*
|
|
4
|
+
* No OTel SDK dependency — keeps the CLI binary small.
|
|
5
|
+
* Debug output is gated by the `DEBUG=cortex` environment variable.
|
|
6
|
+
*
|
|
7
|
+
* @module cli/observability
|
|
8
|
+
*/
|
|
9
|
+
import type { Logger } from '@yeseh/cortex-core';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Creates a plain console logger for CLI usage.
|
|
13
|
+
*
|
|
14
|
+
* Writes structured JSON log lines to stderr (not stdout) to avoid
|
|
15
|
+
* polluting piped command output. Debug output is gated by the
|
|
16
|
+
* `DEBUG=cortex` environment variable.
|
|
17
|
+
*
|
|
18
|
+
* @returns Logger instance writing to stderr
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```typescript
|
|
22
|
+
* const logger = createCliLogger();
|
|
23
|
+
* logger.info('Starting command', { store: 'global' });
|
|
24
|
+
* // → {"ts":"2024-01-01T00:00:00.000Z","level":"info","msg":"Starting command","store":"global"}
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```bash
|
|
29
|
+
* # Enable debug output
|
|
30
|
+
* DEBUG=cortex cortex memory list
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export const createCliLogger = (): Logger => {
|
|
34
|
+
const debugEnabled =
|
|
35
|
+
typeof process.env.DEBUG === 'string' && process.env.DEBUG.includes('cortex');
|
|
36
|
+
|
|
37
|
+
const write = (level: string, msg: string, meta?: Record<string, unknown>): void => {
|
|
38
|
+
process.stderr.write(
|
|
39
|
+
JSON.stringify({ ts: new Date().toISOString(), level, msg, ...meta }) + '\n'
|
|
40
|
+
);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
debug(msg: string, meta?: Record<string, unknown>): void {
|
|
45
|
+
if (debugEnabled) write('debug', msg, meta);
|
|
46
|
+
},
|
|
47
|
+
info(msg: string, meta?: Record<string, unknown>): void {
|
|
48
|
+
write('info', msg, meta);
|
|
49
|
+
},
|
|
50
|
+
warn(msg: string, meta?: Record<string, unknown>): void {
|
|
51
|
+
write('warn', msg, meta);
|
|
52
|
+
},
|
|
53
|
+
error(msg: string, err?: Error | unknown, meta?: Record<string, unknown>): void {
|
|
54
|
+
const errMeta =
|
|
55
|
+
err instanceof Error
|
|
56
|
+
? { error: err.message, stack: err.stack }
|
|
57
|
+
: err !== null && err !== undefined
|
|
58
|
+
? { error: String(err) }
|
|
59
|
+
: {};
|
|
60
|
+
write('error', msg, { ...meta, ...errMeta });
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
};
|