@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,79 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
|
|
2
|
+
import { CommanderError, InvalidArgumentError } from '@commander-js/extra-typings';
|
|
3
|
+
import { mkdtemp, rm } from 'node:fs/promises';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { MemoryPath, ok } from '@yeseh/cortex-core';
|
|
7
|
+
|
|
8
|
+
import { handleRemove } from './remove.ts';
|
|
9
|
+
import {
|
|
10
|
+
createCaptureStream,
|
|
11
|
+
createMemoryCommandContext,
|
|
12
|
+
createMemoryFixture,
|
|
13
|
+
createMockMemoryCommandAdapter,
|
|
14
|
+
} from './test-helpers.spec.ts';
|
|
15
|
+
|
|
16
|
+
describe('handleRemove', () => {
|
|
17
|
+
let tempDir: string;
|
|
18
|
+
|
|
19
|
+
beforeEach(async () => {
|
|
20
|
+
tempDir = await mkdtemp(join(tmpdir(), 'cortex-cli-memory-remove-'));
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
afterEach(async () => {
|
|
24
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should throw InvalidArgumentError for invalid memory paths', async () => {
|
|
28
|
+
const ctx = createMemoryCommandContext({
|
|
29
|
+
adapter: createMockMemoryCommandAdapter(),
|
|
30
|
+
storePath: tempDir,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
await expect(handleRemove(ctx, undefined, 'invalid')).rejects.toThrow(
|
|
34
|
+
InvalidArgumentError,
|
|
35
|
+
);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('should throw CommanderError when store is missing', async () => {
|
|
39
|
+
const ctx = createMemoryCommandContext({
|
|
40
|
+
adapter: createMockMemoryCommandAdapter(),
|
|
41
|
+
storePath: tempDir,
|
|
42
|
+
stores: undefined,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
await expect(handleRemove(ctx, 'missing-store', 'project/one')).rejects.toThrow(
|
|
46
|
+
CommanderError,
|
|
47
|
+
);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should remove memory and report output', async () => {
|
|
51
|
+
const memory = createMemoryFixture('project/remove');
|
|
52
|
+
const removeCalls: MemoryPath[] = [];
|
|
53
|
+
|
|
54
|
+
const adapter = createMockMemoryCommandAdapter({
|
|
55
|
+
memories: {
|
|
56
|
+
load: async () => ok(memory),
|
|
57
|
+
remove: async (path: MemoryPath) => {
|
|
58
|
+
removeCalls.push(path);
|
|
59
|
+
return ok(undefined);
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const capture = createCaptureStream();
|
|
65
|
+
const ctx = createMemoryCommandContext({
|
|
66
|
+
adapter,
|
|
67
|
+
storePath: tempDir,
|
|
68
|
+
stdout: capture.stream,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
await handleRemove(ctx, undefined, 'project/remove');
|
|
72
|
+
|
|
73
|
+
expect(removeCalls).toHaveLength(1);
|
|
74
|
+
if (removeCalls[0]) {
|
|
75
|
+
expect(removeCalls[0].toString()).toBe('project/remove');
|
|
76
|
+
}
|
|
77
|
+
expect(capture.getOutput()).toContain('Removed memory project/remove.');
|
|
78
|
+
});
|
|
79
|
+
});
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory remove command implementation using Commander.js.
|
|
3
|
+
*
|
|
4
|
+
* Deletes an existing memory at the specified path.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```bash
|
|
8
|
+
* # Remove a memory
|
|
9
|
+
* cortex memory remove project/tech-stack
|
|
10
|
+
*
|
|
11
|
+
* # Remove memory from a specific store
|
|
12
|
+
* cortex memory --store work remove project/notes
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { Command } from '@commander-js/extra-typings';
|
|
17
|
+
import { throwCliError } from '../../errors.ts';
|
|
18
|
+
import { MemoryPath, type CortexContext } from '@yeseh/cortex-core';
|
|
19
|
+
import { createCliCommandContext } from '../../context.ts';
|
|
20
|
+
import { serializeOutput, type OutputFormat } from '../../output.ts';
|
|
21
|
+
|
|
22
|
+
/** Options for the remove command. */
|
|
23
|
+
export interface RemoveCommandOptions {
|
|
24
|
+
/** Output format (yaml, json, toon) */
|
|
25
|
+
format?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Handler for the memory remove command.
|
|
30
|
+
* Exported for direct testing without Commander parsing.
|
|
31
|
+
*
|
|
32
|
+
* @param ctx - CLI context containing Cortex client and streams
|
|
33
|
+
* @param storeName - Optional store name from parent command
|
|
34
|
+
* @param path - Memory path to remove (e.g., "project/tech-stack")
|
|
35
|
+
*/
|
|
36
|
+
export async function handleRemove(
|
|
37
|
+
ctx: CortexContext,
|
|
38
|
+
storeName: string | undefined,
|
|
39
|
+
path: string,
|
|
40
|
+
options: RemoveCommandOptions = {}
|
|
41
|
+
): Promise<void> {
|
|
42
|
+
const pathResult = MemoryPath.fromString(path);
|
|
43
|
+
if (!pathResult.ok()) {
|
|
44
|
+
throwCliError(pathResult.error);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const storeResult = ctx.cortex.getStore(storeName ?? 'global');
|
|
48
|
+
if (!storeResult.ok()) {
|
|
49
|
+
throwCliError(storeResult.error);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const store = storeResult.value;
|
|
53
|
+
const rootResult = store.root();
|
|
54
|
+
if (!rootResult.ok()) {
|
|
55
|
+
throwCliError(rootResult.error);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const categoryResult = pathResult.value.category.isRoot
|
|
59
|
+
? rootResult
|
|
60
|
+
: rootResult.value.getCategory(pathResult.value.category.toString());
|
|
61
|
+
if (!categoryResult.ok()) {
|
|
62
|
+
throwCliError(categoryResult.error);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const memoryClient = categoryResult.value.getMemory(pathResult.value.slug.toString());
|
|
66
|
+
const removeResult = await memoryClient.delete();
|
|
67
|
+
if (!removeResult.ok()) {
|
|
68
|
+
throwCliError(removeResult.error);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const out = ctx.stdout ?? process.stdout;
|
|
72
|
+
const rawFormat = options.format;
|
|
73
|
+
if (!rawFormat) {
|
|
74
|
+
out.write(`Removed memory ${pathResult.value.toString()}.\n`);
|
|
75
|
+
} else {
|
|
76
|
+
const format = rawFormat as OutputFormat;
|
|
77
|
+
const serialized = serializeOutput({kind: 'path', value: { path: pathResult.value.toString() }}, format);
|
|
78
|
+
if (!serialized.ok()) {
|
|
79
|
+
throwCliError({ code: 'SERIALIZE_FAILED', message: serialized.error.message });
|
|
80
|
+
}
|
|
81
|
+
out.write(serialized.value + '\n');
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* The `memory remove` subcommand.
|
|
87
|
+
*
|
|
88
|
+
* Deletes an existing memory at the specified path.
|
|
89
|
+
*
|
|
90
|
+
* The `--store` option is inherited from the parent `memory` command.
|
|
91
|
+
*/
|
|
92
|
+
export const removeCommand = new Command('remove')
|
|
93
|
+
.description('Delete a memory')
|
|
94
|
+
.argument('<path>', 'Memory path to remove')
|
|
95
|
+
.option('-o, --format <format>', 'Output format (yaml, json, toon)')
|
|
96
|
+
.action(async (path, options, command) => {
|
|
97
|
+
const parentOpts = command.parent?.opts() as { store?: string } | undefined;
|
|
98
|
+
const context = await createCliCommandContext();
|
|
99
|
+
if (!context.ok()) {
|
|
100
|
+
throwCliError(context.error);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
await handleRemove(context.value, parentOpts?.store, path, options);
|
|
104
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
|
|
2
|
+
import { CommanderError, InvalidArgumentError } from '@commander-js/extra-typings';
|
|
3
|
+
import { mkdtemp, rm } from 'node:fs/promises';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { ok } from '@yeseh/cortex-core';
|
|
7
|
+
|
|
8
|
+
import { handleShow } from './show.ts';
|
|
9
|
+
import {
|
|
10
|
+
createCaptureStream,
|
|
11
|
+
createMemoryCommandContext,
|
|
12
|
+
createMemoryFixture,
|
|
13
|
+
createMockMemoryCommandAdapter,
|
|
14
|
+
} from './test-helpers.spec.ts';
|
|
15
|
+
|
|
16
|
+
describe('handleShow', () => {
|
|
17
|
+
let tempDir: string;
|
|
18
|
+
|
|
19
|
+
beforeEach(async () => {
|
|
20
|
+
tempDir = await mkdtemp(join(tmpdir(), 'cortex-cli-memory-show-'));
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
afterEach(async () => {
|
|
24
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should throw InvalidArgumentError for invalid memory paths', async () => {
|
|
28
|
+
const ctx = createMemoryCommandContext({
|
|
29
|
+
adapter: createMockMemoryCommandAdapter(),
|
|
30
|
+
storePath: tempDir,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
await expect(handleShow(ctx, undefined, 'invalid', { format: 'yaml' })).rejects.toThrow(
|
|
34
|
+
InvalidArgumentError,
|
|
35
|
+
);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('should throw CommanderError when store is missing', async () => {
|
|
39
|
+
const ctx = createMemoryCommandContext({
|
|
40
|
+
adapter: createMockMemoryCommandAdapter(),
|
|
41
|
+
storePath: tempDir,
|
|
42
|
+
stores: undefined,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
await expect(handleShow(ctx, 'missing-store', 'project/one', { format: 'yaml' })).rejects.toThrow(
|
|
46
|
+
CommanderError,
|
|
47
|
+
);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should output serialized memory details', async () => {
|
|
51
|
+
const memory = createMemoryFixture('project/show', {}, 'Show content');
|
|
52
|
+
const adapter = createMockMemoryCommandAdapter({
|
|
53
|
+
memories: {
|
|
54
|
+
load: async () => ok(memory),
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
const capture = createCaptureStream();
|
|
58
|
+
const ctx = createMemoryCommandContext({
|
|
59
|
+
adapter,
|
|
60
|
+
storePath: tempDir,
|
|
61
|
+
stdout: capture.stream,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
await handleShow(ctx, undefined, 'project/show', { format: 'json' });
|
|
65
|
+
|
|
66
|
+
const output = JSON.parse(capture.getOutput());
|
|
67
|
+
expect(output.value.path).toBe('project/show');
|
|
68
|
+
expect(output.value.content).toBe('Show content');
|
|
69
|
+
expect(output.value.metadata.tags).toEqual(['test']);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory show command for displaying a memory's content and metadata.
|
|
3
|
+
*
|
|
4
|
+
* This command reads a memory from the store and displays it in the
|
|
5
|
+
* specified format. Expired memories are excluded by default unless
|
|
6
|
+
* the `--include-expired` flag is provided.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```bash
|
|
10
|
+
* # Show a memory in YAML format (default)
|
|
11
|
+
* cortex memory show project/notes
|
|
12
|
+
*
|
|
13
|
+
* # Show a memory in JSON format
|
|
14
|
+
* cortex memory show project/notes --format json
|
|
15
|
+
*
|
|
16
|
+
* # Include expired memories
|
|
17
|
+
* cortex memory show project/notes --include-expired
|
|
18
|
+
*
|
|
19
|
+
* # Use a specific store
|
|
20
|
+
* cortex memory --store my-store show project/notes
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { Command } from '@commander-js/extra-typings';
|
|
25
|
+
import { throwCliError } from '../../errors.ts';
|
|
26
|
+
|
|
27
|
+
import { defaultTokenizer, MemoryPath, type CortexContext } from '@yeseh/cortex-core';
|
|
28
|
+
import { type StoreClient } from '@yeseh/cortex-core/store';
|
|
29
|
+
import { serializeOutput, type OutputMemory, type OutputFormat } from '../../output.ts';
|
|
30
|
+
import { createCliCommandContext } from '../../context.ts';
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Options for the show command.
|
|
34
|
+
*/
|
|
35
|
+
export interface ShowCommandOptions {
|
|
36
|
+
/** Include expired memories in the output */
|
|
37
|
+
includeExpired?: boolean;
|
|
38
|
+
/** Output format (yaml, json, toon) */
|
|
39
|
+
format?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Dependencies for the show command handler.
|
|
44
|
+
* Allows injection for testing.
|
|
45
|
+
*/
|
|
46
|
+
export interface ShowHandlerDeps {
|
|
47
|
+
/** Output stream for writing results (defaults to process.stdout) */
|
|
48
|
+
stdout?: NodeJS.WritableStream;
|
|
49
|
+
/** Pre-resolved store client for testing */
|
|
50
|
+
store?: StoreClient;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Handles the show command execution.
|
|
55
|
+
*
|
|
56
|
+
* This function:
|
|
57
|
+
* 1. Resolves the store context
|
|
58
|
+
* 2. Validates the memory path
|
|
59
|
+
* 3. Reads the memory file from storage
|
|
60
|
+
* 4. Parses the memory content and frontmatter
|
|
61
|
+
* 5. Checks expiration status (unless --include-expired)
|
|
62
|
+
* 6. Serializes and outputs the result
|
|
63
|
+
*
|
|
64
|
+
* @param path - The memory path to show (e.g., "project/notes")
|
|
65
|
+
* @param options - Command options (includeExpired, format)
|
|
66
|
+
* @param storeName - Optional store name from parent command
|
|
67
|
+
* @param deps - Optional dependencies for testing
|
|
68
|
+
* @throws {InvalidArgumentError} When the path is invalid
|
|
69
|
+
* @throws {CommanderError} When the memory is not found or read fails
|
|
70
|
+
*/
|
|
71
|
+
export async function handleShow(
|
|
72
|
+
ctx: CortexContext,
|
|
73
|
+
storeName: string | undefined,
|
|
74
|
+
path: string,
|
|
75
|
+
options: ShowCommandOptions,
|
|
76
|
+
deps: ShowHandlerDeps = {}
|
|
77
|
+
): Promise<void> {
|
|
78
|
+
const pathResult = MemoryPath.fromString(path);
|
|
79
|
+
if (!pathResult.ok()) {
|
|
80
|
+
throwCliError(pathResult.error);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const storeResult = ctx.cortex.getStore(storeName ?? 'global');
|
|
84
|
+
if (!storeResult.ok()) {
|
|
85
|
+
throwCliError(storeResult.error);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const store = deps.store ?? storeResult.value;
|
|
89
|
+
|
|
90
|
+
const rootResult = store.root();
|
|
91
|
+
if (!rootResult.ok()) {
|
|
92
|
+
throwCliError(rootResult.error);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const categoryResult = pathResult.value.category.isRoot
|
|
96
|
+
? rootResult
|
|
97
|
+
: rootResult.value.getCategory(pathResult.value.category.toString());
|
|
98
|
+
if (!categoryResult.ok()) {
|
|
99
|
+
throwCliError(categoryResult.error);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const memoryClient = categoryResult.value.getMemory(pathResult.value.slug.toString());
|
|
103
|
+
const readResult = await memoryClient.get({
|
|
104
|
+
includeExpired: options.includeExpired ?? false,
|
|
105
|
+
now: ctx.now(),
|
|
106
|
+
});
|
|
107
|
+
if (!readResult.ok()) {
|
|
108
|
+
throwCliError(readResult.error);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const memory = readResult.value;
|
|
112
|
+
const tokenEstimateResult = defaultTokenizer.estimateTokens(memory.content);
|
|
113
|
+
const tokenEstimate = tokenEstimateResult.ok() ? tokenEstimateResult.value : undefined;
|
|
114
|
+
|
|
115
|
+
const outputMemory: OutputMemory = {
|
|
116
|
+
path: memory.path.toString(),
|
|
117
|
+
metadata: {
|
|
118
|
+
createdAt: memory.metadata.createdAt,
|
|
119
|
+
updatedAt: memory.metadata.updatedAt,
|
|
120
|
+
tags: memory.metadata.tags,
|
|
121
|
+
source: memory.metadata.source,
|
|
122
|
+
tokenEstimate,
|
|
123
|
+
expiresAt: memory.metadata.expiresAt,
|
|
124
|
+
},
|
|
125
|
+
content: memory.content,
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const format = (options.format as OutputFormat) ?? 'yaml';
|
|
129
|
+
const serialized = serializeOutput({kind: 'memory', value: outputMemory}, format);
|
|
130
|
+
if (!serialized.ok()) {
|
|
131
|
+
throwCliError({ code: 'SERIALIZE_FAILED', message: serialized.error.message });
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const out = deps.stdout ?? ctx.stdout;
|
|
135
|
+
out.write(serialized.value + '\n');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* The `show` subcommand for displaying a memory.
|
|
140
|
+
*
|
|
141
|
+
* Reads a memory from the store and displays its content and metadata
|
|
142
|
+
* in the specified format. By default, expired memories are excluded.
|
|
143
|
+
*
|
|
144
|
+
* @example
|
|
145
|
+
* ```bash
|
|
146
|
+
* cortex memory show project/notes
|
|
147
|
+
* cortex memory show project/notes --format json
|
|
148
|
+
* cortex memory show project/notes --include-expired
|
|
149
|
+
* ```
|
|
150
|
+
*/
|
|
151
|
+
export const showCommand = new Command('show')
|
|
152
|
+
.description('Display a memory')
|
|
153
|
+
.argument('<path>', 'Memory path to show')
|
|
154
|
+
.option('-x, --include-expired', 'Include expired memories')
|
|
155
|
+
.option('-o, --format <format>', 'Output format (yaml, json, toon)', 'yaml')
|
|
156
|
+
.action(async (path, options, command) => {
|
|
157
|
+
const parentOpts = command.parent?.opts() as { store?: string } | undefined;
|
|
158
|
+
const context = await createCliCommandContext();
|
|
159
|
+
if (!context.ok()) {
|
|
160
|
+
throwCliError(context.error);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
await handleShow(context.value, parentOpts?.store, path, options);
|
|
164
|
+
});
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { PassThrough } from 'node:stream';
|
|
2
|
+
import {
|
|
3
|
+
Cortex,
|
|
4
|
+
Memory,
|
|
5
|
+
ok,
|
|
6
|
+
type AdapterFactory,
|
|
7
|
+
type ConfigAdapter,
|
|
8
|
+
type ConfigStores,
|
|
9
|
+
type CortexContext,
|
|
10
|
+
type CortexSettings,
|
|
11
|
+
type MemoryMetadata,
|
|
12
|
+
type StorageAdapter,
|
|
13
|
+
} from '@yeseh/cortex-core';
|
|
14
|
+
|
|
15
|
+
export const createMockMemoryCommandAdapter = (
|
|
16
|
+
overrides: Record<string, unknown> = {},
|
|
17
|
+
): StorageAdapter =>
|
|
18
|
+
({
|
|
19
|
+
memories: {
|
|
20
|
+
load: async () => ok(null),
|
|
21
|
+
save: async () => ok(undefined),
|
|
22
|
+
add: async () => ok(undefined),
|
|
23
|
+
remove: async () => ok(undefined),
|
|
24
|
+
move: async () => ok(undefined),
|
|
25
|
+
...(overrides.memories as object | undefined),
|
|
26
|
+
},
|
|
27
|
+
indexes: {
|
|
28
|
+
load: async () => ok(null),
|
|
29
|
+
write: async () => ok(undefined),
|
|
30
|
+
reindex: async () => ok({ warnings: [] }),
|
|
31
|
+
updateAfterMemoryWrite: async () => ok(undefined),
|
|
32
|
+
...(overrides.indexes as object | undefined),
|
|
33
|
+
},
|
|
34
|
+
categories: {
|
|
35
|
+
exists: async () => ok(true),
|
|
36
|
+
ensure: async () => ok(undefined),
|
|
37
|
+
delete: async () => ok(undefined),
|
|
38
|
+
setDescription: async () => ok(undefined),
|
|
39
|
+
...(overrides.categories as object | undefined),
|
|
40
|
+
},
|
|
41
|
+
}) as unknown as StorageAdapter;
|
|
42
|
+
|
|
43
|
+
export const createMemoryCommandContext = (options: {
|
|
44
|
+
adapter: StorageAdapter;
|
|
45
|
+
storePath: string;
|
|
46
|
+
stdout?: PassThrough;
|
|
47
|
+
stdin?: PassThrough;
|
|
48
|
+
stores?: ConfigStores;
|
|
49
|
+
settings?: CortexSettings;
|
|
50
|
+
now?: () => Date;
|
|
51
|
+
adapterFactory?: AdapterFactory;
|
|
52
|
+
}): CortexContext => {
|
|
53
|
+
const stores: ConfigStores = options.stores ?? {
|
|
54
|
+
global: {
|
|
55
|
+
kind: 'filesystem',
|
|
56
|
+
properties: { path: options.storePath },
|
|
57
|
+
categories: {},
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const cortex = Cortex.init({
|
|
62
|
+
settings: options.settings,
|
|
63
|
+
stores,
|
|
64
|
+
adapterFactory: options.adapterFactory ?? (() => options.adapter),
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
cortex,
|
|
69
|
+
config: {
|
|
70
|
+
path: '/tmp/test/config.yaml',
|
|
71
|
+
data: null,
|
|
72
|
+
get stores() {
|
|
73
|
+
return stores;
|
|
74
|
+
},
|
|
75
|
+
get settings() {
|
|
76
|
+
return options.settings ?? null;
|
|
77
|
+
},
|
|
78
|
+
initializeConfig: async () => ok(undefined),
|
|
79
|
+
getSettings: async () => ok(options.settings ?? {}),
|
|
80
|
+
getStores: async () => ok(stores),
|
|
81
|
+
getStore: async (name: string) => ok(stores[name] ?? null),
|
|
82
|
+
saveStore: async () => ok(undefined),
|
|
83
|
+
} as ConfigAdapter,
|
|
84
|
+
settings: (options.settings ?? {}) as CortexSettings,
|
|
85
|
+
stores,
|
|
86
|
+
now: options.now ?? (() => new Date('2025-01-01T00:00:00.000Z')),
|
|
87
|
+
stdin: (options.stdin ?? new PassThrough()) as unknown as NodeJS.ReadStream,
|
|
88
|
+
stdout: (options.stdout ?? new PassThrough()) as unknown as NodeJS.WriteStream,
|
|
89
|
+
};
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export const createCaptureStream = (): { stream: PassThrough; getOutput: () => string } => {
|
|
93
|
+
let output = '';
|
|
94
|
+
const stream = new PassThrough();
|
|
95
|
+
const originalWrite = stream.write.bind(stream);
|
|
96
|
+
stream.write = ((chunk: unknown, encoding?: unknown, cb?: unknown) => {
|
|
97
|
+
output += Buffer.from(chunk as Buffer).toString(
|
|
98
|
+
typeof encoding === 'string' ? (encoding as BufferEncoding) : undefined,
|
|
99
|
+
);
|
|
100
|
+
return originalWrite(chunk as Buffer, encoding as BufferEncoding, cb as () => void);
|
|
101
|
+
}) as typeof stream.write;
|
|
102
|
+
|
|
103
|
+
return { stream, getOutput: () => output };
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export const createMemoryFixture = (
|
|
107
|
+
path: string,
|
|
108
|
+
overrides: Partial<MemoryMetadata> = {},
|
|
109
|
+
content = 'Memory content',
|
|
110
|
+
): Memory => {
|
|
111
|
+
const timestamp = new Date('2025-01-01T00:00:00.000Z');
|
|
112
|
+
const metadata: MemoryMetadata = {
|
|
113
|
+
createdAt: timestamp,
|
|
114
|
+
updatedAt: timestamp,
|
|
115
|
+
tags: ['test'],
|
|
116
|
+
source: 'user',
|
|
117
|
+
citations: [],
|
|
118
|
+
...overrides,
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const result = Memory.init(path, metadata, content);
|
|
122
|
+
if (!result.ok()) {
|
|
123
|
+
throw new Error('Test setup failed to create memory.');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return result.value;
|
|
127
|
+
};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
|
|
2
|
+
import { CommanderError, InvalidArgumentError } from '@commander-js/extra-typings';
|
|
3
|
+
import { mkdtemp, rm } from 'node:fs/promises';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { Memory, MemoryPath, ok } from '@yeseh/cortex-core';
|
|
7
|
+
|
|
8
|
+
import { handleUpdate } from './update.ts';
|
|
9
|
+
import {
|
|
10
|
+
createCaptureStream,
|
|
11
|
+
createMemoryCommandContext,
|
|
12
|
+
createMemoryFixture,
|
|
13
|
+
createMockMemoryCommandAdapter,
|
|
14
|
+
} from './test-helpers.spec.ts';
|
|
15
|
+
|
|
16
|
+
describe('handleUpdate', () => {
|
|
17
|
+
let tempDir: string;
|
|
18
|
+
|
|
19
|
+
beforeEach(async () => {
|
|
20
|
+
tempDir = await mkdtemp(join(tmpdir(), 'cortex-cli-memory-update-'));
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
afterEach(async () => {
|
|
24
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should throw InvalidArgumentError for invalid memory paths', async () => {
|
|
28
|
+
const ctx = createMemoryCommandContext({
|
|
29
|
+
adapter: createMockMemoryCommandAdapter(),
|
|
30
|
+
storePath: tempDir,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
await expect(handleUpdate(ctx, undefined, 'invalid', { content: 'Next' })).rejects.toThrow(
|
|
34
|
+
InvalidArgumentError,
|
|
35
|
+
);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('should throw CommanderError when store is missing', async () => {
|
|
39
|
+
const ctx = createMemoryCommandContext({
|
|
40
|
+
adapter: createMockMemoryCommandAdapter(),
|
|
41
|
+
storePath: tempDir,
|
|
42
|
+
stores: undefined,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
await expect(
|
|
46
|
+
handleUpdate(ctx, 'missing-store', 'project/one', { content: 'Next' }),
|
|
47
|
+
).rejects.toThrow(CommanderError);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should update memory and report output', async () => {
|
|
51
|
+
const memory = createMemoryFixture('project/update', {}, 'Original content');
|
|
52
|
+
const writeCalls: Memory[] = [];
|
|
53
|
+
|
|
54
|
+
const adapter = createMockMemoryCommandAdapter({
|
|
55
|
+
memories: {
|
|
56
|
+
load: async () => ok(memory),
|
|
57
|
+
save: async (_: MemoryPath, next: Memory) => {
|
|
58
|
+
writeCalls.push(next);
|
|
59
|
+
return ok(undefined);
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
indexes: {
|
|
63
|
+
updateAfterMemoryWrite: async () => ok(undefined),
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const capture = createCaptureStream();
|
|
68
|
+
const ctx = createMemoryCommandContext({
|
|
69
|
+
adapter,
|
|
70
|
+
storePath: tempDir,
|
|
71
|
+
stdout: capture.stream,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
await handleUpdate(ctx, undefined, 'project/update', {
|
|
75
|
+
content: 'Updated content',
|
|
76
|
+
tags: ['new-tag'],
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
expect(writeCalls).toHaveLength(1);
|
|
80
|
+
if (writeCalls[0]) {
|
|
81
|
+
expect(writeCalls[0].content).toBe('Updated content');
|
|
82
|
+
expect(writeCalls[0].metadata.tags).toEqual(['new-tag']);
|
|
83
|
+
}
|
|
84
|
+
expect(capture.getOutput()).toContain('Updated memory project/update.');
|
|
85
|
+
});
|
|
86
|
+
});
|