om-memory-system 5.5.0 → 5.7.0-next.218

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 (76) hide show
  1. package/README.md +1 -1
  2. package/dist/adapters/pi/extension.js +5 -0
  3. package/dist/cli/space-command.d.ts +6 -5
  4. package/dist/cli/space-command.js +154 -13
  5. package/dist/cli/space-transfer-command.d.ts +25 -0
  6. package/dist/cli/space-transfer-command.js +79 -0
  7. package/dist/core/memory-operations.d.ts +12 -2
  8. package/dist/core/memory-operations.js +14 -4
  9. package/dist/core/recall-summary.js +7 -5
  10. package/dist/core/retrieval.js +5 -2
  11. package/dist/core/space-tool.d.ts +9 -4
  12. package/dist/core/space-tool.js +35 -3
  13. package/dist/services/active-space-access.d.ts +6 -0
  14. package/dist/services/active-space-access.js +8 -0
  15. package/dist/services/chat/chat-runtime.js +1 -1
  16. package/dist/services/client.d.ts +6 -16
  17. package/dist/services/client.js +54 -62
  18. package/dist/services/context.d.ts +2 -0
  19. package/dist/services/context.js +12 -1
  20. package/dist/services/linked-search.d.ts +108 -0
  21. package/dist/services/linked-search.js +219 -0
  22. package/dist/services/space-routes.d.ts +2 -0
  23. package/dist/services/space-routes.js +4 -3
  24. package/dist/services/space-transfer-routes.d.ts +10 -0
  25. package/dist/services/space-transfer-routes.js +344 -0
  26. package/dist/services/spaces/active-space.js +14 -7
  27. package/dist/services/spaces/delete.d.ts +2 -0
  28. package/dist/services/spaces/delete.js +4 -0
  29. package/dist/services/spaces/export-space.d.ts +60 -0
  30. package/dist/services/spaces/export-space.js +367 -0
  31. package/dist/services/spaces/identifier-scrub.d.ts +26 -0
  32. package/dist/services/spaces/identifier-scrub.js +40 -0
  33. package/dist/services/spaces/import-space.d.ts +85 -0
  34. package/dist/services/spaces/import-space.js +349 -0
  35. package/dist/services/spaces/operations.d.ts +57 -5
  36. package/dist/services/spaces/operations.js +161 -5
  37. package/dist/services/spaces/read-store-cache.d.ts +13 -0
  38. package/dist/services/spaces/read-store-cache.js +34 -0
  39. package/dist/services/spaces/registry-store.d.ts +4 -0
  40. package/dist/services/spaces/registry-store.js +15 -1
  41. package/dist/services/spaces/registry.d.ts +32 -0
  42. package/dist/services/spaces/registry.js +81 -7
  43. package/dist/services/spaces/space-store.d.ts +50 -0
  44. package/dist/services/spaces/space-store.js +72 -0
  45. package/dist/services/spaces/transfer-service.d.ts +41 -0
  46. package/dist/services/spaces/transfer-service.js +408 -0
  47. package/dist/services/spaces/transfer-types.d.ts +163 -0
  48. package/dist/services/spaces/transfer-types.js +10 -0
  49. package/dist/services/spaces/transfer.d.ts +31 -0
  50. package/dist/services/spaces/transfer.js +276 -0
  51. package/dist/services/turso/connection-manager.d.ts +4 -2
  52. package/dist/services/turso/connection-manager.js +11 -5
  53. package/dist/services/turso/cross-process-write-lock.d.ts +1 -1
  54. package/dist/services/turso/cross-process-write-lock.js +5 -5
  55. package/dist/services/turso/operation-lock.d.ts +2 -2
  56. package/dist/services/turso/operation-lock.js +5 -5
  57. package/dist/services/turso/shard-manager.d.ts +8 -0
  58. package/dist/services/turso/shard-manager.js +23 -22
  59. package/dist/services/turso/vector-search.d.ts +6 -2
  60. package/dist/services/turso/vector-search.js +8 -4
  61. package/dist/services/user-profile/user-profile-manager.d.ts +5 -1
  62. package/dist/services/user-profile/user-profile-manager.js +13 -10
  63. package/dist/services/web-route-modules.d.ts +5 -1
  64. package/dist/services/web-route-modules.js +4 -0
  65. package/dist/services/web-server.js +1 -1
  66. package/dist/shared/api/space-export-schemas.d.ts +159 -0
  67. package/dist/shared/api/space-export-schemas.js +72 -0
  68. package/dist/v2/adapter.d.ts +15 -0
  69. package/dist/v2/adapter.js +5 -0
  70. package/dist/web/assets/index-2BNRLGqe.css +2 -0
  71. package/dist/web/assets/index-C6WnDE6h.js +170 -0
  72. package/dist/web/index.html +2 -2
  73. package/package.json +1 -1
  74. package/skills/omms-memory/SKILL.md +16 -14
  75. package/dist/web/assets/index-Dcvz3EXt.js +0 -170
  76. package/dist/web/assets/index-b8k5Tuei.css +0 -2
