om-memory-system 5.5.0-next.212 → 5.6.0-next.216

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 (104) hide show
  1. package/README.md +18 -13
  2. package/dist/adapters/pi/extension.js +8 -0
  3. package/dist/cli/index.js +5 -0
  4. package/dist/cli/memory-command.js +22 -1
  5. package/dist/cli/space-command.d.ts +15 -0
  6. package/dist/cli/space-command.js +189 -0
  7. package/dist/cli/space-transfer-command.d.ts +25 -0
  8. package/dist/cli/space-transfer-command.js +79 -0
  9. package/dist/config.d.ts +3 -0
  10. package/dist/config.js +16 -19
  11. package/dist/core/memory-operations.d.ts +15 -1
  12. package/dist/core/memory-operations.js +21 -2
  13. package/dist/core/recall-summary.js +7 -5
  14. package/dist/core/retrieval.js +5 -2
  15. package/dist/core/space-tool.d.ts +11 -0
  16. package/dist/core/space-tool.js +46 -0
  17. package/dist/services/active-space-access.d.ts +6 -0
  18. package/dist/services/active-space-access.js +8 -0
  19. package/dist/services/ai/session/ai-session-manager.js +2 -2
  20. package/dist/services/chat/chat-runtime.js +1 -1
  21. package/dist/services/client.d.ts +6 -16
  22. package/dist/services/client.js +54 -62
  23. package/dist/services/context.d.ts +2 -0
  24. package/dist/services/context.js +21 -4
  25. package/dist/services/embedding.js +2 -1
  26. package/dist/services/host-appearance.d.ts +18 -7
  27. package/dist/services/host-appearance.js +44 -34
  28. package/dist/services/legacy-migration.d.ts +3 -1
  29. package/dist/services/legacy-migration.js +1 -1
  30. package/dist/services/linked-search.d.ts +108 -0
  31. package/dist/services/linked-search.js +219 -0
  32. package/dist/services/memory-host-routes.d.ts +1 -0
  33. package/dist/services/memory-host-routes.js +4 -2
  34. package/dist/services/space-routes.d.ts +18 -0
  35. package/dist/services/space-routes.js +223 -0
  36. package/dist/services/space-transfer-routes.d.ts +10 -0
  37. package/dist/services/space-transfer-routes.js +344 -0
  38. package/dist/services/spaces/active-space.d.ts +16 -0
  39. package/dist/services/spaces/active-space.js +39 -0
  40. package/dist/services/spaces/backup.d.ts +52 -0
  41. package/dist/services/spaces/backup.js +125 -0
  42. package/dist/services/spaces/context-line.d.ts +10 -0
  43. package/dist/services/spaces/context-line.js +13 -0
  44. package/dist/services/spaces/delete.d.ts +24 -0
  45. package/dist/services/spaces/delete.js +44 -0
  46. package/dist/services/spaces/export-space.d.ts +60 -0
  47. package/dist/services/spaces/export-space.js +367 -0
  48. package/dist/services/spaces/folder-size.d.ts +2 -0
  49. package/dist/services/spaces/folder-size.js +9 -0
  50. package/dist/services/spaces/identifier-scrub.d.ts +26 -0
  51. package/dist/services/spaces/identifier-scrub.js +40 -0
  52. package/dist/services/spaces/import-space.d.ts +85 -0
  53. package/dist/services/spaces/import-space.js +349 -0
  54. package/dist/services/spaces/operations.d.ts +78 -0
  55. package/dist/services/spaces/operations.js +238 -0
  56. package/dist/services/spaces/read-store-cache.d.ts +13 -0
  57. package/dist/services/spaces/read-store-cache.js +34 -0
  58. package/dist/services/spaces/registry-store.d.ts +36 -0
  59. package/dist/services/spaces/registry-store.js +148 -0
  60. package/dist/services/spaces/registry.d.ts +102 -0
  61. package/dist/services/spaces/registry.js +301 -0
  62. package/dist/services/spaces/restore.d.ts +20 -0
  63. package/dist/services/spaces/restore.js +80 -0
  64. package/dist/services/spaces/space-store.d.ts +50 -0
  65. package/dist/services/spaces/space-store.js +72 -0
  66. package/dist/services/spaces/storage-resolution.d.ts +17 -0
  67. package/dist/services/spaces/storage-resolution.js +29 -0
  68. package/dist/services/spaces/store-handles.d.ts +23 -0
  69. package/dist/services/spaces/store-handles.js +40 -0
  70. package/dist/services/spaces/transfer-service.d.ts +41 -0
  71. package/dist/services/spaces/transfer-service.js +408 -0
  72. package/dist/services/spaces/transfer-types.d.ts +163 -0
  73. package/dist/services/spaces/transfer-types.js +10 -0
  74. package/dist/services/spaces/transfer.d.ts +31 -0
  75. package/dist/services/spaces/transfer.js +276 -0
  76. package/dist/services/tag-prefix-migration.js +3 -3
  77. package/dist/services/turso/connection-manager.d.ts +6 -2
  78. package/dist/services/turso/connection-manager.js +14 -6
  79. package/dist/services/turso/cross-process-write-lock.d.ts +1 -1
  80. package/dist/services/turso/cross-process-write-lock.js +5 -5
  81. package/dist/services/turso/operation-lock.d.ts +2 -2
  82. package/dist/services/turso/operation-lock.js +5 -5
  83. package/dist/services/turso/ready.js +2 -0
  84. package/dist/services/turso/shard-manager.d.ts +8 -0
  85. package/dist/services/turso/shard-manager.js +25 -24
  86. package/dist/services/turso/vector-search.d.ts +6 -2
  87. package/dist/services/turso/vector-search.js +8 -4
  88. package/dist/services/user-profile/user-profile-manager.d.ts +5 -1
  89. package/dist/services/user-profile/user-profile-manager.js +20 -17
  90. package/dist/services/user-prompt/user-prompt-manager.js +2 -2
  91. package/dist/services/web-route-modules.d.ts +6 -1
  92. package/dist/services/web-route-modules.js +9 -0
  93. package/dist/services/web-server.js +3 -2
  94. package/dist/shared/api/space-export-schemas.d.ts +159 -0
  95. package/dist/shared/api/space-export-schemas.js +72 -0
  96. package/dist/v2/adapter.d.ts +71 -0
  97. package/dist/v2/adapter.js +9 -1
  98. package/dist/web/assets/index-2BNRLGqe.css +2 -0
  99. package/dist/web/assets/index-C6WnDE6h.js +170 -0
  100. package/dist/web/index.html +2 -2
  101. package/package.json +1 -1
  102. package/skills/omms-memory/SKILL.md +16 -12
  103. package/dist/web/assets/index-7p-aYnWZ.css +0 -2
  104. package/dist/web/assets/index-CaFFlcl1.js +0 -170
