@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,80 @@
1
+ /**
2
+ * Unit tests for memory command definitions.
3
+ *
4
+ * Verifies that memory subcommands expose the expected names,
5
+ * arguments, and options.
6
+ *
7
+ * @module cli/memory/commands/definitions.spec
8
+ */
9
+
10
+ import { describe, expect, it } from 'bun:test';
11
+
12
+ import { listCommand } from './list.ts';
13
+ import { moveCommand } from './move.ts';
14
+ import { removeCommand } from './remove.ts';
15
+ import { showCommand } from './show.ts';
16
+ import { updateCommand } from './update.ts';
17
+
18
+ const getLongOptions = (command: { options: ReadonlyArray<{ long?: string }> }): string[] =>
19
+ command.options.map((option) => option.long ?? '').filter(Boolean);
20
+
21
+ describe('memory command definitions', () => {
22
+ describe('listCommand', () => {
23
+ it('should expose expected command name and usage', () => {
24
+ expect(listCommand.name()).toBe('list');
25
+ expect(listCommand.usage()).toContain('[category]');
26
+ });
27
+
28
+ it('should register expected options', () => {
29
+ const options = getLongOptions(listCommand);
30
+ expect(options).toContain('--store');
31
+ expect(options).toContain('--include-expired');
32
+ expect(options).toContain('--format');
33
+ });
34
+ });
35
+
36
+ describe('moveCommand', () => {
37
+ it('should expose expected command name and usage', () => {
38
+ expect(moveCommand.name()).toBe('move');
39
+ expect(moveCommand.usage()).toContain('<from>');
40
+ expect(moveCommand.usage()).toContain('<to>');
41
+ });
42
+ });
43
+
44
+ describe('removeCommand', () => {
45
+ it('should expose expected command name and usage', () => {
46
+ expect(removeCommand.name()).toBe('remove');
47
+ expect(removeCommand.usage()).toContain('<path>');
48
+ });
49
+ });
50
+
51
+ describe('showCommand', () => {
52
+ it('should expose expected command name and usage', () => {
53
+ expect(showCommand.name()).toBe('show');
54
+ expect(showCommand.usage()).toContain('<path>');
55
+ });
56
+
57
+ it('should register expected options', () => {
58
+ const options = getLongOptions(showCommand);
59
+ expect(options).toContain('--include-expired');
60
+ expect(options).toContain('--format');
61
+ });
62
+ });
63
+
64
+ describe('updateCommand', () => {
65
+ it('should expose expected command name and usage', () => {
66
+ expect(updateCommand.name()).toBe('update');
67
+ expect(updateCommand.usage()).toContain('<path>');
68
+ });
69
+
70
+ it('should register expected options', () => {
71
+ const options = getLongOptions(updateCommand);
72
+ expect(options).toContain('--content');
73
+ expect(options).toContain('--file');
74
+ expect(options).toContain('--tags');
75
+ expect(options).toContain('--expires-at');
76
+ expect(options).toContain('--no-expires-at');
77
+ expect(options).toContain('--citation');
78
+ });
79
+ });
80
+ });
@@ -0,0 +1,123 @@
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 {
7
+ CategoryPath,
8
+ MemoryPath,
9
+ ok,
10
+ type AdapterFactory,
11
+ type Category,
12
+ } from '@yeseh/cortex-core';
13
+
14
+ import { handleList } from './list.ts';
15
+ import {
16
+ createCaptureStream,
17
+ createMemoryCommandContext,
18
+ createMemoryFixture,
19
+ createMockMemoryCommandAdapter,
20
+ } from './test-helpers.spec.ts';
21
+
22
+ describe('handleList', () => {
23
+ let tempDir: string;
24
+
25
+ beforeEach(async () => {
26
+ tempDir = await mkdtemp(join(tmpdir(), 'cortex-cli-memory-list-'));
27
+ });
28
+
29
+ afterEach(async () => {
30
+ await rm(tempDir, { recursive: true, force: true });
31
+ });
32
+
33
+ it('should throw InvalidArgumentError for invalid category paths', async () => {
34
+ const ctx = createMemoryCommandContext({
35
+ adapter: createMockMemoryCommandAdapter(),
36
+ storePath: tempDir,
37
+ });
38
+
39
+ await expect(handleList(ctx, undefined, '/ /', { format: 'yaml' })).rejects.toThrow(
40
+ InvalidArgumentError,
41
+ );
42
+ });
43
+
44
+ it('should throw CommanderError when the adapter factory returns nothing', async () => {
45
+ const nullFactory = (() => undefined) as unknown as AdapterFactory;
46
+ const ctx = createMemoryCommandContext({
47
+ adapter: createMockMemoryCommandAdapter(),
48
+ storePath: tempDir,
49
+ adapterFactory: nullFactory,
50
+ stores: {
51
+ global: {
52
+ kind: 'filesystem',
53
+ properties: { path: tempDir },
54
+ categories: {},
55
+ },
56
+ },
57
+ });
58
+
59
+ await expect(handleList(ctx, 'missing-store', undefined, {})).rejects.toThrow(
60
+ CommanderError,
61
+ );
62
+ });
63
+
64
+ it('should write serialized list output', async () => {
65
+ const memoryPath = MemoryPath.fromString('project/one');
66
+ if (!memoryPath.ok()) {
67
+ throw new Error('Test setup failed to create memory path.');
68
+ }
69
+ const memory = createMemoryFixture('project/one');
70
+
71
+ const rootCategory: Category = {
72
+ memories: [],
73
+ subcategories: [
74
+ {
75
+ path: CategoryPath.fromString('project').unwrap(),
76
+ memoryCount: 1,
77
+ description: 'Project memories',
78
+ },
79
+ ],
80
+ };
81
+
82
+ const projectCategory: Category = {
83
+ memories: [
84
+ {
85
+ path: memoryPath.value,
86
+ tokenEstimate: 42,
87
+ },
88
+ ],
89
+ subcategories: [],
90
+ };
91
+
92
+ const adapter = createMockMemoryCommandAdapter({
93
+ indexes: {
94
+ load: async (path: CategoryPath) => {
95
+ if (path.isRoot) {
96
+ return ok(rootCategory);
97
+ }
98
+ if (path.toString() === 'project') {
99
+ return ok(projectCategory);
100
+ }
101
+ return ok(null);
102
+ },
103
+ },
104
+ memories: {
105
+ load: async () => ok(memory),
106
+ },
107
+ });
108
+
109
+ const capture = createCaptureStream();
110
+ const ctx = createMemoryCommandContext({
111
+ adapter,
112
+ storePath: tempDir,
113
+ stdout: capture.stream,
114
+ });
115
+
116
+ await handleList(ctx, undefined, undefined, { format: 'json' });
117
+
118
+ const output = JSON.parse(capture.getOutput());
119
+ expect(output.memories).toHaveLength(1);
120
+ expect(output.memories[0].path).toBe('project/one');
121
+ expect(output.subcategories[0].path).toBe('project');
122
+ });
123
+ });
@@ -0,0 +1,268 @@
1
+ /**
2
+ * Memory list command for browsing memories with optional filtering.
3
+ *
4
+ * Lists all memories in a category, or all memories across all categories
5
+ * if no category is specified. Expired memories are excluded by default
6
+ * unless the `--include-expired` flag is provided.
7
+ *
8
+ * When no category is specified, the command dynamically discovers all
9
+ * root categories from the store's index rather than using a hardcoded list.
10
+ *
11
+ * @example
12
+ * ```bash
13
+ * # List all memories
14
+ * cortex memory list
15
+ *
16
+ * # List memories in a specific category
17
+ * cortex memory list project/cortex
18
+ *
19
+ * # Include expired memories
20
+ * cortex memory list --include-expired
21
+ *
22
+ * # Output in JSON format
23
+ * cortex memory list --format json
24
+ *
25
+ * # Use a specific store (either placement works)
26
+ * cortex memory list -s my-store
27
+ * cortex memory -s my-store list
28
+ * ```
29
+ */
30
+
31
+ import { Command } from '@commander-js/extra-typings';
32
+ import { throwCliError } from '../../errors.ts';
33
+
34
+ import { CategoryPath, type CategoryClient } from '@yeseh/cortex-core/category';
35
+ import { serialize, type CortexContext } from '@yeseh/cortex-core';
36
+ import type { SubcategoryEntry } from '@yeseh/cortex-core/category';
37
+ import { type OutputFormat } from '../../output.ts';
38
+ import { createCliCommandContext } from '../../context.ts';
39
+
40
+ /**
41
+ * Options for the list command.
42
+ */
43
+ export interface ListCommandOptions {
44
+ /** Include expired memories in the output */
45
+ includeExpired?: boolean;
46
+ /** Output format (yaml, json, toon) */
47
+ format?: string;
48
+ /** Store name (can be specified on subcommand or parent) */
49
+ store?: string;
50
+ }
51
+
52
+ /**
53
+ * Dependencies for the list command handler.
54
+ * Allows injection for testing.
55
+ */
56
+ export interface ListHandlerDeps {
57
+ /** Output stream for writing results (defaults to process.stdout) */
58
+ stdout?: NodeJS.WritableStream;
59
+ /** Current time for expiration checks */
60
+ now?: Date;
61
+ }
62
+
63
+ /**
64
+ * Entry representing a memory in the list output.
65
+ */
66
+ export interface ListMemoryEntry {
67
+ path: string;
68
+ tokenEstimate: number;
69
+ summary?: string;
70
+ expiresAt?: Date;
71
+ isExpired: boolean;
72
+ }
73
+
74
+ /**
75
+ * Entry representing a subcategory in the list output.
76
+ */
77
+ export interface ListSubcategoryEntry {
78
+ path: string;
79
+ memoryCount: number;
80
+ description?: string;
81
+ }
82
+
83
+ /**
84
+ * Result of the list command containing memories and subcategories.
85
+ */
86
+ export interface ListResult {
87
+ memories: ListMemoryEntry[];
88
+ subcategories: ListSubcategoryEntry[];
89
+ }
90
+
91
+ /**
92
+ * Handles the list command execution.
93
+ *
94
+ * This function:
95
+ * 1. Resolves the store context
96
+ * 2. Loads category index (or all categories if none specified)
97
+ * 3. Collects memories and subcategories, filtering expired if needed
98
+ * 4. Formats and outputs the result
99
+ *
100
+ * @param category - Optional category path to list (lists all if omitted)
101
+ * @param options - Command options (includeExpired, format)
102
+ * @param storeName - Optional store name from parent command
103
+ * @param deps - Optional dependencies for testing
104
+ * @throws {InvalidArgumentError} When arguments are invalid
105
+ * @throws {CommanderError} When read or parse fails
106
+ */
107
+ export async function handleList(
108
+ ctx: CortexContext,
109
+ storeName: string | undefined,
110
+ category: string | undefined,
111
+ options: ListCommandOptions,
112
+ deps: ListHandlerDeps = {}
113
+ ): Promise<void> {
114
+ const categoryResult = CategoryPath.fromString(category ?? '');
115
+ if (!categoryResult.ok()) {
116
+ throwCliError(categoryResult.error);
117
+ }
118
+
119
+ const storeResult = ctx.cortex.getStore(storeName ?? 'global');
120
+ if (!storeResult.ok()) {
121
+ throwCliError(storeResult.error);
122
+ }
123
+
124
+ const rootResult = storeResult.value.root();
125
+ if (!rootResult.ok()) {
126
+ throwCliError(rootResult.error);
127
+ }
128
+
129
+ const root = rootResult.value;
130
+ let categoryClient: CategoryClient;
131
+
132
+ if (categoryResult.value.isRoot) {
133
+ categoryClient = root;
134
+ } else {
135
+ const categoryClientResult = root.getCategory(categoryResult.value.toString());
136
+ if (!categoryClientResult.ok()) {
137
+ throwCliError(categoryClientResult.error);
138
+ }
139
+
140
+ categoryClient = categoryClientResult.value;
141
+ }
142
+ const now = deps.now ?? ctx.now();
143
+ const includeExpired = options.includeExpired ?? false;
144
+ const visited = new Set<string>();
145
+
146
+ const collectMemories = async (client: CategoryClient): Promise<ListMemoryEntry[]> => {
147
+ const categoryKey = client.rawPath;
148
+ if (visited.has(categoryKey)) {
149
+ return [];
150
+ }
151
+ visited.add(categoryKey);
152
+
153
+ const memoriesResult = await client.listMemories({ includeExpired });
154
+ if (!memoriesResult.ok()) {
155
+ throwCliError(memoriesResult.error);
156
+ }
157
+
158
+ const memories: ListMemoryEntry[] = [];
159
+ for (const entry of memoriesResult.value) {
160
+ const memoryClient = client.getMemory(entry.path.slug.toString());
161
+ const memoryResult = await memoryClient.get({ includeExpired: true, now });
162
+ if (!memoryResult.ok()) {
163
+ if (memoryResult.error.code === 'MEMORY_NOT_FOUND') {
164
+ continue;
165
+ }
166
+ if (!includeExpired && memoryResult.error.code === 'MEMORY_EXPIRED') {
167
+ continue;
168
+ }
169
+ throwCliError(memoryResult.error);
170
+ }
171
+
172
+ const memory = memoryResult.value;
173
+ const isExpired = memory.isExpired(now);
174
+ if (!includeExpired && isExpired) {
175
+ continue;
176
+ }
177
+
178
+ memories.push({
179
+ path: entry.path.toString(),
180
+ tokenEstimate: entry.tokenEstimate,
181
+ summary: undefined,
182
+ expiresAt: memory.metadata.expiresAt,
183
+ isExpired,
184
+ });
185
+ }
186
+
187
+ const subcategoriesResult = await client.listSubcategories();
188
+ if (!subcategoriesResult.ok()) {
189
+ throwCliError(subcategoriesResult.error);
190
+ }
191
+
192
+ for (const subcategory of subcategoriesResult.value) {
193
+ const subcategoryClientResult = root.getCategory(subcategory.path.toString());
194
+ if (!subcategoryClientResult.ok()) {
195
+ throwCliError(subcategoryClientResult.error);
196
+ }
197
+ const subMemories = await collectMemories(subcategoryClientResult.value);
198
+ memories.push(...subMemories);
199
+ }
200
+
201
+ return memories;
202
+ };
203
+
204
+ const subcategoriesResult = await categoryClient.listSubcategories();
205
+ if (!subcategoriesResult.ok()) {
206
+ throwCliError(subcategoriesResult.error);
207
+ }
208
+
209
+ const memories = await collectMemories(categoryClient);
210
+ const result: ListResult = {
211
+ memories,
212
+ subcategories: subcategoriesResult.value.map((subcategory: SubcategoryEntry) => ({
213
+ path: subcategory.path.toString(),
214
+ memoryCount: subcategory.memoryCount,
215
+ description: subcategory.description,
216
+ })),
217
+ };
218
+
219
+ // 3. Format and output
220
+ const validFormats = ['yaml', 'json', 'toon'] as const;
221
+ const format: OutputFormat = validFormats.includes(options.format as OutputFormat)
222
+ ? (options.format as OutputFormat)
223
+ : 'yaml';
224
+ const output = serialize(result, format);
225
+ if (!output.ok()) {
226
+ throwCliError({ code: 'SERIALIZE_FAILED', message: output.error.message });
227
+ }
228
+
229
+ const out = deps.stdout ?? ctx.stdout;
230
+ out.write(output.value + '\n');
231
+ }
232
+
233
+ /**
234
+ * The `list` subcommand for browsing memories.
235
+ *
236
+ * Lists memories in a category, or all memories across all root categories
237
+ * if no category is specified. By default, expired memories are excluded.
238
+ *
239
+ * The `--store` option can be specified either on this command or on the
240
+ * parent `memory` command for flexibility.
241
+ *
242
+ * @example
243
+ * ```bash
244
+ * cortex memory list
245
+ * cortex memory list project/cortex
246
+ * cortex memory list --include-expired
247
+ * cortex memory list --format json
248
+ * cortex memory list -s my-store
249
+ * cortex memory -s my-store list
250
+ * ```
251
+ */
252
+ export const listCommand = new Command('list')
253
+ .description('List memories in a category')
254
+ .argument('[category]', 'Category path to list (lists all if omitted)')
255
+ .option('-s, --store <name>', 'Use a specific named store')
256
+ .option('-x, --include-expired', 'Include expired memories')
257
+ .option('-o, --format <format>', 'Output format (yaml, json, toon)', 'yaml')
258
+ .action(async (category, options, command) => {
259
+ const parentOpts = command.parent?.opts() as { store?: string } | undefined;
260
+ // Allow store to be specified on either the subcommand or parent command
261
+ const storeName = options.store ?? parentOpts?.store;
262
+ const context = await createCliCommandContext();
263
+ if (!context.ok()) {
264
+ throwCliError(context.error);
265
+ }
266
+
267
+ await handleList(context.value, storeName, category, options);
268
+ });
@@ -0,0 +1,85 @@
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 { handleMove } from './move.ts';
9
+ import {
10
+ createCaptureStream,
11
+ createMemoryCommandContext,
12
+ createMemoryFixture,
13
+ createMockMemoryCommandAdapter,
14
+ } from './test-helpers.spec.ts';
15
+
16
+ describe('handleMove', () => {
17
+ let tempDir: string;
18
+
19
+ beforeEach(async () => {
20
+ tempDir = await mkdtemp(join(tmpdir(), 'cortex-cli-memory-move-'));
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(handleMove(ctx, undefined, 'invalid', 'project/two')).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(handleMove(ctx, 'missing-store', 'project/one', 'project/two')).rejects.toThrow(
46
+ CommanderError,
47
+ );
48
+ });
49
+
50
+ it('should move memory and report output', async () => {
51
+ const memory = createMemoryFixture('project/from');
52
+ const moveCalls: { from: MemoryPath; to: MemoryPath }[] = [];
53
+
54
+ const adapter = createMockMemoryCommandAdapter({
55
+ memories: {
56
+ load: async (path: MemoryPath) => {
57
+ if (path.toString() === 'project/from') {
58
+ return ok(memory);
59
+ }
60
+ return ok(null);
61
+ },
62
+ move: async (from: MemoryPath, to: MemoryPath) => {
63
+ moveCalls.push({ from, to });
64
+ return ok(undefined);
65
+ },
66
+ },
67
+ });
68
+
69
+ const capture = createCaptureStream();
70
+ const ctx = createMemoryCommandContext({
71
+ adapter,
72
+ storePath: tempDir,
73
+ stdout: capture.stream,
74
+ });
75
+
76
+ await handleMove(ctx, undefined, 'project/from', 'project/to');
77
+
78
+ expect(moveCalls).toHaveLength(1);
79
+ if (moveCalls[0]) {
80
+ expect(moveCalls[0].from.toString()).toBe('project/from');
81
+ expect(moveCalls[0].to.toString()).toBe('project/to');
82
+ }
83
+ expect(capture.getOutput()).toContain('Moved memory project/from to project/to');
84
+ });
85
+ });
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Memory move command implementation using Commander.js.
3
+ *
4
+ * Moves a memory from one path to another within the same store.
5
+ *
6
+ * @example
7
+ * ```bash
8
+ * # Move a memory to a new location
9
+ * cortex memory move project/old-name project/new-name
10
+ *
11
+ * # Move with explicit store
12
+ * cortex memory --store my-store move project/old project/new
13
+ * ```\n */
14
+
15
+ import { Command } from '@commander-js/extra-typings';
16
+ import { throwCliError } from '../../errors.ts';
17
+ import { MemoryPath, type CortexContext } from '@yeseh/cortex-core';
18
+ import { createCliCommandContext } from '../../context.ts';
19
+ import { serializeOutput, type OutputFormat } from '../../output.ts';
20
+
21
+ /** Options for the move command. */
22
+ export interface MoveCommandOptions {
23
+ /** Output format (yaml, json, toon) */
24
+ format?: string;
25
+ }
26
+
27
+ /**
28
+ * Handler for the memory move command.
29
+ * Exported for direct testing without Commander parsing.
30
+ *
31
+ * @param ctx - CLI context containing Cortex client and streams
32
+ * @param storeName - Optional store name from parent command
33
+ * @param from - Source memory path
34
+ * @param to - Destination memory path
35
+ */
36
+ export async function handleMove(
37
+ ctx: CortexContext,
38
+ storeName: string | undefined,
39
+ from: string,
40
+ to: string,
41
+ options: MoveCommandOptions = {}
42
+ ): Promise<void> {
43
+ const fromResult = MemoryPath.fromString(from);
44
+ if (!fromResult.ok()) {
45
+ throwCliError(fromResult.error);
46
+ }
47
+
48
+ const toResult = MemoryPath.fromString(to);
49
+ if (!toResult.ok()) {
50
+ throwCliError(toResult.error);
51
+ }
52
+
53
+ const storeResult = ctx.cortex.getStore(storeName ?? 'global');
54
+ if (!storeResult.ok()) {
55
+ throwCliError(storeResult.error);
56
+ }
57
+
58
+ const store = storeResult.value;
59
+ const rootResult = store.root();
60
+ if (!rootResult.ok()) {
61
+ throwCliError(rootResult.error);
62
+ }
63
+
64
+ const sourceCategoryResult = fromResult.value.category.isRoot
65
+ ? rootResult
66
+ : rootResult.value.getCategory(fromResult.value.category.toString());
67
+ if (!sourceCategoryResult.ok()) {
68
+ throwCliError(sourceCategoryResult.error);
69
+ }
70
+
71
+ const sourceMemory = sourceCategoryResult.value.getMemory(fromResult.value.slug.toString());
72
+ const moveResult = await sourceMemory.move(toResult.value);
73
+ if (!moveResult.ok()) {
74
+ throwCliError(moveResult.error);
75
+ }
76
+
77
+ const out = ctx.stdout ?? process.stdout;
78
+ const rawFormat = options.format;
79
+ if (!rawFormat) {
80
+ out.write(`Moved memory ${fromResult.value.toString()} to ${toResult.value.toString()}.\n`);
81
+ } else {
82
+ const format = rawFormat as OutputFormat;
83
+ const serialized = serializeOutput(
84
+ { kind: 'moved-memory', value: { from: fromResult.value.toString(), to: toResult.value.toString() } },
85
+ format
86
+ );
87
+ if (!serialized.ok()) {
88
+ throwCliError({ code: 'SERIALIZE_FAILED', message: serialized.error.message });
89
+ }
90
+ out.write(serialized.value + '\n');
91
+ }
92
+ }
93
+
94
+ /**
95
+ * The `memory move` subcommand.
96
+ *
97
+ * Moves a memory from one path to another within the store.
98
+ * Both paths must be valid memory slug paths.
99
+ *
100
+ * The `--store` option is inherited from the parent `memory` command.
101
+ */
102
+ export const moveCommand = new Command('move')
103
+ .description('Move a memory to a new path')
104
+ .argument('<from>', 'Source memory path')
105
+ .argument('<to>', 'Destination memory path')
106
+ .option('-o, --format <format>', 'Output format (yaml, json, toon)')
107
+ .action(async (from, to, options, command) => {
108
+ const parentOpts = command.parent?.opts() as { store?: string } | undefined;
109
+ const context = await createCliCommandContext();
110
+ if (!context.ok()) {
111
+ throwCliError(context.error);
112
+ }
113
+
114
+ await handleMove(context.value, parentOpts?.store, from, to, options);
115
+ });