@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.
Files changed (189) hide show
  1. package/README.md +144 -0
  2. package/dist/category/commands/create.d.ts +44 -0
  3. package/dist/category/commands/create.d.ts.map +1 -0
  4. package/dist/category/commands/create.spec.d.ts +7 -0
  5. package/dist/category/commands/create.spec.d.ts.map +1 -0
  6. package/dist/category/index.d.ts +19 -0
  7. package/dist/category/index.d.ts.map +1 -0
  8. package/dist/commands/init.d.ts +58 -0
  9. package/dist/commands/init.d.ts.map +1 -0
  10. package/dist/commands/init.spec.d.ts +2 -0
  11. package/dist/commands/init.spec.d.ts.map +1 -0
  12. package/dist/context.d.ts +18 -0
  13. package/dist/context.d.ts.map +1 -0
  14. package/dist/context.spec.d.ts +2 -0
  15. package/dist/context.spec.d.ts.map +1 -0
  16. package/dist/create-cli-command.d.ts +23 -0
  17. package/dist/create-cli-command.d.ts.map +1 -0
  18. package/dist/create-cli-command.spec.d.ts +10 -0
  19. package/dist/create-cli-command.spec.d.ts.map +1 -0
  20. package/dist/errors.d.ts +57 -0
  21. package/dist/errors.d.ts.map +1 -0
  22. package/dist/errors.spec.d.ts +2 -0
  23. package/dist/errors.spec.d.ts.map +1 -0
  24. package/dist/input.d.ts +42 -0
  25. package/dist/input.d.ts.map +1 -0
  26. package/dist/input.spec.d.ts +2 -0
  27. package/dist/input.spec.d.ts.map +1 -0
  28. package/dist/memory/commands/add.d.ts +62 -0
  29. package/dist/memory/commands/add.d.ts.map +1 -0
  30. package/dist/memory/commands/add.spec.d.ts +7 -0
  31. package/dist/memory/commands/add.spec.d.ts.map +1 -0
  32. package/dist/memory/commands/definitions.spec.d.ts +10 -0
  33. package/dist/memory/commands/definitions.spec.d.ts.map +1 -0
  34. package/dist/memory/commands/handlers.spec.d.ts +2 -0
  35. package/dist/memory/commands/handlers.spec.d.ts.map +1 -0
  36. package/dist/memory/commands/list.d.ts +119 -0
  37. package/dist/memory/commands/list.d.ts.map +1 -0
  38. package/dist/memory/commands/list.spec.d.ts +2 -0
  39. package/dist/memory/commands/list.spec.d.ts.map +1 -0
  40. package/dist/memory/commands/move.d.ts +42 -0
  41. package/dist/memory/commands/move.d.ts.map +1 -0
  42. package/dist/memory/commands/move.spec.d.ts +2 -0
  43. package/dist/memory/commands/move.spec.d.ts.map +1 -0
  44. package/dist/memory/commands/remove.d.ts +41 -0
  45. package/dist/memory/commands/remove.d.ts.map +1 -0
  46. package/dist/memory/commands/remove.spec.d.ts +2 -0
  47. package/dist/memory/commands/remove.spec.d.ts.map +1 -0
  48. package/dist/memory/commands/show.d.ts +81 -0
  49. package/dist/memory/commands/show.d.ts.map +1 -0
  50. package/dist/memory/commands/show.spec.d.ts +2 -0
  51. package/dist/memory/commands/show.spec.d.ts.map +1 -0
  52. package/dist/memory/commands/test-helpers.spec.d.ts +19 -0
  53. package/dist/memory/commands/test-helpers.spec.d.ts.map +1 -0
  54. package/dist/memory/commands/update.d.ts +73 -0
  55. package/dist/memory/commands/update.d.ts.map +1 -0
  56. package/dist/memory/commands/update.spec.d.ts +2 -0
  57. package/dist/memory/commands/update.spec.d.ts.map +1 -0
  58. package/dist/memory/index.d.ts +29 -0
  59. package/dist/memory/index.d.ts.map +1 -0
  60. package/dist/memory/index.spec.d.ts +10 -0
  61. package/dist/memory/index.spec.d.ts.map +1 -0
  62. package/dist/memory/parsing.d.ts +3 -0
  63. package/dist/memory/parsing.d.ts.map +1 -0
  64. package/dist/memory/parsing.spec.d.ts +7 -0
  65. package/dist/memory/parsing.spec.d.ts.map +1 -0
  66. package/dist/output.d.ts +87 -0
  67. package/dist/output.d.ts.map +1 -0
  68. package/dist/output.spec.d.ts +2 -0
  69. package/dist/output.spec.d.ts.map +1 -0
  70. package/dist/paths.d.ts +27 -0
  71. package/dist/paths.d.ts.map +1 -0
  72. package/dist/paths.spec.d.ts +7 -0
  73. package/dist/paths.spec.d.ts.map +1 -0
  74. package/dist/program.d.ts +41 -0
  75. package/dist/program.d.ts.map +1 -0
  76. package/dist/program.spec.d.ts +11 -0
  77. package/dist/program.spec.d.ts.map +1 -0
  78. package/dist/run.d.ts +7 -0
  79. package/dist/run.d.ts.map +1 -0
  80. package/dist/run.spec.d.ts +12 -0
  81. package/dist/run.spec.d.ts.map +1 -0
  82. package/dist/store/commands/add.d.ts +73 -0
  83. package/dist/store/commands/add.d.ts.map +1 -0
  84. package/dist/store/commands/add.spec.d.ts +17 -0
  85. package/dist/store/commands/add.spec.d.ts.map +1 -0
  86. package/dist/store/commands/init.d.ts +75 -0
  87. package/dist/store/commands/init.d.ts.map +1 -0
  88. package/dist/store/commands/init.spec.d.ts +7 -0
  89. package/dist/store/commands/init.spec.d.ts.map +1 -0
  90. package/dist/store/commands/list.d.ts +62 -0
  91. package/dist/store/commands/list.d.ts.map +1 -0
  92. package/dist/store/commands/list.spec.d.ts +7 -0
  93. package/dist/store/commands/list.spec.d.ts.map +1 -0
  94. package/dist/store/commands/prune.d.ts +92 -0
  95. package/dist/store/commands/prune.d.ts.map +1 -0
  96. package/dist/store/commands/prune.spec.d.ts +7 -0
  97. package/dist/store/commands/prune.spec.d.ts.map +1 -0
  98. package/dist/store/commands/reindexs.d.ts +54 -0
  99. package/dist/store/commands/reindexs.d.ts.map +1 -0
  100. package/dist/store/commands/reindexs.spec.d.ts +7 -0
  101. package/dist/store/commands/reindexs.spec.d.ts.map +1 -0
  102. package/dist/store/commands/remove.d.ts +63 -0
  103. package/dist/store/commands/remove.d.ts.map +1 -0
  104. package/dist/store/commands/remove.spec.d.ts +17 -0
  105. package/dist/store/commands/remove.spec.d.ts.map +1 -0
  106. package/dist/store/index.d.ts +32 -0
  107. package/dist/store/index.d.ts.map +1 -0
  108. package/dist/store/index.spec.d.ts +9 -0
  109. package/dist/store/index.spec.d.ts.map +1 -0
  110. package/dist/store/utils/resolve-store-name.d.ts +30 -0
  111. package/dist/store/utils/resolve-store-name.d.ts.map +1 -0
  112. package/dist/store/utils/resolve-store-name.spec.d.ts +2 -0
  113. package/dist/store/utils/resolve-store-name.spec.d.ts.map +1 -0
  114. package/dist/test-helpers.spec.d.ts +224 -0
  115. package/dist/test-helpers.spec.d.ts.map +1 -0
  116. package/dist/tests/cli.integration.spec.d.ts +11 -0
  117. package/dist/tests/cli.integration.spec.d.ts.map +1 -0
  118. package/dist/toon.d.ts +197 -0
  119. package/dist/toon.d.ts.map +1 -0
  120. package/dist/toon.spec.d.ts +9 -0
  121. package/dist/toon.spec.d.ts.map +1 -0
  122. package/dist/utils/git.d.ts +20 -0
  123. package/dist/utils/git.d.ts.map +1 -0
  124. package/dist/utils/git.spec.d.ts +7 -0
  125. package/dist/utils/git.spec.d.ts.map +1 -0
  126. package/package.json +45 -0
  127. package/src/category/commands/create.spec.ts +139 -0
  128. package/src/category/commands/create.ts +115 -0
  129. package/src/category/index.ts +24 -0
  130. package/src/commands/init.spec.ts +203 -0
  131. package/src/commands/init.ts +301 -0
  132. package/src/context.spec.ts +60 -0
  133. package/src/context.ts +175 -0
  134. package/src/errors.spec.ts +264 -0
  135. package/src/errors.ts +105 -0
  136. package/src/memory/commands/add.spec.ts +169 -0
  137. package/src/memory/commands/add.ts +157 -0
  138. package/src/memory/commands/definitions.spec.ts +80 -0
  139. package/src/memory/commands/list.spec.ts +123 -0
  140. package/src/memory/commands/list.ts +268 -0
  141. package/src/memory/commands/move.spec.ts +85 -0
  142. package/src/memory/commands/move.ts +115 -0
  143. package/src/memory/commands/remove.spec.ts +79 -0
  144. package/src/memory/commands/remove.ts +104 -0
  145. package/src/memory/commands/show.spec.ts +71 -0
  146. package/src/memory/commands/show.ts +164 -0
  147. package/src/memory/commands/test-helpers.spec.ts +127 -0
  148. package/src/memory/commands/update.spec.ts +86 -0
  149. package/src/memory/commands/update.ts +229 -0
  150. package/src/memory/index.spec.ts +59 -0
  151. package/src/memory/index.ts +44 -0
  152. package/src/memory/parsing.spec.ts +105 -0
  153. package/src/memory/parsing.ts +22 -0
  154. package/src/observability.spec.ts +139 -0
  155. package/src/observability.ts +63 -0
  156. package/src/output.spec.ts +835 -0
  157. package/src/output.ts +119 -0
  158. package/src/program.spec.ts +46 -0
  159. package/src/program.ts +75 -0
  160. package/src/run.spec.ts +31 -0
  161. package/src/run.ts +9 -0
  162. package/src/store/commands/add.spec.ts +131 -0
  163. package/src/store/commands/add.ts +231 -0
  164. package/src/store/commands/init.spec.ts +236 -0
  165. package/src/store/commands/init.ts +256 -0
  166. package/src/store/commands/list.spec.ts +175 -0
  167. package/src/store/commands/list.ts +102 -0
  168. package/src/store/commands/prune.spec.ts +120 -0
  169. package/src/store/commands/prune.ts +152 -0
  170. package/src/store/commands/reindexs.spec.ts +94 -0
  171. package/src/store/commands/reindexs.ts +96 -0
  172. package/src/store/commands/remove.spec.ts +97 -0
  173. package/src/store/commands/remove.ts +189 -0
  174. package/src/store/index.spec.ts +60 -0
  175. package/src/store/index.ts +49 -0
  176. package/src/store/utils/resolve-store-name.spec.ts +62 -0
  177. package/src/store/utils/resolve-store-name.ts +79 -0
  178. package/src/test-helpers.spec.ts +430 -0
  179. package/src/tests/cli.integration.spec.ts +1170 -0
  180. package/src/toon.spec.ts +183 -0
  181. package/src/toon.ts +462 -0
  182. package/src/utils/git.spec.ts +95 -0
  183. package/src/utils/git.ts +51 -0
  184. package/src/utils/input.spec.ts +326 -0
  185. package/src/utils/input.ts +145 -0
  186. package/src/utils/paths.spec.ts +235 -0
  187. package/src/utils/paths.ts +75 -0
  188. package/src/utils/prompts.spec.ts +23 -0
  189. 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
+ });