package/README.md CHANGED
@@ -117,6 +117,10 @@ cannot be saved.
117
117
 
118
118
  ![Memory page showing the Pi host view with synthetic import status](.github/screenshots/memory.png)
119
119
 
120
+ ### Memory spaces
121
+
122
+ Keep separate stores for separate lines of work. The Spaces item in the web app sidebar opens a page with one card for each space. Switching a space applies to every host. Memories can be copied or moved between spaces, exported to a shareable file and imported into a new space, and a space can read the spaces it links. See [Memory spaces](docs/memory-spaces.md).
123
+
120
124
  ### Settings
121
125
 
122
126
  Choose models and manage access. Diagnostics show capture outcomes and component health.
@@ -125,19 +129,20 @@ Choose models and manage access. Diagnostics show capture outcomes and component
125
129
 
126
130
  ## Documentation
127
131
 
128
- | Guide | Covers |
129
- | ------------------------------------------- | ----------------------------------------------------------- |
130
- | [Using memory](docs/using-memory.md) | Daily use, capture, recall and the user profile |
131
- | [Web UI](docs/web-ui.md) | Starting the app, browsing memories and safe network access |
132
- | [Memory page](docs/web-ui-memory.md) | One card per host, imports, shared settings and avatars |
133
- | [Settings page](docs/web-ui-settings.md) | Models, credentials, diagnostics and profile identities |
134
- | [Configuration](docs/configuration.md) | Config files, model selection, privacy and embeddings |
135
- | [Moving projects](docs/moving-projects.md) | Folder moves, backup and restore |
136
- | [Updating and upgrading](docs/upgrading.md) | Updates, version pinning and older stores |
137
- | [CLI reference](docs/cli.md) | Terminal commands |
138
- | [Skill learning](docs/skill-learning.md) | Opt-in turn recording, skill ranking and draft review |
139
- | [For developers](docs/developers.md) | Building, testing and architecture |
140
- | [Contributing](CONTRIBUTING.md) | Development workflow and pull requests |
132
+ | Guide | Covers |
133
+ | ------------------------------------------- | ----------------------------------------------------------------- |
134
+ | [Using memory](docs/using-memory.md) | Daily use, capture, recall and the user profile |
135
+ | [Web UI](docs/web-ui.md) | Starting the app, browsing memories and safe network access |
136
+ | [Memory page](docs/web-ui-memory.md) | One card per host, imports, shared settings and avatars |
137
+ | [Settings page](docs/web-ui-settings.md) | Models, credentials, diagnostics and profile identities |
138
+ | [Configuration](docs/configuration.md) | Config files, model selection, privacy and embeddings |
139
+ | [Moving projects](docs/moving-projects.md) | Folder moves, backup and restore |
140
+ | [Updating and upgrading](docs/upgrading.md) | Updates, version pinning and older stores |
141
+ | [CLI reference](docs/cli.md) | Terminal commands |
142
+ | [Memory spaces](docs/memory-spaces.md) | Separate stores, the Spaces page, backups and the `space` command |
143
+ | [Skill learning](docs/skill-learning.md) | Opt-in turn recording, skill ranking and draft review |
144
+ | [For developers](docs/developers.md) | Building, testing and architecture |
145
+ | [Contributing](CONTRIBUTING.md) | Development workflow and pull requests |
141
146
 
142
147
  History import guides: [OpenCode](docs/opencode-history-import.md),
143
148
  [Pi](docs/pi-history-import.md) and [Claude Code](docs/claude-code-history-import.md).
@@ -30,6 +30,7 @@ const MEMORY_TOOL_MODES = [
30
30
  "list-shards",
31
31
  "export",
32
32
  "import",
33
+ "space",
33
34
  ];
