suemo 0.0.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/LICENSE +674 -0
- package/README.md +248 -0
- package/package.json +58 -0
- package/src/cli/commands/believe.ts +27 -0
- package/src/cli/commands/consolidate.ts +24 -0
- package/src/cli/commands/export-import.ts +91 -0
- package/src/cli/commands/goal.ts +71 -0
- package/src/cli/commands/health.ts +43 -0
- package/src/cli/commands/init.ts +90 -0
- package/src/cli/commands/observe.ts +40 -0
- package/src/cli/commands/query.ts +31 -0
- package/src/cli/commands/recall.ts +17 -0
- package/src/cli/commands/serve.ts +19 -0
- package/src/cli/commands/shared.ts +20 -0
- package/src/cli/commands/sync.ts +23 -0
- package/src/cli/commands/timeline.ts +37 -0
- package/src/cli/commands/wander.ts +34 -0
- package/src/cli/index.ts +41 -0
- package/src/cli/shared.ts +9 -0
- package/src/cognitive/consolidate.ts +349 -0
- package/src/cognitive/contradiction.ts +50 -0
- package/src/cognitive/health.ts +123 -0
- package/src/config.ts +114 -0
- package/src/db/client.ts +59 -0
- package/src/db/preflight.ts +152 -0
- package/src/db/schema.surql +109 -0
- package/src/db/schema.ts +24 -0
- package/src/env.d.ts +4 -0
- package/src/goal.ts +39 -0
- package/src/index.ts +13 -0
- package/src/logger.ts +60 -0
- package/src/mcp/server.ts +23 -0
- package/src/mcp/tools.ts +100 -0
- package/src/memory/episode.ts +66 -0
- package/src/memory/read.ts +223 -0
- package/src/memory/write.ts +134 -0
- package/src/sync.ts +120 -0
- package/src/types.ts +144 -0
package/src/sync.ts
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { createNodeEngines } from '@surrealdb/node'
|
|
2
|
+
import { createHash } from 'node:crypto'
|
|
3
|
+
import { createRemoteEngines, Surreal } from 'surrealdb'
|
|
4
|
+
import type { SurrealTarget } from './config.ts'
|
|
5
|
+
import { getLogger } from './logger.ts'
|
|
6
|
+
import type { SyncResult } from './types.ts'
|
|
7
|
+
|
|
8
|
+
const log = getLogger(['suemo', 'sync'])
|
|
9
|
+
|
|
10
|
+
const BATCH_SIZE = 500
|
|
11
|
+
|
|
12
|
+
function remoteKey(target: SurrealTarget): string {
|
|
13
|
+
return createHash('sha1')
|
|
14
|
+
.update(`${target.url}|${target.namespace}|${target.database}`)
|
|
15
|
+
.digest('hex')
|
|
16
|
+
.slice(0, 16)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function syncTo(
|
|
20
|
+
sourceDb: Surreal,
|
|
21
|
+
target: SurrealTarget,
|
|
22
|
+
opts: { dryRun?: boolean } = {},
|
|
23
|
+
): Promise<SyncResult> {
|
|
24
|
+
log.info('syncTo()', { target: `${target.url}/${target.namespace}/${target.database}`, dryRun: opts.dryRun })
|
|
25
|
+
|
|
26
|
+
// Load cursor (last successfully synced created_at)
|
|
27
|
+
const key = remoteKey(target)
|
|
28
|
+
const cursorResult = await sourceDb.query<[{ cursor: string }[]]>(
|
|
29
|
+
`
|
|
30
|
+
SELECT cursor FROM sync_cursor WHERE remote_key = $key LIMIT 1
|
|
31
|
+
`,
|
|
32
|
+
{ key },
|
|
33
|
+
)
|
|
34
|
+
const cursor = cursorResult[0]?.[0]?.cursor ?? '1970-01-01T00:00:00Z'
|
|
35
|
+
|
|
36
|
+
log.info('Sync cursor', { cursor })
|
|
37
|
+
|
|
38
|
+
// Connect to remote
|
|
39
|
+
const remoteDb = new Surreal({
|
|
40
|
+
engines: {
|
|
41
|
+
...createRemoteEngines(),
|
|
42
|
+
...createNodeEngines(),
|
|
43
|
+
},
|
|
44
|
+
})
|
|
45
|
+
await remoteDb.connect(target.url, {
|
|
46
|
+
namespace: target.namespace,
|
|
47
|
+
database: target.database,
|
|
48
|
+
authentication: () => ({
|
|
49
|
+
username: target.auth.user,
|
|
50
|
+
password: target.auth.pass,
|
|
51
|
+
}),
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
let pushed = 0
|
|
55
|
+
let skipped = 0
|
|
56
|
+
let errors = 0
|
|
57
|
+
let latestCreatedAt = cursor
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
let offset = 0
|
|
61
|
+
|
|
62
|
+
while (true) {
|
|
63
|
+
const batch = await sourceDb.query<[{ id: string; created_at: string; [k: string]: unknown }[]]>(
|
|
64
|
+
`
|
|
65
|
+
SELECT * FROM memory
|
|
66
|
+
WHERE created_at > <datetime>$cursor
|
|
67
|
+
ORDER BY created_at ASC
|
|
68
|
+
LIMIT $limit START $offset
|
|
69
|
+
`,
|
|
70
|
+
{ cursor, limit: BATCH_SIZE, offset },
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
const rows = batch[0] ?? []
|
|
74
|
+
if (rows.length === 0) break
|
|
75
|
+
|
|
76
|
+
for (const row of rows) {
|
|
77
|
+
if (opts.dryRun) {
|
|
78
|
+
log.debug('dry-run: would push', { id: row['id'] })
|
|
79
|
+
skipped++
|
|
80
|
+
} else {
|
|
81
|
+
try {
|
|
82
|
+
// INSERT IGNORE: if the ID already exists on remote, skip it
|
|
83
|
+
await remoteDb.query('INSERT IGNORE INTO memory $row', { row })
|
|
84
|
+
pushed++
|
|
85
|
+
} catch (e) {
|
|
86
|
+
log.error('Failed to push record', { id: row['id'], error: String(e) })
|
|
87
|
+
errors++
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (row['created_at'] && row['created_at'] > latestCreatedAt) {
|
|
91
|
+
latestCreatedAt = row['created_at']
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
offset += rows.length
|
|
96
|
+
if (rows.length < BATCH_SIZE) break
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Also sync relations
|
|
100
|
+
// (similar batching logic; omitted for brevity — same pattern with relates_to table)
|
|
101
|
+
|
|
102
|
+
if (!opts.dryRun && pushed > 0) {
|
|
103
|
+
// Advance cursor
|
|
104
|
+
await sourceDb.query(
|
|
105
|
+
`
|
|
106
|
+
UPSERT sync_cursor SET
|
|
107
|
+
remote_key = $key,
|
|
108
|
+
cursor = <datetime>$cursor,
|
|
109
|
+
last_synced = time::now()
|
|
110
|
+
`,
|
|
111
|
+
{ key, cursor: latestCreatedAt },
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
} finally {
|
|
115
|
+
await remoteDb.close()
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
log.info('syncTo() complete', { pushed, skipped, errors })
|
|
119
|
+
return { pushed, skipped, errors, cursor: latestCreatedAt }
|
|
120
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// Zod v4 (^4.3.6): import from "zod" root — package root now exports v4.
|
|
2
|
+
// Key v4 behavior relevant here: .default() always applies even for absent keys.
|
|
3
|
+
import { z } from 'zod'
|
|
4
|
+
|
|
5
|
+
// ── Memory kinds ─────────────────────────────────────────────────────────────
|
|
6
|
+
export const MemoryKindSchema = z.enum([
|
|
7
|
+
'observation',
|
|
8
|
+
'belief',
|
|
9
|
+
'question',
|
|
10
|
+
'hypothesis',
|
|
11
|
+
'goal',
|
|
12
|
+
])
|
|
13
|
+
export type MemoryKind = z.infer<typeof MemoryKindSchema>
|
|
14
|
+
|
|
15
|
+
// ── Relation kinds ───────────────────────────────────────────────────────────
|
|
16
|
+
export const RelationKindSchema = z.enum([
|
|
17
|
+
'supports',
|
|
18
|
+
'contradicts',
|
|
19
|
+
'derived_from',
|
|
20
|
+
'caused_by',
|
|
21
|
+
'similar_to',
|
|
22
|
+
'updates',
|
|
23
|
+
'sequential',
|
|
24
|
+
])
|
|
25
|
+
export type RelationKind = z.infer<typeof RelationKindSchema>
|
|
26
|
+
|
|
27
|
+
// ── Core memory node (matches DB schema exactly) ─────────────────────────────
|
|
28
|
+
export const MemoryNodeSchema = z.object({
|
|
29
|
+
id: z.string(),
|
|
30
|
+
kind: MemoryKindSchema,
|
|
31
|
+
content: z.string(),
|
|
32
|
+
summary: z.string().nullable(),
|
|
33
|
+
tags: z.array(z.string()),
|
|
34
|
+
scope: z.string().nullable(),
|
|
35
|
+
embedding: z.array(z.number()),
|
|
36
|
+
confidence: z.number().min(0).max(1),
|
|
37
|
+
salience: z.number().min(0).max(1),
|
|
38
|
+
valid_from: z.iso.datetime(),
|
|
39
|
+
valid_until: z.iso.datetime().nullable(),
|
|
40
|
+
source: z.string().nullable(),
|
|
41
|
+
created_at: z.iso.datetime(),
|
|
42
|
+
updated_at: z.iso.datetime(),
|
|
43
|
+
consolidated: z.boolean(),
|
|
44
|
+
consolidated_into: z.string().nullable(), // record ID string
|
|
45
|
+
fsrs_stability: z.number().nullable(),
|
|
46
|
+
fsrs_difficulty: z.number().nullable(),
|
|
47
|
+
fsrs_next_review: z.iso.datetime().nullable(),
|
|
48
|
+
})
|
|
49
|
+
export type MemoryNode = z.infer<typeof MemoryNodeSchema>
|
|
50
|
+
|
|
51
|
+
// ── Relation edge ─────────────────────────────────────────────────────────────
|
|
52
|
+
export const RelationSchema = z.object({
|
|
53
|
+
id: z.string(),
|
|
54
|
+
in: z.string(), // memory record ID
|
|
55
|
+
out: z.string(), // memory record ID
|
|
56
|
+
kind: RelationKindSchema,
|
|
57
|
+
strength: z.number().min(0).max(1),
|
|
58
|
+
valid_from: z.iso.datetime(),
|
|
59
|
+
valid_until: z.iso.datetime().nullable(),
|
|
60
|
+
created_at: z.iso.datetime(),
|
|
61
|
+
})
|
|
62
|
+
export type Relation = z.infer<typeof RelationSchema>
|
|
63
|
+
|
|
64
|
+
// ── Episode ───────────────────────────────────────────────────────────────────
|
|
65
|
+
export const EpisodeSchema = z.object({
|
|
66
|
+
id: z.string(),
|
|
67
|
+
session_id: z.string(),
|
|
68
|
+
started_at: z.iso.datetime(),
|
|
69
|
+
ended_at: z.iso.datetime().nullable(),
|
|
70
|
+
summary: z.string().nullable(),
|
|
71
|
+
memory_ids: z.array(z.string()),
|
|
72
|
+
})
|
|
73
|
+
export type Episode = z.infer<typeof EpisodeSchema>
|
|
74
|
+
|
|
75
|
+
// ── Consolidation run log ────────────────────────────────────────────────────
|
|
76
|
+
export const ConsolidationRunSchema = z.object({
|
|
77
|
+
id: z.string(),
|
|
78
|
+
started_at: z.iso.datetime(),
|
|
79
|
+
completed_at: z.iso.datetime().nullable(),
|
|
80
|
+
phase: z.enum(['nrem', 'rem', 'full']),
|
|
81
|
+
nodes_in: z.number().int(),
|
|
82
|
+
nodes_out: z.number().int(),
|
|
83
|
+
status: z.enum(['running', 'done', 'failed']),
|
|
84
|
+
error: z.string().nullable(),
|
|
85
|
+
})
|
|
86
|
+
export type ConsolidationRun = z.infer<typeof ConsolidationRunSchema>
|
|
87
|
+
|
|
88
|
+
// ── observe() input ───────────────────────────────────────────────────────────
|
|
89
|
+
// NOTE: no valid_from, valid_until — system-managed only, never in inputs
|
|
90
|
+
// Zod v4: .default() on a field applies the default even when the key is absent
|
|
91
|
+
// from input — this is the intended behavior for all optional fields here.
|
|
92
|
+
export const ObserveInputSchema = z.object({
|
|
93
|
+
content: z.string().min(1),
|
|
94
|
+
kind: MemoryKindSchema.default('observation').optional(),
|
|
95
|
+
tags: z.array(z.string()).default([]).optional(),
|
|
96
|
+
scope: z.string().optional(),
|
|
97
|
+
source: z.string().optional(),
|
|
98
|
+
confidence: z.number().min(0).max(1).default(1.0).optional(),
|
|
99
|
+
})
|
|
100
|
+
export type ObserveInput = z.infer<typeof ObserveInputSchema>
|
|
101
|
+
|
|
102
|
+
// ── query() input ─────────────────────────────────────────────────────────────
|
|
103
|
+
export const QueryInputSchema = z.object({
|
|
104
|
+
input: z.string().min(1),
|
|
105
|
+
scope: z.string().optional(),
|
|
106
|
+
kind: z.array(MemoryKindSchema).optional(),
|
|
107
|
+
topK: z.number().int().min(1).max(50).default(5).optional(),
|
|
108
|
+
activeOnly: z.boolean().default(true).optional(),
|
|
109
|
+
strategies: z
|
|
110
|
+
.array(z.enum(['vector', 'bm25', 'graph', 'temporal']))
|
|
111
|
+
.default(['vector', 'bm25', 'graph'])
|
|
112
|
+
.optional(),
|
|
113
|
+
})
|
|
114
|
+
export type QueryInput = z.infer<typeof QueryInputSchema>
|
|
115
|
+
|
|
116
|
+
// ── Health report ─────────────────────────────────────────────────────────────
|
|
117
|
+
export const HealthReportSchema = z.object({
|
|
118
|
+
nodes: z.object({
|
|
119
|
+
total: z.number(),
|
|
120
|
+
active: z.number(),
|
|
121
|
+
consolidated: z.number(),
|
|
122
|
+
by_kind: z.record(z.string(), z.number()),
|
|
123
|
+
by_scope: z.record(z.string(), z.number()),
|
|
124
|
+
}),
|
|
125
|
+
relations: z.number(),
|
|
126
|
+
goals_active: z.number(),
|
|
127
|
+
fsrs_due: z.number(),
|
|
128
|
+
last_consolidation: ConsolidationRunSchema.nullable(),
|
|
129
|
+
version_check: z.object({
|
|
130
|
+
surreal_version: z.string(),
|
|
131
|
+
surrealkv: z.boolean(),
|
|
132
|
+
retention_ok: z.boolean(), // SURREAL_DATASTORE_RETENTION >= 90d on the server
|
|
133
|
+
}),
|
|
134
|
+
})
|
|
135
|
+
export type HealthReport = z.infer<typeof HealthReportSchema>
|
|
136
|
+
|
|
137
|
+
// ── Sync result ───────────────────────────────────────────────────────────────
|
|
138
|
+
export const SyncResultSchema = z.object({
|
|
139
|
+
pushed: z.number(),
|
|
140
|
+
skipped: z.number(),
|
|
141
|
+
errors: z.number(),
|
|
142
|
+
cursor: z.iso.datetime(),
|
|
143
|
+
})
|
|
144
|
+
export type SyncResult = z.infer<typeof SyncResultSchema>
|