synomem 0.5.3 → 0.6.1

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 (77) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/README.md +30 -12
  3. package/dist/cli.d.ts +5 -3
  4. package/dist/cli.d.ts.map +1 -1
  5. package/dist/cli.js +245 -47
  6. package/dist/cli.js.map +1 -1
  7. package/dist/client.d.ts.map +1 -1
  8. package/dist/client.js +10 -0
  9. package/dist/client.js.map +1 -1
  10. package/dist/config.d.ts +1 -0
  11. package/dist/config.d.ts.map +1 -1
  12. package/dist/config.js +4 -0
  13. package/dist/config.js.map +1 -1
  14. package/dist/configure.d.ts.map +1 -1
  15. package/dist/configure.js +5 -4
  16. package/dist/configure.js.map +1 -1
  17. package/dist/discover.d.ts +9 -11
  18. package/dist/discover.d.ts.map +1 -1
  19. package/dist/discover.js +14 -15
  20. package/dist/discover.js.map +1 -1
  21. package/dist/import.d.ts +4 -4
  22. package/dist/index.d.ts +5 -1
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +3 -1
  25. package/dist/index.js.map +1 -1
  26. package/dist/mcp/index.d.ts.map +1 -1
  27. package/dist/mcp/index.js +46 -0
  28. package/dist/mcp/index.js.map +1 -1
  29. package/dist/mcp-server.js +26 -3
  30. package/dist/mcp-server.js.map +1 -1
  31. package/dist/ports/projections.d.ts +8 -0
  32. package/dist/ports/projections.d.ts.map +1 -1
  33. package/dist/project.d.ts +51 -0
  34. package/dist/project.d.ts.map +1 -0
  35. package/dist/project.js +143 -0
  36. package/dist/project.js.map +1 -0
  37. package/dist/projections.d.ts +14 -0
  38. package/dist/projections.d.ts.map +1 -1
  39. package/dist/projections.js +36 -1
  40. package/dist/projections.js.map +1 -1
  41. package/dist/remote.d.ts.map +1 -1
  42. package/dist/remote.js +9 -0
  43. package/dist/remote.js.map +1 -1
  44. package/dist/schemas.d.ts +18 -4
  45. package/dist/schemas.d.ts.map +1 -1
  46. package/dist/schemas.js +29 -2
  47. package/dist/schemas.js.map +1 -1
  48. package/dist/service.d.ts +1 -0
  49. package/dist/service.d.ts.map +1 -1
  50. package/dist/storage.d.ts +13 -0
  51. package/dist/storage.d.ts.map +1 -1
  52. package/dist/storage.js +24 -0
  53. package/dist/storage.js.map +1 -1
  54. package/dist/types.d.ts +1 -0
  55. package/dist/types.d.ts.map +1 -1
  56. package/dist/workspaces.d.ts +41 -0
  57. package/dist/workspaces.d.ts.map +1 -0
  58. package/dist/workspaces.js +96 -0
  59. package/dist/workspaces.js.map +1 -0
  60. package/package.json +1 -1
  61. package/src/cli.ts +305 -50
  62. package/src/client.ts +10 -0
  63. package/src/config.ts +4 -0
  64. package/src/configure.ts +5 -4
  65. package/src/discover.ts +18 -19
  66. package/src/index.ts +22 -1
  67. package/src/mcp/index.ts +62 -0
  68. package/src/mcp-server.ts +32 -5
  69. package/src/ports/projections.ts +9 -0
  70. package/src/project.ts +168 -0
  71. package/src/projections.ts +38 -1
  72. package/src/remote.ts +9 -0
  73. package/src/schemas.ts +44 -12
  74. package/src/service.ts +1 -0
  75. package/src/storage.ts +28 -0
  76. package/src/types.ts +1 -0
  77. package/src/workspaces.ts +107 -0
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Local workspaces.
3
+ *
4
+ * On Synomem Cloud a workspace is a row, and isolation is enforced by the
5
+ * database: every record table carries `workspace_id`, has row-level security
6
+ * enabled, and every query runs with `synomem.workspace_id` set — so a query
7
+ * that forgets to filter returns nothing rather than another workspace's rows.
8
+ *
9
+ * SQLite has no row-level security, so the same schema locally would not buy
10
+ * the same guarantee: isolation would rest on every one of eighty query sites
11
+ * staying correct, with nothing underneath to catch a miss, and a miss would
12
+ * silently mix workspaces rather than fail. So a local workspace is a separate
13
+ * DATABASE — its own home, its own file. The filesystem does the isolating, and
14
+ * cross-workspace leakage stops being a thing anybody can write by accident.
15
+ *
16
+ * That is also why nothing downstream needs to know. A local workspace resolves
17
+ * to a home and a hosted one resolves to an id, both at the single seam in
18
+ * `backend.ts` that already chooses between the two backends. No domain code,
19
+ * CLI command or MCP tool can tell which it got.
20
+ */
21
+ import { existsSync, lstatSync, readdirSync } from 'node:fs';
22
+ import { join } from 'node:path';
23
+ import { z } from 'zod';
24
+ import { SynomemError } from './errors.js';
25
+ import { resolveHome } from './config.js';
26
+
27
+ /**
28
+ * The directory named workspaces live under, relative to the root.
29
+ *
30
+ * `workspaces` is a reserved agent handle for exactly this reason: projected
31
+ * agent directories sit at `<home>/<handle>/`, so an agent allowed to call
32
+ * itself `workspaces` would collide with this.
33
+ */
34
+ export const WORKSPACES_DIRECTORY = 'workspaces';
35
+
36
+ /**
37
+ * The workspace whose home is the root itself.
38
+ *
39
+ * The root stays a complete Synomem home rather than becoming a container, so
40
+ * an existing install keeps its database exactly where it is and needs no
41
+ * migration. Named workspaces are added alongside it.
42
+ */
43
+ export const DEFAULT_WORKSPACE = 'default';
44
+
45
+ /** Same shape as an agent handle: a name somebody types, and a directory name. */
46
+ export const workspaceNameSchema = z
47
+ .string()
48
+ .trim()
49
+ .min(1)
50
+ .max(63)
51
+ .regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, 'Use lowercase ASCII letters, digits, and hyphens')
52
+ .refine((name) => name !== WORKSPACES_DIRECTORY, 'Reserved workspace name');
53
+
54
+ export interface LocalWorkspace {
55
+ name: string;
56
+ home: string;
57
+ /** False until `init` or the first write has created the store. */
58
+ initialized: boolean;
59
+ }
60
+
61
+ /**
62
+ * Where a named local workspace lives.
63
+ *
64
+ * The name is validated rather than trusted: it becomes a directory, so a value
65
+ * containing a separator or `..` would escape the root.
66
+ */
67
+ export function localWorkspaceHome(name: string, explicitRoot?: string): string {
68
+ const root = resolveHome(explicitRoot);
69
+ if (name === DEFAULT_WORKSPACE) return root;
70
+ const parsed = workspaceNameSchema.safeParse(name);
71
+ if (!parsed.success) {
72
+ throw new SynomemError(
73
+ 'INVALID_INPUT',
74
+ `Invalid workspace name: ${name}. ${parsed.error.issues[0]?.message ?? ''}`.trim(),
75
+ );
76
+ }
77
+ return join(root, WORKSPACES_DIRECTORY, parsed.data);
78
+ }
79
+
80
+ /**
81
+ * The local workspaces on this machine, discovered from disk.
82
+ *
83
+ * Read from the filesystem rather than a registry file, so a workspace cannot
84
+ * be listed and then turn out not to exist — and a directory copied in by hand
85
+ * is found without having to be registered.
86
+ */
87
+ export function listLocalWorkspaces(explicitRoot?: string): LocalWorkspace[] {
88
+ const root = resolveHome(explicitRoot);
89
+ const found: LocalWorkspace[] = [
90
+ { name: DEFAULT_WORKSPACE, home: root, initialized: existsSync(join(root, 'config.json')) },
91
+ ];
92
+
93
+ const container = join(root, WORKSPACES_DIRECTORY);
94
+ if (!existsSync(container)) return found;
95
+ for (const entry of readdirSync(container, { withFileTypes: true })) {
96
+ // A symbolic link here would point the store outside the root.
97
+ if (!entry.isDirectory() || lstatSync(join(container, entry.name)).isSymbolicLink()) continue;
98
+ if (!workspaceNameSchema.safeParse(entry.name).success) continue;
99
+ const home = join(container, entry.name);
100
+ found.push({
101
+ name: entry.name,
102
+ home,
103
+ initialized: existsSync(join(home, 'config.json')),
104
+ });
105
+ }
106
+ return found.sort((left, right) => left.name.localeCompare(right.name));
107
+ }