34
35
  function toSessionEntries(branch) {
35
36
  return Array.isArray(branch) ? branch : [];
@@ -347,6 +348,13 @@ export default function ommsPiExtension(pi) {
347
348
  inputPath: Type.Optional(Type.String()),
348
349
  dryRun: Type.Optional(Type.Boolean()),
349
350
  allowLinkedSource: Type.Optional(Type.Boolean()),
351
+ spaceAction: Type.Optional(Type.String()),
352
+ spaceName: Type.Optional(Type.String()),
353
+ spaceTo: Type.Optional(Type.String()),
354
+ spaceSince: Type.Optional(Type.String()),
355
+ spaceUntil: Type.Optional(Type.String()),
356
+ spaceProject: Type.Optional(Type.String()),
357
+ spaceIn: Type.Optional(Type.String()),
350
358
  }),
351
359
  async execute(_toolCallId, args, _signal, _onUpdate, ctx) {
352
360
  const result = await executeMemoryOperation(args, {
package/dist/cli/index.js CHANGED
@@ -17,6 +17,7 @@ Commands:
17
17
  web install|uninstall|status Manage the web app login item
18
18
  web update Update the global install and restart the web app
19
19
  memory <mode> [options] Search, add, list, or forget memories (run with --help)
20
+ space <action> [name] List, create, switch, or archive memory spaces (run with --help)
20
21
  skills <mode> [options] List skills, review proposals, record or delete turns (run with --help)
21
22
  profile-catch-up Analyse every prompt waiting for profile learning (run with --help)
22
23
  profile-normalise-categories Preview or confirm profile category renames (run with --help)
@@ -66,6 +67,10 @@ export async function runCli(argv) {
66
67
  const { runMemoryCommand } = await import("./memory-command.js");
67
68
  return await runMemoryCommand(argv.slice(1));
68
69
  }
70
+ if (argv[0] === "space") {
71
+ const { runSpaceCommand } = await import("./space-command.js");
72
+ return await runSpaceCommand(argv.slice(1));
73
+ }
69
74
  if (argv[0] === "skills") {
70
75
  const { runSkillsCommand } = await import("./skills-command.js");
71
76
  return await runSkillsCommand(argv.slice(1));
@@ -10,6 +10,7 @@ const MODES = {
10
10
  migrate: "Reassociate orphaned shards after a directory move (--from-path or --from-hash)",
11
11
  export: "Export project memories to a portable JSON file (--output)",
12
12
  import: "Import memories from a portable JSON file (--input)",
13
+ space: "Manage memory spaces: list, status, create, archive, unarchive, use (--action, --name)",
13
14
  };
14
15
  const VALUE_FLAGS = {
15
16
  "--content": "content",
@@ -23,6 +24,8 @@ const VALUE_FLAGS = {
23
24
  "--from-hash": "fromHash",
24
25
  "--output": "outputPath",
25
26
  "--input": "inputPath",
27
+ "--action": "spaceAction",
28
+ "--name": "spaceName",
26
29
  "--directory": "directory",
27
30
  };
28
31
  const BOOLEAN_FLAGS = {
@@ -51,6 +54,8 @@ Options:
51
54
  --from-hash <hash> Old project hash (migrate)
52
55
  --output <file> Export file (export)
53
56
  --input <file> Import file (import)
57
+ --action <action> Space action: list, status, create, archive, unarchive, use (space)
58
+ --name <name> Space name or id (space)
54
59
  --dry-run Report without writing (migrate, import)
55
60
  --allow-linked-source Allow a linked source directory (migrate)
56
61
  --directory <path> Project directory (default: the working directory)
@@ -107,7 +112,23 @@ export function parseMemoryCommandArgs(argv) {
107
112
  throw new Error(`${flag} needs a value`);
108
113
  values[key] = value;
109
114
  }
110
- if (positionals.length > 0) {
115
+ if (mode === "space") {
116
+ // `space <action> [name]`
117
+ const [action, name, ...extra] = positionals;
118
+ if (extra.length > 0)
119
+ throw new Error(`Unexpected argument "${extra[0]}"`);
120
+ if (action !== undefined) {
121
+ if (values.spaceAction !== undefined)
122
+ throw new Error(`Unexpected argument "${action}"`);
123
+ values.spaceAction = action;
124
+ }
125
+ if (name !== undefined) {
126
+ if (values.spaceName !== undefined)
127
+ throw new Error(`Unexpected argument "${name}"`);
128
+ values.spaceName = name;
129
+ }
130
+ }
131
+ else if (positionals.length > 0) {
111
132
  if (mode !== "search" || positionals.length > 1 || values.query !== undefined) {
112
133
  throw new Error(`Unexpected argument "${positionals[0]}"`);
113
134
  }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * `om-memory-system space <action> [name] [options]`: list, create, switch, archive,
3
+ * unarchive, preview, copy, link, unlink, export and import memory spaces from a
4
+ * terminal. Deleting, renaming, backing up, restoring and moving exist only in the
5
+ * web app, so those actions reach the shared operation and are refused there.
6
+ * Parsing stays free of engine imports so `--help` is fast.
7
+ */
8
+ export interface SpaceCommandIO {
9
+ cwd?: string;
10
+ stdout?: (text: string) => void;
11
+ stderr?: (text: string) => void;
12
+ }
13
+ export declare const spaceCommandUsage = "Usage: om-memory-system space <action> [name] [options]\n\nActions:\n list List spaces and show the active one\n status Show the space this project uses now\n create <name> Add an empty space (it does not become active)\n use <name> Make a space the active one for every host, from the next turn\n archive <name> Hide a space from the switcher and keep its data\n unarchive <name> Show an archived space again\n preview [--from <a>] --to <b> [--since <date>] [--until <date>] [--project <tag>]\n Count the memories a copy would carry; writes nothing\n copy [--from <a>] --to <b> [--since <date>] [--until <date>] [--project <tag>]\n Copy memories into another space; the source stays unchanged\n link <name> [--in <space>]\n Make <name> searchable from <space> (the active space by default)\n unlink <name> [--in <space>]\n Remove that link\n export <name> --out <file> [--since <date>] [--until <date>] [--include-profile]\n Write one space to a JSON file that is safe to share\n import <file> --name <new> [--retag-to-project <folder>]\n Create a new space from an export file\n\n<name> is a space name or id. Dates are ISO 8601 and both bounds are inclusive;\n--project takes the project tag a preview's groups show. Moving memories between\nspaces, and deleting, renaming, backing up or restoring a space, are only possible\nin the web app.\n\nExamples:\n om-memory-system space list\n om-memory-system space create \"Client work\"\n om-memory-system space use \"Client work\"\n om-memory-system space copy --from Experiments --to \"Client work\" --since 2026-10-01\n om-memory-system space link \"Client work\" --in Experiments\n om-memory-system space export \"Client work\" --out client.json";
14
+ /** Run `om-memory-system space ...` and return the process exit code. */
15
+ export declare function runSpaceCommand(argv: string[], io?: SpaceCommandIO): Promise<number>;
@@ -0,0 +1,189 @@
1
+ import { resolve } from "node:path";
2
+ import { SPACE_ACTIONS, WEB_ONLY_SPACE_ACTIONS } from "../services/spaces/operations.js";
3
+ import { runSpaceExport, runSpaceImport } from "./space-transfer-command.js";
4
+ export const spaceCommandUsage = `Usage: om-memory-system space <action> [name] [options]
5
+
6
+ Actions:
7
+ list List spaces and show the active one
8
+ status Show the space this project uses now
9
+ create <name> Add an empty space (it does not become active)
10
+ use <name> Make a space the active one for every host, from the next turn
11
+ archive <name> Hide a space from the switcher and keep its data
12
+ unarchive <name> Show an archived space again
13
+ preview [--from <a>] --to <b> [--since <date>] [--until <date>] [--project <tag>]
14
+ Count the memories a copy would carry; writes nothing
15
+ copy [--from <a>] --to <b> [--since <date>] [--until <date>] [--project <tag>]
16
+ Copy memories into another space; the source stays unchanged
17
+ link <name> [--in <space>]
18
+ Make <name> searchable from <space> (the active space by default)
19
+ unlink <name> [--in <space>]
20
+ Remove that link
21
+ export <name> --out <file> [--since <date>] [--until <date>] [--include-profile]
22
+ Write one space to a JSON file that is safe to share
23
+ import <file> --name <new> [--retag-to-project <folder>]
24
+ Create a new space from an export file
25
+
26
+ <name> is a space name or id. Dates are ISO 8601 and both bounds are inclusive;
27
+ --project takes the project tag a preview's groups show. Moving memories between
28
+ spaces, and deleting, renaming, backing up or restoring a space, are only possible
29
+ in the web app.
30
+
31
+ Examples:
32
+ om-memory-system space list
33
+ om-memory-system space create "Client work"
34
+ om-memory-system space use "Client work"
35
+ om-memory-system space copy --from Experiments --to "Client work" --since 2026-10-01
36
+ om-memory-system space link "Client work" --in Experiments
37
+ om-memory-system space export "Client work" --out client.json`;
38
+ /** Every action the parser admits; the shared operation refuses the web-only ones. */
39
+ const KNOWN_ACTIONS = new Set([
40
+ ...SPACE_ACTIONS,
41
+ ...WEB_ONLY_SPACE_ACTIONS,
42
+ "move",
43
+ "export",
44
+ "import",
45
+ ]);
46
+ /** The value flags of the transfer forms. `--directory` is the runner's own flag. */
47
+ const VALUE_FLAGS = new Set([
48
+ "--from",
49
+ "--to",
50
+ "--since",
51
+ "--until",
52
+ "--project",
53
+ "--in",
54
+ "--out",
55
+ "--name",
56
+ "--retag-to-project",
57
+ "--directory",
58
+ ]);
59
+ /** How many positional arguments each action takes and which value flags it allows. */
60
+ const ACTION_FORMS = {
61
+ list: { positionals: 0, flags: new Set() },
62
+ status: { positionals: 0, flags: new Set() },
63
+ create: { positionals: 1, flags: new Set() },
64
+ use: { positionals: 1, flags: new Set() },
65
+ archive: { positionals: 1, flags: new Set() },
66
+ unarchive: { positionals: 1, flags: new Set() },
67
+ delete: { positionals: 1, flags: new Set() },
68
+ rename: { positionals: 1, flags: new Set() },
69
+ backup: { positionals: 1, flags: new Set() },
70
+ restore: { positionals: 1, flags: new Set() },
71
+ move: { positionals: 1, flags: new Set(["--from", "--to"]) },
72
+ preview: {
73
+ positionals: 1,
74
+ flags: new Set(["--from", "--to", "--since", "--until", "--project"]),
75
+ },
76
+ copy: { positionals: 1, flags: new Set(["--from", "--to", "--since", "--until", "--project"]) },
77
+ link: { positionals: 1, flags: new Set(["--in"]) },
78
+ unlink: { positionals: 1, flags: new Set(["--in"]) },
79
+ export: { positionals: 1, flags: new Set(["--out", "--since", "--until"]) },
80
+ import: { positionals: 1, flags: new Set(["--name", "--retag-to-project"]) },
81
+ };
82
+ /** One pass over the argument list; every option is `--flag value` or a positional. */
83
+ function parseSpaceArgs(argv) {
84
+ const parsed = {
85
+ values: new Map(),
86
+ includeProfile: false,
87
+ positionals: [],
88
+ directory: undefined,
89
+ };
90
+ for (let i = 0; i < argv.length; i++) {
91
+ const token = argv[i];
92
+ if (token === "--include-profile") {
93
+ parsed.includeProfile = true;
94
+ continue;
95
+ }
96
+ if (token.startsWith("--")) {
97
+ if (!VALUE_FLAGS.has(token))
98
+ return `Unknown option "${token}".`;
99
+ const value = argv[++i];
100
+ if (value === undefined)
101
+ return `Option "${token}" needs a value.`;
102
+ parsed.values.set(token.slice(2), value);
103
+ continue;
104
+ }
105
+ parsed.positionals.push(token);
106
+ }
107
+ parsed.directory = parsed.values.get("directory");
108
+ return parsed;
109
+ }
110
+ /** Run `om-memory-system space ...` and return the process exit code. */
111
+ export async function runSpaceCommand(argv, io = {}) {
112
+ const stdout = io.stdout ?? ((text) => console.log(text));
113
+ const stderr = io.stderr ?? ((text) => console.error(text));
114
+ const [action, ...rest] = argv;
115
+ if (action === undefined || action === "--help" || action === "-h") {
116
+ stdout(spaceCommandUsage);
117
+ return 0;
118
+ }
119
+ const fail = (problem) => {
120
+ stderr(`${problem}\n\n${spaceCommandUsage}`);
121
+ return 1;
122
+ };
123
+ if (!KNOWN_ACTIONS.has(action))
124
+ return fail(`Unknown space action "${action}"`);
125
+ const parsed = parseSpaceArgs(rest);
126
+ if (typeof parsed === "string")
127
+ return fail(parsed);
128
+ const form = ACTION_FORMS[action];
129
+ const { values, positionals, includeProfile } = parsed;
130
+ if (positionals.length > form.positionals) {
131
+ return fail(`Unexpected argument "${positionals[form.positionals]}"`);
132
+ }
133
+ for (const flag of values.keys()) {
134
+ if (flag === "directory")
135
+ continue;
136
+ if (!form.flags.has(`--${flag}`))
137
+ return fail(`The ${action} action does not take --${flag}.`);
138
+ }
139
+ if (includeProfile && action !== "export") {
140
+ return fail(`The ${action} action does not take --include-profile.`);
141
+ }
142
+ const directory = resolve(parsed.directory ?? io.cwd ?? process.cwd());
143
+ let result;
144
+ try {
145
+ if (action === "export" || action === "import") {
146
+ // The portability runners own their config bridge; the memory tool has none.
147
+ result =
148
+ action === "export"
149
+ ? await runSpaceExport({
150
+ name: positionals[0] ?? "",
151
+ outPath: values.get("out") ?? "",
152
+ ...(values.has("since") ? { since: values.get("since") } : {}),
153
+ ...(values.has("until") ? { until: values.get("until") } : {}),
154
+ ...(includeProfile ? { includeProfile: true } : {}),
155
+ }, directory)
156
+ : await runSpaceImport({
157
+ filePath: positionals[0] ?? "",
158
+ name: values.get("name") ?? "",
159
+ ...(values.has("retag-to-project")
160
+ ? { retagToProject: values.get("retag-to-project") }
161
+ : {}),
162
+ }, directory);
163
+ }
164
+ else {
165
+ const { initConfig } = await import("../config.js");
166
+ initConfig(directory);
167
+ const { runSpaceMode } = await import("../core/space-tool.js");
168
+ result = await runSpaceMode({
169
+ mode: "space",
170
+ spaceAction: action,
171
+ // The transfer source may be positional or --from; the other actions take a name.
172
+ spaceName: positionals[0] ??
173
+ (action === "preview" || action === "copy" || action === "move"
174
+ ? values.get("from")
175
+ : undefined),
176
+ ...(values.has("to") ? { spaceTo: values.get("to") } : {}),
177
+ ...(values.has("since") ? { spaceSince: values.get("since") } : {}),
178
+ ...(values.has("until") ? { spaceUntil: values.get("until") } : {}),
179
+ ...(values.has("project") ? { spaceProject: values.get("project") } : {}),
180
+ ...(values.has("in") ? { spaceIn: values.get("in") } : {}),
181
+ }, directory);
182
+ }
183
+ }
184
+ catch (error) {
185
+ result = { success: false, error: error instanceof Error ? error.message : String(error) };
186
+ }
187
+ stdout(JSON.stringify(result, null, 2));
188
+ return result.success === false ? 1 : 0;
189
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * The `space export` and `space import` runners (design D8). Both are CLI-only: the
3
+ * `memory` tool `space` mode refuses them. This file is the one place that bridges
4
+ * CONFIG into `exportSpace` and `importSpace`, which themselves never read CONFIG;
5
+ * the shared space operation runs them only when the CLI hands it this bridge.
6
+ */
7
+ export interface SpaceExportCliRequest {
8
+ /** The space to export: a name or id. */
9
+ name: string;
10
+ outPath: string;
11
+ since?: string;
12
+ until?: string;
13
+ includeProfile?: boolean;
14
+ }
15
+ export interface SpaceImportCliRequest {
16
+ /** Path of the export file to read. */
17
+ filePath: string;
18
+ /** The name of the new space the file becomes. */
19
+ name: string;
20
+ retagToProject?: string;
21
+ }
22
+ /** Runs `space export <name> --out <file>` through the shared space operation. */
23
+ export declare function runSpaceExport(request: SpaceExportCliRequest, directory: string): Promise<Record<string, unknown>>;
24
+ /** Runs `space import <file> --name <new>` through the shared space operation. */
25
+ export declare function runSpaceImport(request: SpaceImportCliRequest, directory: string): Promise<Record<string, unknown>>;
@@ -0,0 +1,79 @@
1
+ import { homedir } from "node:os";
2
+ /** The bridge the CLI hands the shared operation: the export/import capability made real. */
3
+ async function portabilityBridge(files, directory) {
4
+ const { CONFIG } = await import("../config.js");
5
+ return {
6
+ export: async (request) => {
7
+ const { exportSpace, getOmmsVersion } = await import("../services/spaces/export-space.js");
8
+ return exportSpace({
9
+ ...request,
10
+ embeddingModel: CONFIG.embeddingModel,
11
+ embeddingDimensions: CONFIG.embeddingDimensions,
12
+ ommsVersion: getOmmsVersion(),
13
+ homePath: homedir(),
14
+ });
15
+ },
16
+ import: async (request) => {
17
+ const { importSpace } = await import("../services/spaces/import-space.js");
18
+ const { embeddingService } = await import("../services/embedding.js");
19
+ const { getTags } = await import("../services/tags.js");
20
+ const user = getTags(directory).user;
21
+ return importSpace({
22
+ ...request,
23
+ files,
24
+ embedding: { model: CONFIG.embeddingModel, dimensions: CONFIG.embeddingDimensions },
25
+ embed: (text) => embeddingService.embedWithTimeout(text, { task: "document" }),
26
+ user: {
27
+ userTag: user.tag,
28
+ displayName: user.displayName,
29
+ ...(user.userName ? { userName: user.userName } : {}),
30
+ ...(user.userEmail ? { userEmail: user.userEmail } : {}),
31
+ },
32
+ });
33
+ },
34
+ };
35
+ }
36
+ /** Runs `space export <name> --out <file>` through the shared space operation. */
37
+ export async function runSpaceExport(request, directory) {
38
+ const { CONFIG, getActiveSpace, initConfig } = await import("../config.js");
39
+ initConfig(directory);
40
+ const { executeSpaceOperation } = await import("../services/spaces/operations.js");
41
+ const { spaceFiles } = await import("../services/spaces/registry-store.js");
42
+ const { ommsHomeDir } = await import("../services/spaces/storage-resolution.js");
43
+ const { spaceOperationSettings } = await import("../core/space-tool.js");
44
+ const files = spaceFiles(ommsHomeDir());
45
+ return executeSpaceOperation({
46
+ action: "export",
47
+ name: request.name,
48
+ outPath: request.outPath,
49
+ ...(request.since === undefined ? {} : { since: request.since }),
50
+ ...(request.until === undefined ? {} : { until: request.until }),
51
+ ...(request.includeProfile ? { includeProfile: true } : {}),
52
+ }, {
53
+ files,
54
+ current: getActiveSpace(),
55
+ settings: spaceOperationSettings(CONFIG, files.spacesRoot),
56
+ portability: await portabilityBridge(files, directory),
57
+ });
58
+ }
59
+ /** Runs `space import <file> --name <new>` through the shared space operation. */
60
+ export async function runSpaceImport(request, directory) {
61
+ const { CONFIG, getActiveSpace, initConfig } = await import("../config.js");
62
+ initConfig(directory);
63
+ const { executeSpaceOperation } = await import("../services/spaces/operations.js");
64
+ const { spaceFiles } = await import("../services/spaces/registry-store.js");
65
+ const { ommsHomeDir } = await import("../services/spaces/storage-resolution.js");
66
+ const { spaceOperationSettings } = await import("../core/space-tool.js");
67
+ const files = spaceFiles(ommsHomeDir());
68
+ return executeSpaceOperation({
69
+ action: "import",
70
+ name: request.name,
71
+ filePath: request.filePath,
72
+ ...(request.retagToProject === undefined ? {} : { retagToProject: request.retagToProject }),
73
+ }, {
74
+ files,
75
+ current: getActiveSpace(),
76
+ settings: spaceOperationSettings(CONFIG, files.spacesRoot),
77
+ portability: await portabilityBridge(files, directory),
78
+ });
79
+ }
package/dist/config.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { isPlaceholderApiKey } from "./services/ai/api-key-placeholder.js";
2
2
  import { type KeywordMode } from "./utils/keyword-boost.js";
3
+ import { type ActiveSpace } from "./services/spaces/active-space.js";
3
4
  interface OmmsConfig {
4
5
  storagePath?: string;
5
6
  userEmailOverride?: string;
@@ -359,6 +360,8 @@ type RuntimeConfig = ReturnType<typeof buildConfig>;
359
360
  export { getAutoCaptureProviderStatus, type AutoCaptureProviderStatus, } from "./services/ai/live-model-choice.js";
360
361
  export { isPlaceholderApiKey };
361
362
  export declare function hasAutoCaptureProviderConfig(config?: RuntimeConfig): boolean;
363
+ /** The memory space in use, as of the last config load. */
364
+ export declare const getActiveSpace: () => ActiveSpace;
362
365
  export declare function refreshConfigIfChanged(directory: string): void;
363
366
  export declare function initConfig(directory: string, options?: {
364
367
  strict?: boolean;
package/dist/config.js CHANGED
@@ -17,7 +17,10 @@ import { defaultSimilarityThreshold } from "./utils/threshold-default.js";
17
17
  import { RERANKER_TIMEOUT_DEFAULT_MS } from "./services/request-timeouts.js";
18
18
  import { checkedRerankerSettings } from "./utils/reranker-settings.js";
19
19
  import { checkedKeywordBoost, checkedKeywordMode, } from "./utils/keyword-boost.js";
20
- import { resolveDefaultStoragePath, runLegacyStoreMigration, legacyMigrationPaths, } from "./services/legacy-migration.js";
20
+ import { runLegacyStoreMigration, legacyMigrationPaths } from "./services/legacy-migration.js";
21
+ import { expandPath, storagePathFrom, withSkillStorage, } from "./services/spaces/storage-resolution.js";
22
+ import { resolveActiveSpace } from "./services/spaces/active-space.js";
23
+ import { resetStoreHandles } from "./services/spaces/store-handles.js";
21
24
  const OMMS_CONFIG_DIR = join(homedir(), ".config", "omms");
22
25
  const DATA_DIR = join(homedir(), ".omms");
23
26
  const LEGACY_CONFIG_FILES = [
@@ -120,23 +123,6 @@ const DEFAULTS = {
120
123
  injectOn: "first",
121
124
  },
122
125
  };
123
- function expandPath(path) {
124
- if (path.startsWith("~/")) {
125
- return join(homedir(), path.slice(2));
126
- }
127
- if (path === "~") {
128
- return homedir();
129
- }
130
- return path;
131
- }
132
- function storagePathFrom(fileConfig) {
133
- return fileConfig.storagePath ? expandPath(fileConfig.storagePath) : resolveDefaultStoragePath();
134
- }
135
- /** Keeps skills.db in the global storage path when a project config moves storagePath (ADR-036). */
136
- function withSkillStorage(config, global) {
137
- const path = storagePathFrom(global);
138
- return { ...config, skillStoragePath: path === config.storagePath ? undefined : path };
139
- }
140
126
  /**
141
127
  * Load the first config file that exists, in priority order. A file that
142
128
  * exists but cannot be read or parsed is not skipped: falling through would let
@@ -472,7 +458,7 @@ function projectConfigPaths(directory) {
472
458
  ];
473
459
  }
474
460
  function configSignature(directory) {
475
- return [...CONFIG_FILES, ...projectConfigPaths(directory)]
461
+ return [...CONFIG_FILES, ...projectConfigPaths(directory), join(DATA_DIR, "spaces.json")]
476
462
  .filter((path) => existsSync(path))
477
463
  .map((path) => {
478
464
  try {
@@ -491,6 +477,9 @@ let lastConfigDirectory;
491
477
  let lastConfigSignature;
492
478
  /** Directory and signature of the last reload that failed. */
493
479
  let lastFailedConfig;
480
+ let activeSpace = { id: "default", name: "Default", customStoragePath: false };
481
+ /** The memory space in use, as of the last config load. */
482
+ export const getActiveSpace = () => activeSpace;
494
483
  export function refreshConfigIfChanged(directory) {
495
484
  // Both hosts initialise their config at startup. Direct callers that have
496
485
  // not initialised it keep their in-memory settings untouched.
@@ -565,10 +554,18 @@ export function initConfig(directory, options = {}) {
565
554
  delete projectOverrides.webServerAutoStart;
566
555
  delete projectOverrides.webServerEnabled;
567
556
  const merged = { ...globalConfig, ...projectOverrides };
557
+ // The active memory space picks the storage folder unless the project sets its own (ADR-041).
558
+ activeSpace = resolveActiveSpace(DATA_DIR, storagePathFrom(globalConfig), "storagePath" in projectConfig);
559
+ if (activeSpace.storagePath)
560
+ merged.storagePath = activeSpace.storagePath;
568
561
  const nextConfig = withSkillStorage(buildConfig(merged), globalConfig);
569
562
  lastFileConfig = merged;
570
563
  lastGlobalFileConfig = globalConfig;
564
+ const storageMoved = nextConfig.storagePath !== CONFIG.storagePath;
571
565
  CONFIG = nextConfig;
566
+ // A new storage folder (another memory space) needs fresh store handles (ADR-041).
567
+ if (storageMoved)
568
+ void resetStoreHandles();
572
569
  lastConfigDirectory = directory;
573
570
  lastConfigSignature = configSignature(directory);
574
571
  }
@@ -1,7 +1,7 @@
1
1
  import { type MemoryScope } from "../services/client.js";
2
2
  import { getTags } from "../services/tags.js";
3
3
  import type { MemoryHost, MemoryType } from "../types/index.js";
4
- export type MemoryOperationMode = "add" | "search" | "profile" | "list" | "forget" | "help" | "migrate" | "list-shards" | "export" | "import";
4
+ export type MemoryOperationMode = "add" | "search" | "profile" | "list" | "forget" | "help" | "migrate" | "list-shards" | "export" | "import" | "space";
5
5
  export interface MemoryOperationArgs {
6
6
  mode?: MemoryOperationMode;
7
7
  content?: string;
@@ -17,6 +17,20 @@ export interface MemoryOperationArgs {
17
17
  inputPath?: string;
18
18
  dryRun?: boolean;
19
19
  allowLinkedSource?: boolean;
20
+ /** `space` mode: list, status, create, archive, unarchive, use, preview, copy, link or unlink. */
21
+ spaceAction?: string;
22
+ /** `space` mode: the space name or id; the transfer source or the link target. */
23
+ spaceName?: string;
24
+ /** `space` mode: the transfer target (preview, copy). */
25
+ spaceTo?: string;
26
+ /** `space` mode: inclusive ISO 8601 lower bound on the creation time (preview, copy). */
27
+ spaceSince?: string;
28
+ /** `space` mode: inclusive ISO 8601 upper bound on the creation time (preview, copy). */
29
+ spaceUntil?: string;
30
+ /** `space` mode: only memories with this project tag (preview, copy). */
31
+ spaceProject?: string;
32
+ /** `space` mode: the space that owns a link (link, unlink); the active space when omitted. */
33
+ spaceIn?: string;
20
34
  }
21
35
  export interface MemoryOperationContext {
22
36
  directory: string;
@@ -14,6 +14,8 @@ function formatSearchResults(query, results, limit) {
14
14
  content: result.memory || result.chunk,
15
15
  type: result.type,
16
16
  similarity: Math.round(result.similarity * 100),
17
+ // A linked result carries its space so the caller can tell it is read-only.
18
+ ...(result.spaceName ? { space: result.spaceName } : {}),
17
19
  })),
18
20
  };
19
21
  }
@@ -28,6 +30,10 @@ export async function executeMemoryOperation(args, context) {
28
30
  };
29
31
  }
30
32
  const mode = args.mode || "help";
33
+ if (mode === "space") {
34
+ const { runSpaceMode } = await import("./space-tool.js");
35
+ return runSpaceMode(args, context.directory);
36
+ }
31
37
  const needsEmbedding = !["help", "list-shards", "migrate", "export"].includes(mode);
32
38
  if (needsEmbedding) {
33
39
  const embeddingInitError = memoryClient.getEmbeddingInitError?.();
@@ -75,6 +81,19 @@ export async function executeMemoryOperation(args, context) {
75
81
  args: ["content?"],
76
82
  },
77
83
  { command: "list", description: "List recent memories", args: ["limit?"] },
84
+ {
85
+ command: "space",
86
+ description: "Manage memory spaces: list, status, create, archive, unarchive, use, preview, copy, link, unlink. Moving, deleting, exporting and importing are not offered here",
87
+ args: [
88
+ "spaceAction?",
89
+ "spaceName?",
90
+ "spaceTo?",
91
+ "spaceSince?",
92
+ "spaceUntil?",
93
+ "spaceProject?",
94
+ "spaceIn?",
95
+ ],
96
+ },
78
97
  { command: "forget", description: "Remove memory", args: ["memoryId"] },
79
98
  {
80
99
  command: "list-shards",
@@ -132,7 +151,7 @@ export async function executeMemoryOperation(args, context) {
132
151
  case "search": {
133
152
  if (!args.query)
134
153
  return { success: false, error: "query required" };
135
- const result = await memoryClient.searchMemories(args.query, tags.project.tag, args.scope ?? CONFIG.memory.defaultScope);
154
+ const result = await memoryClient.searchMemories(args.query, tags.project.tag, args.scope ?? CONFIG.memory.defaultScope, { includeLinked: true });
136
155
  if (!result.success)
137
156
  return { success: false, error: result.error };
138
157
  return formatSearchResults(args.query, result, args.limit);
@@ -201,7 +220,7 @@ export async function executeMemoryOperation(args, context) {
201
220
  };
202
221
  }
203
222
  case "list": {
204
- const result = await memoryClient.listMemories(tags.project.tag, args.limit || 20, args.scope ?? CONFIG.memory.defaultScope);
223
+ const result = await memoryClient.listMemories(tags.project.tag, args.limit || 20, args.scope ?? CONFIG.memory.defaultScope, { includeLinked: true });
205
224
  if (!result.success)
206
225
  return { success: false, error: result.error };
207
226
  return {