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.
- package/CHANGELOG.md +43 -0
- package/README.md +30 -12
- package/dist/cli.d.ts +5 -3
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +245 -47
- package/dist/cli.js.map +1 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +10 -0
- package/dist/client.js.map +1 -1
- package/dist/config.d.ts +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +4 -0
- package/dist/config.js.map +1 -1
- package/dist/configure.d.ts.map +1 -1
- package/dist/configure.js +5 -4
- package/dist/configure.js.map +1 -1
- package/dist/discover.d.ts +9 -11
- package/dist/discover.d.ts.map +1 -1
- package/dist/discover.js +14 -15
- package/dist/discover.js.map +1 -1
- package/dist/import.d.ts +4 -4
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +46 -0
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp-server.js +26 -3
- package/dist/mcp-server.js.map +1 -1
- package/dist/ports/projections.d.ts +8 -0
- package/dist/ports/projections.d.ts.map +1 -1
- package/dist/project.d.ts +51 -0
- package/dist/project.d.ts.map +1 -0
- package/dist/project.js +143 -0
- package/dist/project.js.map +1 -0
- package/dist/projections.d.ts +14 -0
- package/dist/projections.d.ts.map +1 -1
- package/dist/projections.js +36 -1
- package/dist/projections.js.map +1 -1
- package/dist/remote.d.ts.map +1 -1
- package/dist/remote.js +9 -0
- package/dist/remote.js.map +1 -1
- package/dist/schemas.d.ts +18 -4
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +29 -2
- package/dist/schemas.js.map +1 -1
- package/dist/service.d.ts +1 -0
- package/dist/service.d.ts.map +1 -1
- package/dist/storage.d.ts +13 -0
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +24 -0
- package/dist/storage.js.map +1 -1
- package/dist/types.d.ts +1 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/workspaces.d.ts +41 -0
- package/dist/workspaces.d.ts.map +1 -0
- package/dist/workspaces.js +96 -0
- package/dist/workspaces.js.map +1 -0
- package/package.json +1 -1
- package/src/cli.ts +305 -50
- package/src/client.ts +10 -0
- package/src/config.ts +4 -0
- package/src/configure.ts +5 -4
- package/src/discover.ts +18 -19
- package/src/index.ts +22 -1
- package/src/mcp/index.ts +62 -0
- package/src/mcp-server.ts +32 -5
- package/src/ports/projections.ts +9 -0
- package/src/project.ts +168 -0
- package/src/projections.ts +38 -1
- package/src/remote.ts +9 -0
- package/src/schemas.ts +44 -12
- package/src/service.ts +1 -0
- package/src/storage.ts +28 -0
- package/src/types.ts +1 -0
- 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
|
+
}
|