package/README.md CHANGED
@@ -119,7 +119,7 @@ cannot be saved.
119
119
 
120
120
  ### Memory spaces
121
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. See [Memory spaces](docs/memory-spaces.md).
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
123
 
124
124
  ### Settings
125
125
 
@@ -350,6 +350,11 @@ export default function ommsPiExtension(pi) {
350
350
  allowLinkedSource: Type.Optional(Type.Boolean()),
351
351
  spaceAction: Type.Optional(Type.String()),
352
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()),
353
358
  }),
354
359
  async execute(_toolCallId, args, _signal, _onUpdate, ctx) {
355
360
  const result = await executeMemoryOperation(args, {
@@ -1,14 +1,15 @@
1
1
  /**
2
- * `om-memory-system space <action> [name]`: list, create, switch, archive and unarchive memory
3
- * spaces from a terminal. Deleting, renaming, backing up and restoring a space exist only in the
4
- * web app, so those actions are refused here. Parsing stays free of engine imports so `--help`
5
- * is fast.
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.
6
7
  */
7
8
  export interface SpaceCommandIO {
8
9
  cwd?: string;
9
10
  stdout?: (text: string) => void;
10
11
  stderr?: (text: string) => void;
11
12
  }
12
- export declare const spaceCommandUsage = "Usage: om-memory-system space <action> [name]\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\n<name> is a space name or id. Deleting, renaming, backing up and restoring a space\nare only possible in 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\"";
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";
13
14
  /** Run `om-memory-system space ...` and return the process exit code. */
14
15
  export declare function runSpaceCommand(argv: string[], io?: SpaceCommandIO): Promise<number>;
@@ -1,6 +1,7 @@
1
1
  import { resolve } from "node:path";
2
2
  import { SPACE_ACTIONS, WEB_ONLY_SPACE_ACTIONS } from "../services/spaces/operations.js";
3
- export const spaceCommandUsage = `Usage: om-memory-system space <action> [name]
3
+ import { runSpaceExport, runSpaceImport } from "./space-transfer-command.js";
4
+ export const spaceCommandUsage = `Usage: om-memory-system space <action> [name] [options]
4
5
 
5
6
  Actions:
6
7
  list List spaces and show the active one
@@ -9,36 +10,176 @@ Actions:
9
10
  use <name> Make a space the active one for every host, from the next turn
10
11
  archive <name> Hide a space from the switcher and keep its data
11
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
12
25
 
13
- <name> is a space name or id. Deleting, renaming, backing up and restoring a space
14
- are only possible in the web app.
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.
15
30
 
16
31
  Examples:
17
32
  om-memory-system space list
18
33
  om-memory-system space create "Client work"
19
- om-memory-system space use "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
+ }
20
110
  /** Run `om-memory-system space ...` and return the process exit code. */
21
111
  export async function runSpaceCommand(argv, io = {}) {
22
112
  const stdout = io.stdout ?? ((text) => console.log(text));
23
113
  const stderr = io.stderr ?? ((text) => console.error(text));
24
- const [action, name, ...extra] = argv;
114
+ const [action, ...rest] = argv;
25
115
  if (action === undefined || action === "--help" || action === "-h") {
26
116
  stdout(spaceCommandUsage);
27
117
  return 0;
28
118
  }
29
- const known = [...SPACE_ACTIONS, ...WEB_ONLY_SPACE_ACTIONS];
30
- if (!known.includes(action) || extra.length > 0 || (name?.startsWith("--") ?? false)) {
31
- const problem = extra.length > 0 ? `Unexpected argument "${extra[0]}"` : `Unknown space action "${action}"`;
119
+ const fail = (problem) => {
32
120
  stderr(`${problem}\n\n${spaceCommandUsage}`);
33
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.`);
34
141
  }
35
- const directory = resolve(io.cwd ?? process.cwd());
142
+ const directory = resolve(parsed.directory ?? io.cwd ?? process.cwd());
36
143
  let result;
37
144
  try {
38
- const { initConfig } = await import("../config.js");
39
- initConfig(directory);
40
- const { runSpaceMode } = await import("../core/space-tool.js");
41
- result = await runSpaceMode({ mode: "space", spaceAction: action, spaceName: name }, directory);
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
+ }
42
183
  }
43
184
  catch (error) {
44
185
  result = { success: false, error: error instanceof Error ? error.message : String(error) };
@@ -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
+ }
@@ -17,10 +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 or use. */
20
+ /** `space` mode: list, status, create, archive, unarchive, use, preview, copy, link or unlink. */
21
21
  spaceAction?: string;
22
- /** `space` mode: the space name or id. */
22
+ /** `space` mode: the space name or id; the transfer source or the link target. */
23
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;
24
34
  }
25
35
  export interface MemoryOperationContext {
26
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
  }
@@ -81,8 +83,16 @@ export async function executeMemoryOperation(args, context) {
81
83
  { command: "list", description: "List recent memories", args: ["limit?"] },
82
84
  {
83
85
  command: "space",
84
- description: "Manage memory spaces: list, status, create, archive, unarchive, use. Deleting a space is only possible in the web app",
85
- args: ["spaceAction?", "spaceName?"],
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
+ ],
86
96
  },
87
97
  { command: "forget", description: "Remove memory", args: ["memoryId"] },
88
98
  {
@@ -141,7 +151,7 @@ export async function executeMemoryOperation(args, context) {
141
151
  case "search": {
142
152
  if (!args.query)
143
153
  return { success: false, error: "query required" };
144
- 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 });
145
155
  if (!result.success)
146
156
  return { success: false, error: result.error };
147
157
  return formatSearchResults(args.query, result, args.limit);
@@ -210,7 +220,7 @@ export async function executeMemoryOperation(args, context) {
210
220
  };
211
221
  }
212
222
  case "list": {
213
- 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 });
214
224
  if (!result.success)
215
225
  return { success: false, error: result.error };
216
226
  return {
@@ -7,12 +7,14 @@ export function queryLine(query, max = QUERY_CUT) {
7
7
  return line.length > max ? `${line.slice(0, max - 3)}...` : line;
8
8
  }
9
9
  // Entry shape comes from `formatContextForPrompt`: `<memory id="..." relevance="N%">`, with the
10
- // id missing when the result had none. A body ends at its own `</memory>` when another entry, a
11
- // section close or the end of the text follows it. The Claude Code hook can cut a block short and
12
- // leave tags open, so a body also ends at a section close or at the end of the text. An opening
13
- // tag inside the text, even at the start of a line, does not end it: a memory can quote the tag.
10
+ // id missing when the result had none. A linked memory carries further attributes after the
11
+ // relevance (`space="..."`), so any number of them may sit before the tag closes. A body ends at
12
+ // its own `</memory>` when another entry, a section close or the end of the text follows it. The
13
+ // Claude Code hook can cut a block short and leave tags open, so a body also ends at a section
14
+ // close or at the end of the text. An opening tag inside the text, even at the start of a line,
15
+ // does not end it: a memory can quote the tag.
14
16
  const SECTION_CLOSE = String.raw `<\/project_knowledge>|<\/memory_context>|<\/omms-retrieval>`;
15
- const MEMORY_ENTRY = new RegExp(String.raw `<memory(?:\s+id="([^"]*)")?\s+relevance="(\d+)%">([\s\S]*?)` +
17
+ const MEMORY_ENTRY = new RegExp(String.raw `<memory(?:\s+id="([^"]*)")?\s+relevance="(\d+)%"(?:\s+[a-z][a-z-]*="[^"]*")*>([\s\S]*?)` +
16
18
  String.raw `(?=<\/memory>\s*(?:<memory\s|${SECTION_CLOSE}|$)|${SECTION_CLOSE}|$)`, "g");
17
19
  // A restored-session section from `formatMemoriesForCompaction`: `### Memory N (id)`, no relevance.
18
20
  // Same rule as the Claude Code parser (`hooks/omms-recall-parse.js`).
@@ -17,7 +17,10 @@ export async function searchSectionMemories(query, projectTag, sessionId) {
17
17
  // Snapshot before the async search: an excludeCurrentSession edit during
18
18
  // the search must not change this operation's filtering.
19
19
  const excludeCurrentSession = CONFIG.chatMessage.excludeCurrentSession;
20
- const search = await memoryClient.searchMemories(query, projectTag);
20
+ // Recall reads linked spaces too (D6); capture and profile steps keep the default.
21
+ const search = await memoryClient.searchMemories(query, projectTag, "project", {
22
+ includeLinked: true,
23
+ });
21
24
  if (!search.success)
22
25
  return null;
23
26
  const results = excludeCurrentSession
@@ -57,7 +60,7 @@ export async function buildRetrievalSection(prompt, directory, sessionId, budget
57
60
  * Code `SessionStart`). Returns null when no memory qualifies.
58
61
  */
59
62
  export async function buildRecentMemoriesSection(options) {
60
- const listResult = await memoryClient.listMemories(options.projectTag, options.maxMemories);
63
+ const listResult = await memoryClient.listMemories(options.projectTag, options.maxMemories, "project", { includeLinked: true });
61
64
  let memories = listResult.success ? listResult.memories : [];
62
65
  if (options.excludeCurrentSession) {
63
66
  memories = memories.filter((m) => m.metadata?.sessionID !== options.sessionId);
@@ -1,6 +1,11 @@
1
1
  import type { MemoryOperationArgs } from "./memory-operations.js";
2
- /**
3
- * The `space` mode of the `memory` tool. It loads on demand, so the other modes never import
4
- * the space code. It needs no embedding model and no open memory store.
5
- */
2
+ import type { SpaceOperationSettings } from "../services/spaces/operations.js";
3
+ /** Snapshots the settings the space-bound operations need from a CONFIG-shaped object. */
4
+ export declare function spaceOperationSettings(config: {
5
+ skillStoragePath?: string;
6
+ storagePath: string;
7
+ embeddingModel: string;
8
+ embeddingDimensions: number;
9
+ maxVectorsPerShard: number;
10
+ }, spacesRoot: string): SpaceOperationSettings;
6
11
  export declare function runSpaceMode(args: MemoryOperationArgs, directory: string): Promise<Record<string, unknown>>;
@@ -1,13 +1,45 @@
1
1
  /**
2
2
  * The `space` mode of the `memory` tool. It loads on demand, so the other modes never import
3
- * the space code. It needs no embedding model and no open memory store.
3
+ * the space code. `list`, `use` and `create` need no embedding model and no open memory store;
4
+ * `preview` and `copy` open space stores, so only they carry the settings snapshot.
4
5
  */
6
+ /** The actions that read or write a space store, so their settings are loaded only for them. */
7
+ const STORE_ACTIONS = new Set(["preview", "copy"]);
8
+ /** Snapshots the settings the space-bound operations need from a CONFIG-shaped object. */
9
+ export function spaceOperationSettings(config, spacesRoot) {
10
+ return {
11
+ paths: {
12
+ // Default's folder is the global store, even while a space or project folder is in use.
13
+ globalStoragePath: config.skillStoragePath ?? config.storagePath,
14
+ spacesRoot,
15
+ },
16
+ embedding: { model: config.embeddingModel, dimensions: config.embeddingDimensions },
17
+ maxVectorsPerShard: config.maxVectorsPerShard,
18
+ };
19
+ }
5
20
  export async function runSpaceMode(args, directory) {
6
- const { getActiveSpace, refreshConfigIfChanged } = await import("../config.js");
21
+ const { CONFIG, getActiveSpace, refreshConfigIfChanged } = await import("../config.js");
7
22
  const { executeSpaceOperation } = await import("../services/spaces/operations.js");
8
23
  const { spaceFiles } = await import("../services/spaces/registry-store.js");
9
24
  const { ommsHomeDir } = await import("../services/spaces/storage-resolution.js");
10
- const result = await executeSpaceOperation({ action: args.spaceAction, name: args.spaceName }, { files: spaceFiles(ommsHomeDir()), current: getActiveSpace() });
25
+ const files = spaceFiles(ommsHomeDir());
26
+ const action = args.spaceAction?.trim().toLowerCase();
27
+ const result = await executeSpaceOperation({
28
+ action: args.spaceAction,
29
+ name: args.spaceName,
30
+ ...(args.spaceTo === undefined ? {} : { to: args.spaceTo }),
31
+ ...(args.spaceSince === undefined ? {} : { since: args.spaceSince }),
32
+ ...(args.spaceUntil === undefined ? {} : { until: args.spaceUntil }),
33
+ ...(args.spaceProject === undefined ? {} : { project: args.spaceProject }),
34
+ ...(args.spaceIn === undefined ? {} : { owner: args.spaceIn }),
35
+ }, {
36
+ files,
37
+ current: getActiveSpace(),
38
+ // The transfer actions open space stores, so only they get the settings (design D8).
39
+ ...(STORE_ACTIONS.has(action ?? "")
40
+ ? { settings: spaceOperationSettings(CONFIG, files.spacesRoot) }
41
+ : {}),
42
+ });
11
43
  // A switch made here takes effect in this process now, not only on its next config refresh.
12
44
  refreshConfigIfChanged(directory);
13
45
  return result;
@@ -0,0 +1,6 @@
1
+ import type { LinkedOwner } from "./linked-search.js";
2
+ /**
3
+ * Reads the active space the way partial test stubs allow: a `config.js` stub may
4
+ * leave out `getActiveSpace`, and a missing one means the Default space.
5
+ */
6
+ export declare function activeOwner(): LinkedOwner;
@@ -0,0 +1,8 @@
1
+ import * as configModule from "../config.js";
2
+ /**
3
+ * Reads the active space the way partial test stubs allow: a `config.js` stub may
4
+ * leave out `getActiveSpace`, and a missing one means the Default space.
5
+ */
6
+ export function activeOwner() {
7
+ return configModule.getActiveSpace?.() ?? { id: "default", customStoragePath: false };
8
+ }
@@ -89,7 +89,7 @@ export async function chatServiceDeps(config) {
89
89
  retrieve: {
90
90
  searchMemories: async (query, scopeTag) => {
91
91
  const { memoryClient } = await import("../client.js");
92
- const result = await memoryClient.searchMemories(query, scopeTag ?? "", scopeTag ? "project" : "all-projects");
92
+ const result = await memoryClient.searchMemories(query, scopeTag ?? "", scopeTag ? "project" : "all-projects", { includeLinked: true });
93
93
  return result.success ? result.results : [];
94
94
  },
95
95
  embed: async (text) => {
@@ -1,4 +1,6 @@
1
+ import { type MemoryReadOptions } from "./linked-search.js";
1
2
  import type { MemoryType } from "../types/index.js";
3
+ import type { SearchResult } from "./turso/types.js";
2
4
  export type MemoryScope = "project" | "all-projects";
3
5
  export declare class LocalMemoryClient {
4
6
  private initPromise;
@@ -16,10 +18,10 @@ export declare class LocalMemoryClient {
16
18
  };
17
19
  reset(): void;
18
20
  close(): Promise<void>;
19
- searchMemories(query: string, containerTag: string, scope?: MemoryScope): Promise<{
21
+ searchMemories(query: string, containerTag: string, scope?: MemoryScope, options?: MemoryReadOptions): Promise<{
20
22
  warnings?: string[] | undefined;
21
23
  success: true;
22
- results: import("./turso/types.js").SearchResult[];
24
+ results: SearchResult[];
23
25
  total: number;
24
26
  timing: number;
25
27
  queryVector: Float32Array<ArrayBufferLike>;
@@ -60,21 +62,9 @@ export declare class LocalMemoryClient {
60
62
  success: boolean;
61
63
  error: string;
62
64
  }>;
63
- listMemories(containerTag: string, limit?: number, scope?: MemoryScope): Promise<{
65
+ listMemories(containerTag: string, limit?: number, scope?: MemoryScope, options?: MemoryReadOptions): Promise<{
64
66
  success: true;
65
- memories: {
66
- id: any;
67
- summary: any;
68
- type: string | undefined;
69
- createdAt: string;
70
- metadata: any;
71
- displayName: any;
72
- userName: any;
73
- userEmail: any;
74
- projectPath: any;
75
- projectName: any;
76
- gitRepoUrl: any;
77
- }[];
67
+ memories: any[];
78
68
  pagination: {
79
69
  currentPage: number;
80
70
  totalItems: number;