suemo 0.0.6 → 0.0.8
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/README.md +73 -9
- package/package.json +5 -5
- package/src/cli/commands/init.ts +671 -3
- package/tsconfig.json +29 -0
package/README.md
CHANGED
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
> [!CAUTION]
|
|
6
6
|
> **Bun-only.** This project will not run on Node.js and there are no plans to support it.
|
|
7
7
|
>
|
|
8
|
-
> suemo is experimental software built for the author's personal agent infrastructure. APIs may change without notice.
|
|
8
|
+
> suemo is experimental software built for the author's personal agent infrastructure. APIs may change without notice.
|
|
9
|
+
>
|
|
10
|
+
> Most of the code is AI-generated and only tested briefly by a single person. Use at your own risk.
|
|
9
11
|
|
|
10
12
|
suemo gives AI agents a memory that survives across sessions, models, and runtimes. Write observations from a Telegram bot, query them from OpenCode, consolidate overnight — all agents share one source of truth in SurrealDB.
|
|
11
13
|
|
|
@@ -17,7 +19,7 @@ suemo gives AI agents a memory that survives across sessions, models, and runtim
|
|
|
17
19
|
- **Bi-temporal nodes** — every fact has `valid_from` / `valid_until`; nothing is hard-deleted
|
|
18
20
|
- **Contradiction detection** — calling `believe()` with a conflicting belief automatically invalidates the old one and links them with a `contradicts` edge
|
|
19
21
|
- **Two-phase consolidation** — NREM clusters and compresses redundant observations via LLM; REM integrates new summaries into the broader graph with auto-scored relations
|
|
20
|
-
- **SurrealKV time-travel** — `VERSION d'...'` queries let you inspect any node's state at any past datetime (requires `SURREAL_DATASTORE_RETENTION=90d`)
|
|
22
|
+
- **SurrealKV time-travel** — `VERSION d'...'` queries let you inspect any node's state at any past datetime (requires `SURREAL_DATASTORE_RETENTION=90d` / `0` for infinite)
|
|
21
23
|
- **Namespace isolation** — one SurrealDB instance, multiple agents, zero collision (`namespace` = agent group, `database` = agent identity)
|
|
22
24
|
- **MCP + CLI** — both interfaces are thin shells over the same domain functions; no business logic duplication
|
|
23
25
|
- **Lightweight** — no bundled vector store, no embedded database process, no OpenAI SDK; embedding via `fn::embed()` runs server-side in SurrealDB
|
|
@@ -36,13 +38,23 @@ SurrealDB must be started with SurrealKV and a retention window:
|
|
|
36
38
|
```sh
|
|
37
39
|
SURREAL_DATASTORE_VERSIONED=true SURREAL_DATASTORE_RETENTION=90d surreal start \
|
|
38
40
|
--bind 0.0.0.0:8000 \
|
|
39
|
-
--allow-funcs "
|
|
41
|
+
--allow-funcs "*" \
|
|
40
42
|
-- surrealkv:///path/to/data
|
|
41
43
|
```
|
|
42
44
|
|
|
43
|
-
For suemo, the critical capability is allowing custom functions
|
|
45
|
+
For suemo, the critical capability is allowing custom functions:
|
|
46
|
+
|
|
47
|
+
- `fn::*`
|
|
48
|
+
- `time::*`
|
|
49
|
+
- `vector::*`
|
|
50
|
+
- `search::*` (for `search::score`)
|
|
51
|
+
- `math::*` (for `math::mean`, `math::min`)
|
|
52
|
+
- `rand::*` (used in `wander` query)
|
|
53
|
+
|
|
54
|
+
If you run with strict capability mode, use an allowlist like:
|
|
44
55
|
|
|
45
|
-
|
|
56
|
+
- for **openai-compatible/stub**: `fn,time,vector,search,math,rand`
|
|
57
|
+
- for **surreal** provider (with `ml::...` in `fn::embed` wrapper): add `ml`
|
|
46
58
|
|
|
47
59
|
---
|
|
48
60
|
|
|
@@ -80,6 +92,56 @@ export default defineConfig({
|
|
|
80
92
|
|
|
81
93
|
With this, suemo resolves default scope from nearest `.ua/suemo.json` (auto-created if missing).
|
|
82
94
|
|
|
95
|
+
### Optional: one-command local service setup (Arch Linux)
|
|
96
|
+
|
|
97
|
+
**NOTE:** These init commands are Arch Linux–specific (author's primary development environment). They rely on `pacman` package checks and systemd paths matching Arch Linux layouts.
|
|
98
|
+
|
|
99
|
+
For local host deployments, suemo can install systemd service assets directly.
|
|
100
|
+
|
|
101
|
+
These commands **must be run as root**:
|
|
102
|
+
|
|
103
|
+
```sh
|
|
104
|
+
sudo suemo init surreal 2gb --dry-run
|
|
105
|
+
sudo suemo init surreal 6gb --dry-run
|
|
106
|
+
sudo suemo init fastembed --dry-run
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
If `suemo` is not on root PATH, use `sudo bunx suemo ...`.
|
|
110
|
+
|
|
111
|
+
Drop `--dry-run` to apply changes.
|
|
112
|
+
|
|
113
|
+
`suemo init surreal` also creates `/opt/suemo/surrealdb/local.env` with commented overrides and does not overwrite it if it already exists. If defined in `local.env`, these values override `common.env`:
|
|
114
|
+
|
|
115
|
+
- `SURREAL_USER`
|
|
116
|
+
- `SURREAL_PASS`
|
|
117
|
+
- `SURREAL_BIND`
|
|
118
|
+
- `SURREAL_PATH`
|
|
119
|
+
|
|
120
|
+
Add `--force` to regenerate managed env files (`common.env` and profile env).
|
|
121
|
+
|
|
122
|
+
What these commands do:
|
|
123
|
+
|
|
124
|
+
- `suemo init surreal <2gb|6gb>`
|
|
125
|
+
- checks `surrealdb` package via `pacman -Q`
|
|
126
|
+
- creates `/opt/suemo/surrealdb/common.env` + profile env (`2gb.env` or `6gb.env`)
|
|
127
|
+
- creates `/opt/suemo/surrealdb/local.env` once (user override file; not overwritten)
|
|
128
|
+
- writes `/etc/systemd/system/suemo-surrealdb@.service`
|
|
129
|
+
- enables + starts `suemo-surrealdb@<profile>.service`
|
|
130
|
+
- includes `VERSIONED` + retention config and strict capability allowlist:
|
|
131
|
+
- `SURREAL_DATASTORE_VERSIONED=true`
|
|
132
|
+
- `SURREAL_DATASTORE_RETENTION=90d`
|
|
133
|
+
- `SURREAL_CAPS_DENY_ALL=true`
|
|
134
|
+
- `SURREAL_CAPS_ALLOW_FUNC=fn,time,vector,search,math,rand,ml`
|
|
135
|
+
|
|
136
|
+
- `suemo init fastembed`
|
|
137
|
+
- checks `python-fastembed`, `python-fastapi`, `python-uvicorn` via `pacman -Q`
|
|
138
|
+
- installs `data/fastembed-server.py` to `/opt/suemo/fastembed-server.py`
|
|
139
|
+
- creates `/opt/fastembed/local.env` once (user override file; not overwritten)
|
|
140
|
+
- writes `/etc/systemd/system/suemo-fastembed.service`
|
|
141
|
+
- enables + starts `suemo-fastembed.service`
|
|
142
|
+
|
|
143
|
+
`--dry-run` prints all generated file content and planned commands to stdout without writing anything.
|
|
144
|
+
|
|
83
145
|
**2. Apply schema**
|
|
84
146
|
|
|
85
147
|
Apply schema after you set/edit namespace/database:
|
|
@@ -155,10 +217,12 @@ Global flags (inherited by all commands):
|
|
|
155
217
|
-c, --config <path> Path to config file
|
|
156
218
|
-d, --debug Verbose debug logging
|
|
157
219
|
|
|
158
|
-
Commands:
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
220
|
+
Commands:
|
|
221
|
+
init Show init subcommands and usage guidance
|
|
222
|
+
init config Create/update ~/.suemo/suemo.ts
|
|
223
|
+
init schema Apply DB schema from current config (with confirm)
|
|
224
|
+
init surreal Install systemd SurrealDB profile (2gb/6gb) with VERSIONED + allowlist config
|
|
225
|
+
init fastembed Install systemd fastembed service
|
|
162
226
|
skill Print suemo skill docs (or specific reference)
|
|
163
227
|
serve Start the MCP server (HTTP or stdio)
|
|
164
228
|
observe <content> Store an observation
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "suemo",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.8",
|
|
4
4
|
"description": "Persistent semantic memory for AI agents — backed by SurrealDB.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Umar Alfarouk",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"bin": {
|
|
28
28
|
"suemo": "./src/cli/index.ts"
|
|
29
29
|
},
|
|
30
|
-
"files": ["src", "LICENSE", "README.md"],
|
|
30
|
+
"files": ["src", "LICENSE", "README.md", "tsconfig.json"],
|
|
31
31
|
"exports": {
|
|
32
32
|
".": {
|
|
33
33
|
"bun": "./src/index.ts",
|
|
@@ -44,9 +44,9 @@
|
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
46
|
"@crustjs/core": "^0.0.15",
|
|
47
|
-
"@crustjs/plugins": "^0.0.
|
|
48
|
-
"@crustjs/prompts": "^0.0.
|
|
49
|
-
"@crustjs/style": "^0.0.
|
|
47
|
+
"@crustjs/plugins": "^0.0.20",
|
|
48
|
+
"@crustjs/prompts": "^0.0.10",
|
|
49
|
+
"@crustjs/style": "^0.0.6",
|
|
50
50
|
"@logtape/file": "^2.0.4",
|
|
51
51
|
"@logtape/logtape": "^2.0.4",
|
|
52
52
|
"@surrealdb/node": "^3.0.3",
|
package/src/cli/commands/init.ts
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import { confirm } from '@crustjs/prompts'
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { spawnSync } from 'node:child_process'
|
|
3
|
+
import { randomBytes } from 'node:crypto'
|
|
4
|
+
import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
5
|
+
import { tmpdir } from 'node:os'
|
|
6
|
+
import { basename, dirname, join, resolve as resolvePath } from 'node:path'
|
|
7
|
+
import { fileURLToPath } from 'node:url'
|
|
4
8
|
import packageJson from '../../../package.json' with { type: 'json' }
|
|
5
9
|
import { loadConfig } from '../../config.ts'
|
|
6
10
|
import { connect, disconnect } from '../../db/client.ts'
|
|
7
11
|
import { checkCompatibility } from '../../db/preflight.ts'
|
|
8
12
|
import { runSchema } from '../../db/schema.ts'
|
|
9
13
|
import { getLogger } from '../../logger.ts'
|
|
10
|
-
import { app, initCliCommand, resolveOutputModeOrExit } from '../shared.ts'
|
|
14
|
+
import { app, initCliCommand, printCliJson, resolveOutputModeOrExit } from '../shared.ts'
|
|
11
15
|
|
|
12
16
|
import template from '../../config.template.ts' with { type: 'text' }
|
|
13
17
|
|
|
@@ -25,6 +29,541 @@ const log = getLogger(['suemo', 'cli', 'init'])
|
|
|
25
29
|
const init = app.sub('init')
|
|
26
30
|
.meta({ description: 'Initialize suemo config and/or database schema' })
|
|
27
31
|
|
|
32
|
+
const SURREAL_PROFILES_DIR = '/opt/suemo/surrealdb'
|
|
33
|
+
const SURREAL_LOCAL_ENV_PATH = '/opt/suemo/surrealdb/local.env'
|
|
34
|
+
const SURREAL_SYSTEMD_UNIT_PATH = '/etc/systemd/system/suemo-surrealdb@.service'
|
|
35
|
+
const SURREAL_DATA_DIR = '/var/lib/surrealdb'
|
|
36
|
+
const FASTEMBED_INSTALL_DIR = '/opt/suemo'
|
|
37
|
+
const FASTEMBED_LOCAL_ENV_DIR = '/opt/fastembed'
|
|
38
|
+
const FASTEMBED_SCRIPT_TARGET = '/opt/suemo/fastembed-server.py'
|
|
39
|
+
const FASTEMBED_CACHE_DIR = '/var/cache/fastembed'
|
|
40
|
+
const FASTEMBED_SERVICE_PATH = '/etc/systemd/system/suemo-fastembed.service'
|
|
41
|
+
const FASTEMBED_USER_HOME = '/var/lib/fastembed'
|
|
42
|
+
|
|
43
|
+
const SURREAL_TEMPLATE_UNIT = `# Generated by suemo init surreal
|
|
44
|
+
[Unit]
|
|
45
|
+
Description=SurrealDB (%i profile)
|
|
46
|
+
Documentation=https://surrealdb.com/docs/surrealdb
|
|
47
|
+
After=network.target
|
|
48
|
+
Wants=network-online.target
|
|
49
|
+
|
|
50
|
+
[Service]
|
|
51
|
+
Type=simple
|
|
52
|
+
AmbientCapabilities=CAP_NET_BIND_SERVICE
|
|
53
|
+
User=surrealdb
|
|
54
|
+
Group=surrealdb
|
|
55
|
+
EnvironmentFile=/opt/suemo/surrealdb/common.env
|
|
56
|
+
EnvironmentFile=/opt/suemo/surrealdb/%i.env
|
|
57
|
+
EnvironmentFile=-/opt/suemo/surrealdb/local.env
|
|
58
|
+
ExecStart=/usr/bin/surreal start
|
|
59
|
+
ExecStop=/bin/kill -s SIGTERM $MAINPID
|
|
60
|
+
Restart=on-failure
|
|
61
|
+
RestartSec=5s
|
|
62
|
+
TimeoutStartSec=30s
|
|
63
|
+
TimeoutStopSec=30s
|
|
64
|
+
LimitNOFILE=65536
|
|
65
|
+
LimitNPROC=4096
|
|
66
|
+
NoNewPrivileges=true
|
|
67
|
+
PrivateTmp=true
|
|
68
|
+
ProtectSystem=strict
|
|
69
|
+
ProtectHome=true
|
|
70
|
+
ProtectKernelTunables=true
|
|
71
|
+
ProtectKernelModules=true
|
|
72
|
+
ProtectControlGroups=true
|
|
73
|
+
RestrictRealtime=true
|
|
74
|
+
RestrictNamespaces=true
|
|
75
|
+
StateDirectory=surrealdb
|
|
76
|
+
StateDirectoryMode=0750
|
|
77
|
+
WorkingDirectory=/var/lib/surrealdb
|
|
78
|
+
StandardOutput=journal
|
|
79
|
+
StandardError=journal
|
|
80
|
+
SyslogIdentifier=suemo-surrealdb-%i
|
|
81
|
+
|
|
82
|
+
[Install]
|
|
83
|
+
WantedBy=multi-user.target
|
|
84
|
+
`
|
|
85
|
+
|
|
86
|
+
const SURREAL_COMMON_ENV_TEMPLATE = `# Generated by suemo init surreal
|
|
87
|
+
# DO NOT edit - managed by suemo
|
|
88
|
+
|
|
89
|
+
# Root auth (rotate regularly via local.env)
|
|
90
|
+
SURREAL_USER=root
|
|
91
|
+
SURREAL_PASS=__SURREAL_PASS__
|
|
92
|
+
|
|
93
|
+
# Network
|
|
94
|
+
SURREAL_BIND=127.0.0.1:8000
|
|
95
|
+
|
|
96
|
+
# Datastore
|
|
97
|
+
SURREAL_PATH=surrealkv:///var/lib/surrealdb/db
|
|
98
|
+
SURREAL_DATASTORE_VERSIONED=true
|
|
99
|
+
SURREAL_DATASTORE_RETENTION=90d
|
|
100
|
+
SURREAL_DATASTORE_SYNC_DATA=every
|
|
101
|
+
|
|
102
|
+
# SurrealKV storage tuning
|
|
103
|
+
SURREAL_SURREALKV_ENABLE_VLOG=true
|
|
104
|
+
SURREAL_SURREALKV_VLOG_THRESHOLD=4096
|
|
105
|
+
SURREAL_SURREALKV_VERSIONED_INDEX=false
|
|
106
|
+
|
|
107
|
+
# Grouped commit batching
|
|
108
|
+
SURREAL_SURREALKV_GROUPED_COMMIT_TIMEOUT=5000000
|
|
109
|
+
SURREAL_SURREALKV_GROUPED_COMMIT_WAIT_THRESHOLD=12
|
|
110
|
+
SURREAL_SURREALKV_GROUPED_COMMIT_MAX_BATCH_SIZE=4096
|
|
111
|
+
|
|
112
|
+
# Changefeed GC
|
|
113
|
+
SURREAL_CHANGEFEED_GC_INTERVAL=60s
|
|
114
|
+
|
|
115
|
+
# Timeouts
|
|
116
|
+
SURREAL_QUERY_TIMEOUT=30s
|
|
117
|
+
SURREAL_TRANSACTION_TIMEOUT=15s
|
|
118
|
+
|
|
119
|
+
# Capability allowlist (required for suemo workflows)
|
|
120
|
+
SURREAL_CAPS_DENY_ALL=true
|
|
121
|
+
SURREAL_CAPS_ALLOW_FUNC=fn,time,vector,search,math,rand,ml
|
|
122
|
+
|
|
123
|
+
# Logging
|
|
124
|
+
SURREAL_LOG=warn
|
|
125
|
+
SURREAL_NO_BANNER=true
|
|
126
|
+
`
|
|
127
|
+
|
|
128
|
+
const SURREAL_LOCAL_ENV_TEMPLATE = `# User overrides - edit this file to customize credentials
|
|
129
|
+
# This file is NOT overwritten by suemo init surreal
|
|
130
|
+
# Uncomment and set values to override defaults from common.env
|
|
131
|
+
|
|
132
|
+
#SURREAL_USER=root
|
|
133
|
+
#SURREAL_PASS=your-password-here
|
|
134
|
+
#SURREAL_BIND=127.0.0.1:8000
|
|
135
|
+
#SURREAL_PATH=surrealkv:///var/lib/surrealdb/db
|
|
136
|
+
`
|
|
137
|
+
|
|
138
|
+
const SURREAL_PROFILE_2GB_ENV = `# Generated by suemo init surreal 2gb
|
|
139
|
+
SURREAL_SURREALKV_BLOCK_CACHE_CAPACITY=67108864
|
|
140
|
+
SURREAL_SURREALKV_VLOG_MAX_FILE_SIZE=67108864
|
|
141
|
+
SURREAL_TRANSACTION_CACHE_SIZE=2000
|
|
142
|
+
SURREAL_DATASTORE_CACHE_SIZE=500
|
|
143
|
+
SURREAL_HNSW_CACHE_SIZE=16777216
|
|
144
|
+
SURREAL_RUNTIME_WORKER_THREADS=2
|
|
145
|
+
SURREAL_RUNTIME_MAX_BLOCKING_THREADS=64
|
|
146
|
+
SURREAL_NET_MAX_CONCURRENT_REQUESTS=1024
|
|
147
|
+
SURREAL_WEBSOCKET_READ_BUFFER_SIZE=65536
|
|
148
|
+
SURREAL_WEBSOCKET_WRITE_BUFFER_SIZE=65536
|
|
149
|
+
SURREAL_EXTERNAL_SORTING_BUFFER_LIMIT=10000
|
|
150
|
+
SURREAL_MEMORY_THRESHOLD=256m
|
|
151
|
+
`
|
|
152
|
+
|
|
153
|
+
const SURREAL_PROFILE_6GB_ENV = `# Generated by suemo init surreal 6gb
|
|
154
|
+
SURREAL_SURREALKV_BLOCK_CACHE_CAPACITY=1073741824
|
|
155
|
+
SURREAL_SURREALKV_VLOG_MAX_FILE_SIZE=134217728
|
|
156
|
+
SURREAL_TRANSACTION_CACHE_SIZE=5000
|
|
157
|
+
SURREAL_DATASTORE_CACHE_SIZE=1000
|
|
158
|
+
SURREAL_HNSW_CACHE_SIZE=67108864
|
|
159
|
+
SURREAL_RUNTIME_WORKER_THREADS=4
|
|
160
|
+
SURREAL_RUNTIME_MAX_BLOCKING_THREADS=128
|
|
161
|
+
SURREAL_NET_MAX_CONCURRENT_REQUESTS=4096
|
|
162
|
+
SURREAL_WEBSOCKET_READ_BUFFER_SIZE=131072
|
|
163
|
+
SURREAL_WEBSOCKET_WRITE_BUFFER_SIZE=131072
|
|
164
|
+
SURREAL_EXTERNAL_SORTING_BUFFER_LIMIT=25000
|
|
165
|
+
SURREAL_MEMORY_THRESHOLD=512m
|
|
166
|
+
`
|
|
167
|
+
|
|
168
|
+
const FASTEMBED_LOCAL_ENV_PATH = '/opt/fastembed/local.env'
|
|
169
|
+
|
|
170
|
+
const FASTEMBED_LOCAL_ENV_TEMPLATE = `# User overrides - edit this file to customize settings
|
|
171
|
+
# This file is NOT overwritten by suemo init fastembed
|
|
172
|
+
# Uncomment and set values to override defaults
|
|
173
|
+
|
|
174
|
+
#FASTEMBED_HOST=127.0.0.1
|
|
175
|
+
#FASTEMBED_PORT=8080
|
|
176
|
+
#FASTEMBED_MODEL=sentence-transformers/all-MiniLM-L6-v2
|
|
177
|
+
#FASTEMBED_CACHE_DIR=/var/cache/fastembed
|
|
178
|
+
`
|
|
179
|
+
|
|
180
|
+
const FASTEMBED_SYSTEMD_SERVICE = `# Generated by suemo init fastembed
|
|
181
|
+
[Unit]
|
|
182
|
+
Description=FastEmbed OpenAI-compatible embedding service
|
|
183
|
+
After=network.target
|
|
184
|
+
|
|
185
|
+
[Service]
|
|
186
|
+
Type=simple
|
|
187
|
+
AmbientCapabilities=CAP_NET_BIND_SERVICE
|
|
188
|
+
User=fastembed
|
|
189
|
+
Group=fastembed
|
|
190
|
+
WorkingDirectory=/opt/suemo
|
|
191
|
+
Environment=FASTEMBED_HOST=127.0.0.1
|
|
192
|
+
Environment=FASTEMBED_PORT=8080
|
|
193
|
+
Environment=FASTEMBED_MODEL=sentence-transformers/all-MiniLM-L6-v2
|
|
194
|
+
Environment=FASTEMBED_CACHE_DIR=/var/cache/fastembed
|
|
195
|
+
EnvironmentFile=-/opt/fastembed/local.env
|
|
196
|
+
ExecStart=/usr/bin/python /opt/suemo/fastembed-server.py
|
|
197
|
+
Restart=on-failure
|
|
198
|
+
RestartSec=5s
|
|
199
|
+
NoNewPrivileges=true
|
|
200
|
+
PrivateTmp=true
|
|
201
|
+
ProtectSystem=strict
|
|
202
|
+
ProtectHome=true
|
|
203
|
+
ReadWritePaths=/var/cache/fastembed
|
|
204
|
+
StandardOutput=journal
|
|
205
|
+
StandardError=journal
|
|
206
|
+
SyslogIdentifier=suemo-fastembed
|
|
207
|
+
|
|
208
|
+
[Install]
|
|
209
|
+
WantedBy=multi-user.target
|
|
210
|
+
`
|
|
211
|
+
|
|
212
|
+
type InitAction =
|
|
213
|
+
| {
|
|
214
|
+
kind: 'mkdir'
|
|
215
|
+
path: string
|
|
216
|
+
mode: number
|
|
217
|
+
}
|
|
218
|
+
| {
|
|
219
|
+
kind: 'write'
|
|
220
|
+
path: string
|
|
221
|
+
mode: number
|
|
222
|
+
content: string
|
|
223
|
+
}
|
|
224
|
+
| {
|
|
225
|
+
kind: 'run'
|
|
226
|
+
command: string
|
|
227
|
+
args: string[]
|
|
228
|
+
requireRoot?: boolean
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
type CredentialStatus = 'preserved' | 'generated'
|
|
232
|
+
|
|
233
|
+
interface SurrealInitResult {
|
|
234
|
+
actions: InitAction[]
|
|
235
|
+
credentialStatus: CredentialStatus
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function modeText(mode: number): string {
|
|
239
|
+
return mode.toString(8)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function hasRoot(): boolean {
|
|
243
|
+
return typeof process.getuid === 'function' ? process.getuid() === 0 : false
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function runCommand(command: string, args: string[], options?: { requireRoot?: boolean; quiet?: boolean }): void {
|
|
247
|
+
const requireRoot = options?.requireRoot ?? false
|
|
248
|
+
const quiet = options?.quiet ?? false
|
|
249
|
+
if (requireRoot && !hasRoot()) {
|
|
250
|
+
throw new Error(
|
|
251
|
+
`Root privileges required for ${command}. Run init commands as root (e.g. sudo suemo init ... or sudo bunx suemo init ...).`,
|
|
252
|
+
)
|
|
253
|
+
}
|
|
254
|
+
const result = spawnSync(command, args, {
|
|
255
|
+
stdio: quiet ? 'ignore' : 'inherit',
|
|
256
|
+
})
|
|
257
|
+
if ((result.status ?? 1) !== 0) {
|
|
258
|
+
throw new Error(`Command failed: ${[command, ...args].join(' ')}`)
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function requireRootForInit(subcommand: string): void {
|
|
263
|
+
if (hasRoot()) return
|
|
264
|
+
throw new Error(
|
|
265
|
+
`\`suemo ${subcommand}\` must be run as root. Try: \`sudo suemo ${subcommand}\` (or \`sudo bunx suemo ${subcommand}\` if suemo is not on root PATH).`,
|
|
266
|
+
)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function packageInstalled(pkg: string): boolean {
|
|
270
|
+
const result = spawnSync('pacman', ['-Q', pkg], { stdio: 'ignore' })
|
|
271
|
+
return (result.status ?? 1) === 0
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function requireArchPackages(packages: string[]): void {
|
|
275
|
+
const pacmanProbe = spawnSync('pacman', ['--version'], { stdio: 'ignore' })
|
|
276
|
+
if ((pacmanProbe.status ?? 1) !== 0) {
|
|
277
|
+
throw new Error('pacman not found. `suemo init surreal/fastembed` currently supports Arch Linux systems.')
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const missing = packages.filter((pkg) => !packageInstalled(pkg))
|
|
281
|
+
if (missing.length > 0) {
|
|
282
|
+
throw new Error(
|
|
283
|
+
`Missing required Arch package(s): ${missing.join(', ')}. Install with: sudo pacman -S ${missing.join(' ')}`,
|
|
284
|
+
)
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function commandExists(command: string): boolean {
|
|
289
|
+
const result = spawnSync('sh', ['-lc', `command -v ${command}`], { stdio: 'ignore' })
|
|
290
|
+
return (result.status ?? 1) === 0
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function requireCommands(commands: string[]): void {
|
|
294
|
+
const missing = commands.filter((command) => !commandExists(command))
|
|
295
|
+
if (missing.length > 0) {
|
|
296
|
+
throw new Error(`Missing required command(s): ${missing.join(', ')}`)
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function systemUserExists(user: string): boolean {
|
|
301
|
+
const result = spawnSync('id', ['-u', user], { stdio: 'ignore' })
|
|
302
|
+
return (result.status ?? 1) === 0
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function ensureDir(path: string, mode: number): void {
|
|
306
|
+
if (hasRoot()) {
|
|
307
|
+
mkdirSync(path, { recursive: true })
|
|
308
|
+
chmodSync(path, mode)
|
|
309
|
+
return
|
|
310
|
+
}
|
|
311
|
+
runCommand('install', ['-d', '-m', modeText(mode), path], { requireRoot: true })
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function writeRootFile(path: string, content: string, mode: number): void {
|
|
315
|
+
if (hasRoot()) {
|
|
316
|
+
mkdirSync(dirname(path), { recursive: true })
|
|
317
|
+
writeFileSync(path, content, 'utf-8')
|
|
318
|
+
chmodSync(path, mode)
|
|
319
|
+
return
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const tempDir = mkdtempSync(join(tmpdir(), 'suemo-init-'))
|
|
323
|
+
const tempFile = join(tempDir, basename(path))
|
|
324
|
+
try {
|
|
325
|
+
writeFileSync(tempFile, content, 'utf-8')
|
|
326
|
+
runCommand('install', ['-D', '-m', modeText(mode), tempFile, path], { requireRoot: true })
|
|
327
|
+
} finally {
|
|
328
|
+
rmSync(tempDir, { recursive: true, force: true })
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function generatePassword(): string {
|
|
333
|
+
return randomBytes(20).toString('base64url')
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function printDryRunActions(actions: InitAction[]): void {
|
|
337
|
+
for (const action of actions) {
|
|
338
|
+
if (action.kind === 'mkdir') {
|
|
339
|
+
console.log(`[dry-run] mkdir ${action.path} (mode ${modeText(action.mode)})`)
|
|
340
|
+
continue
|
|
341
|
+
}
|
|
342
|
+
if (action.kind === 'run') {
|
|
343
|
+
console.log(`[dry-run] run ${action.command} ${action.args.join(' ')}`.trim())
|
|
344
|
+
continue
|
|
345
|
+
}
|
|
346
|
+
console.log(`[dry-run] write ${action.path} (mode ${modeText(action.mode)})`)
|
|
347
|
+
console.log('-----8<-----')
|
|
348
|
+
console.log(action.content.trimEnd())
|
|
349
|
+
console.log('----->8-----')
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function applyActions(actions: InitAction[]): void {
|
|
354
|
+
for (const action of actions) {
|
|
355
|
+
if (action.kind === 'mkdir') {
|
|
356
|
+
ensureDir(action.path, action.mode)
|
|
357
|
+
continue
|
|
358
|
+
}
|
|
359
|
+
if (action.kind === 'write') {
|
|
360
|
+
writeRootFile(action.path, action.content, action.mode)
|
|
361
|
+
continue
|
|
362
|
+
}
|
|
363
|
+
if (action.requireRoot === true) {
|
|
364
|
+
runCommand(action.command, action.args, { requireRoot: true })
|
|
365
|
+
} else {
|
|
366
|
+
runCommand(action.command, action.args)
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function readEnvFile(path: string): string | null {
|
|
372
|
+
try {
|
|
373
|
+
const content = readFileSync(path, 'utf-8')
|
|
374
|
+
log.debug(`readEnvFile: successfully read file`, { path, length: content.length })
|
|
375
|
+
return content
|
|
376
|
+
} catch (error) {
|
|
377
|
+
const errorCode = error && typeof error === 'object' && 'code' in error
|
|
378
|
+
? (error as { code: string }).code
|
|
379
|
+
: 'UNKNOWN'
|
|
380
|
+
|
|
381
|
+
if (errorCode === 'ENOENT') {
|
|
382
|
+
log.debug(`readEnvFile: file does not exist`, { path })
|
|
383
|
+
return null
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
throw new Error(`Failed to read ${path}: ${error instanceof Error ? error.message : String(error)}`)
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function extractEnvValue(content: string, key: string): string | null {
|
|
391
|
+
const match = new RegExp(`^${key}\\s*=\\s*(.+)$`, 'm').exec(content)
|
|
392
|
+
if (!match || !match[1]) return null
|
|
393
|
+
return match[1].trim()
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function readFastembedScriptSource(): string {
|
|
397
|
+
const path = fileURLToPath(new URL('../../../data/fastembed-server.py', import.meta.url))
|
|
398
|
+
return readFileSync(path, 'utf-8')
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function buildSurrealActions(
|
|
402
|
+
profile: '2gb' | '6gb',
|
|
403
|
+
existingPassword: string | null,
|
|
404
|
+
force: boolean,
|
|
405
|
+
): SurrealInitResult {
|
|
406
|
+
const commonEnvPath = join(SURREAL_PROFILES_DIR, 'common.env')
|
|
407
|
+
const profileEnvPath = join(SURREAL_PROFILES_DIR, `${profile}.env`)
|
|
408
|
+
const localEnvPath = SURREAL_LOCAL_ENV_PATH
|
|
409
|
+
const actions: InitAction[] = []
|
|
410
|
+
|
|
411
|
+
log.debug('buildSurrealActions: start', { profile, existingPassword: existingPassword ? '***' : null, force })
|
|
412
|
+
|
|
413
|
+
if (!systemUserExists('surrealdb')) {
|
|
414
|
+
actions.push({
|
|
415
|
+
kind: 'run',
|
|
416
|
+
command: 'useradd',
|
|
417
|
+
args: [
|
|
418
|
+
'--system',
|
|
419
|
+
'--home-dir',
|
|
420
|
+
'/var/lib/surrealdb',
|
|
421
|
+
'--shell',
|
|
422
|
+
'/usr/bin/nologin',
|
|
423
|
+
'--create-home',
|
|
424
|
+
'surrealdb',
|
|
425
|
+
],
|
|
426
|
+
requireRoot: true,
|
|
427
|
+
})
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const credentialStatus: CredentialStatus = existingPassword && !force ? 'preserved' : 'generated'
|
|
431
|
+
log.debug('buildSurrealActions: credential decision', { credentialStatus })
|
|
432
|
+
|
|
433
|
+
const password = credentialStatus === 'preserved' ? existingPassword! : generatePassword()
|
|
434
|
+
log.debug('buildSurrealActions: password determined', { passwordSet: !!password })
|
|
435
|
+
|
|
436
|
+
let commonEnv = SURREAL_COMMON_ENV_TEMPLATE.replace('__SURREAL_PASS__', password)
|
|
437
|
+
const existingCommonEnv = readEnvFile(commonEnvPath)
|
|
438
|
+
if (existingCommonEnv && !force) {
|
|
439
|
+
const existingUser = extractEnvValue(existingCommonEnv, 'SURREAL_USER')
|
|
440
|
+
const existingPass = extractEnvValue(existingCommonEnv, 'SURREAL_PASS')
|
|
441
|
+
if (existingUser && existingPass) {
|
|
442
|
+
log.debug('buildSurrealActions: preserving existing user and pass from common.env')
|
|
443
|
+
const lines = commonEnv.split('\n')
|
|
444
|
+
const preservedLines = lines.map((line) => {
|
|
445
|
+
if (line.startsWith('SURREAL_USER=')) {
|
|
446
|
+
return `SURREAL_USER=${existingUser}`
|
|
447
|
+
}
|
|
448
|
+
if (line.startsWith('SURREAL_PASS=')) {
|
|
449
|
+
return `SURREAL_PASS=${existingPass}`
|
|
450
|
+
}
|
|
451
|
+
return line
|
|
452
|
+
})
|
|
453
|
+
commonEnv = preservedLines.join('\n')
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
let profileEnv = profile === '2gb' ? SURREAL_PROFILE_2GB_ENV : SURREAL_PROFILE_6GB_ENV
|
|
458
|
+
const existingProfileEnv = readEnvFile(profileEnvPath)
|
|
459
|
+
if (existingProfileEnv && !force) {
|
|
460
|
+
log.debug('buildSurrealActions: preserving existing profile env')
|
|
461
|
+
profileEnv = existingProfileEnv
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const localEnvExists = existsSync(localEnvPath)
|
|
465
|
+
|
|
466
|
+
actions.push(
|
|
467
|
+
{ kind: 'mkdir', path: SURREAL_PROFILES_DIR, mode: 0o750 },
|
|
468
|
+
{ kind: 'mkdir', path: SURREAL_DATA_DIR, mode: 0o750 },
|
|
469
|
+
{ kind: 'write', path: commonEnvPath, mode: 0o640, content: commonEnv },
|
|
470
|
+
{ kind: 'write', path: profileEnvPath, mode: 0o640, content: profileEnv },
|
|
471
|
+
...(!localEnvExists
|
|
472
|
+
? [{ kind: 'write' as const, path: localEnvPath, mode: 0o644, content: SURREAL_LOCAL_ENV_TEMPLATE }]
|
|
473
|
+
: []),
|
|
474
|
+
{ kind: 'write', path: SURREAL_SYSTEMD_UNIT_PATH, mode: 0o644, content: SURREAL_TEMPLATE_UNIT },
|
|
475
|
+
{ kind: 'run', command: 'chown', args: ['-R', 'surrealdb:surrealdb', SURREAL_DATA_DIR], requireRoot: true },
|
|
476
|
+
{
|
|
477
|
+
kind: 'run',
|
|
478
|
+
command: 'chown',
|
|
479
|
+
args: ['root:surrealdb', commonEnvPath],
|
|
480
|
+
requireRoot: true,
|
|
481
|
+
},
|
|
482
|
+
{
|
|
483
|
+
kind: 'run',
|
|
484
|
+
command: 'chown',
|
|
485
|
+
args: ['root:surrealdb', profileEnvPath],
|
|
486
|
+
requireRoot: true,
|
|
487
|
+
},
|
|
488
|
+
{ kind: 'run', command: 'systemctl', args: ['daemon-reload'], requireRoot: true },
|
|
489
|
+
{
|
|
490
|
+
kind: 'run',
|
|
491
|
+
command: 'systemctl',
|
|
492
|
+
args: ['enable', '--now', `suemo-surrealdb@${profile}.service`],
|
|
493
|
+
requireRoot: true,
|
|
494
|
+
},
|
|
495
|
+
)
|
|
496
|
+
|
|
497
|
+
log.debug('buildSurrealActions: complete', { actionCount: actions.length })
|
|
498
|
+
return { actions, credentialStatus }
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function buildFastembedActions(scriptContent: string): InitAction[] {
|
|
502
|
+
const actions: InitAction[] = []
|
|
503
|
+
const localEnvExists = existsSync(FASTEMBED_LOCAL_ENV_PATH)
|
|
504
|
+
|
|
505
|
+
if (!systemUserExists('fastembed')) {
|
|
506
|
+
actions.push({
|
|
507
|
+
kind: 'run',
|
|
508
|
+
command: 'useradd',
|
|
509
|
+
args: [
|
|
510
|
+
'--system',
|
|
511
|
+
'--home-dir',
|
|
512
|
+
FASTEMBED_USER_HOME,
|
|
513
|
+
'--shell',
|
|
514
|
+
'/usr/bin/nologin',
|
|
515
|
+
'--create-home',
|
|
516
|
+
'fastembed',
|
|
517
|
+
],
|
|
518
|
+
requireRoot: true,
|
|
519
|
+
})
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
actions.push(
|
|
523
|
+
{ kind: 'mkdir', path: FASTEMBED_INSTALL_DIR, mode: 0o755 },
|
|
524
|
+
{ kind: 'mkdir', path: FASTEMBED_LOCAL_ENV_DIR, mode: 0o755 },
|
|
525
|
+
{ kind: 'mkdir', path: FASTEMBED_CACHE_DIR, mode: 0o755 },
|
|
526
|
+
{ kind: 'write', path: FASTEMBED_SCRIPT_TARGET, mode: 0o755, content: scriptContent },
|
|
527
|
+
...(!localEnvExists
|
|
528
|
+
? [{ kind: 'write' as const, path: FASTEMBED_LOCAL_ENV_PATH, mode: 0o644, content: FASTEMBED_LOCAL_ENV_TEMPLATE }]
|
|
529
|
+
: []),
|
|
530
|
+
{ kind: 'write', path: FASTEMBED_SERVICE_PATH, mode: 0o644, content: FASTEMBED_SYSTEMD_SERVICE },
|
|
531
|
+
{ kind: 'run', command: 'chown', args: ['-R', 'fastembed:fastembed', FASTEMBED_INSTALL_DIR], requireRoot: true },
|
|
532
|
+
{ kind: 'run', command: 'chown', args: ['root:root', FASTEMBED_LOCAL_ENV_DIR], requireRoot: true },
|
|
533
|
+
{ kind: 'run', command: 'chown', args: ['root:root', FASTEMBED_LOCAL_ENV_PATH], requireRoot: true },
|
|
534
|
+
{ kind: 'run', command: 'chown', args: ['-R', 'fastembed:fastembed', FASTEMBED_CACHE_DIR], requireRoot: true },
|
|
535
|
+
{ kind: 'run', command: 'systemctl', args: ['daemon-reload'], requireRoot: true },
|
|
536
|
+
{ kind: 'run', command: 'systemctl', args: ['enable', '--now', 'suemo-fastembed.service'], requireRoot: true },
|
|
537
|
+
)
|
|
538
|
+
|
|
539
|
+
return actions
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function printInitSystemSummary(kind: 'surreal' | 'fastembed', dryRun: boolean, profile?: '2gb' | '6gb'): void {
|
|
543
|
+
if (kind === 'surreal') {
|
|
544
|
+
if (dryRun) {
|
|
545
|
+
console.log('Dry-run complete for SurrealDB setup.')
|
|
546
|
+
} else {
|
|
547
|
+
console.log('✓ SurrealDB setup complete.')
|
|
548
|
+
}
|
|
549
|
+
if (profile) {
|
|
550
|
+
console.log(`Service: suemo-surrealdb@${profile}.service`)
|
|
551
|
+
console.log(`Status: systemctl status suemo-surrealdb@${profile}.service`)
|
|
552
|
+
console.log(`Logs: journalctl -u suemo-surrealdb@${profile}.service -f`)
|
|
553
|
+
}
|
|
554
|
+
return
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
if (dryRun) {
|
|
558
|
+
console.log('Dry-run complete for fastembed setup.')
|
|
559
|
+
} else {
|
|
560
|
+
console.log('✓ fastembed setup complete.')
|
|
561
|
+
}
|
|
562
|
+
console.log('Service: suemo-fastembed.service')
|
|
563
|
+
console.log('Status: systemctl status suemo-fastembed.service')
|
|
564
|
+
console.log('Logs: journalctl -u suemo-fastembed.service -f')
|
|
565
|
+
}
|
|
566
|
+
|
|
28
567
|
function homeConfigPath(): string {
|
|
29
568
|
const home = process.env.HOME ?? process.env.USERPROFILE
|
|
30
569
|
if (!home) throw new Error('HOME/USERPROFILE is not set; cannot resolve ~/.suemo path')
|
|
@@ -241,10 +780,137 @@ const initOpenCodeCmd = init.sub('opencode')
|
|
|
241
780
|
console.log(`Installed suemo version: ${npmVersion}`)
|
|
242
781
|
})
|
|
243
782
|
|
|
783
|
+
const initSurrealCmd = init.sub('surreal')
|
|
784
|
+
.meta({ description: 'Install SurrealDB systemd profile (2gb|6gb) with VERSIONED + allowlist config' })
|
|
785
|
+
.args([{ name: 'profile', type: 'string', required: true }])
|
|
786
|
+
.flags({
|
|
787
|
+
force: { type: 'boolean', description: 'Overwrite existing env files' },
|
|
788
|
+
'dry-run': { type: 'boolean', description: 'Print generated files and planned commands', default: false },
|
|
789
|
+
json: { type: 'boolean', description: 'Machine-readable output mode' },
|
|
790
|
+
'verbose-json': { type: 'boolean', description: 'Include embeddings in JSON output (implies --json)' },
|
|
791
|
+
pretty: { type: 'boolean', description: 'Human-readable output (default)' },
|
|
792
|
+
})
|
|
793
|
+
.run(async ({ args, flags }) => {
|
|
794
|
+
const outputMode = resolveOutputModeOrExit(flags)
|
|
795
|
+
await initCliCommand('init surreal', {
|
|
796
|
+
debug: flags.debug,
|
|
797
|
+
config: flags.config,
|
|
798
|
+
json: outputMode === 'json',
|
|
799
|
+
quiet: flags.quiet,
|
|
800
|
+
})
|
|
801
|
+
requireRootForInit('init surreal <2gb|6gb>')
|
|
802
|
+
|
|
803
|
+
requireCommands(['pacman', 'systemctl', 'install', 'chown', 'id'])
|
|
804
|
+
requireArchPackages(['surrealdb'])
|
|
805
|
+
|
|
806
|
+
const profileRaw = (args as { profile: string }).profile.trim().toLowerCase()
|
|
807
|
+
if (profileRaw !== '2gb' && profileRaw !== '6gb') {
|
|
808
|
+
throw new Error('Profile must be 2gb or 6gb')
|
|
809
|
+
}
|
|
810
|
+
const profile = profileRaw as '2gb' | '6gb'
|
|
811
|
+
|
|
812
|
+
const commonEnvPath = join(SURREAL_PROFILES_DIR, 'common.env')
|
|
813
|
+
const existingCommonEnv = readEnvFile(commonEnvPath)
|
|
814
|
+
const existingPassword = existingCommonEnv ? extractEnvValue(existingCommonEnv, 'SURREAL_PASS') : null
|
|
815
|
+
const force = Boolean(flags.force)
|
|
816
|
+
const dryRun = Boolean(flags['dry-run'])
|
|
817
|
+
|
|
818
|
+
log.debug('init surreal: reading existing password', {
|
|
819
|
+
commonEnvExists: existsSync(commonEnvPath),
|
|
820
|
+
existingPasswordFound: !!existingPassword,
|
|
821
|
+
force,
|
|
822
|
+
})
|
|
823
|
+
|
|
824
|
+
if (dryRun) {
|
|
825
|
+
if (outputMode === 'json') {
|
|
826
|
+
printCliJson({
|
|
827
|
+
ok: true,
|
|
828
|
+
dryRun: true,
|
|
829
|
+
profile,
|
|
830
|
+
existingPassword: existingPassword ?? 'none (not set)',
|
|
831
|
+
actions: [],
|
|
832
|
+
}, flags)
|
|
833
|
+
return
|
|
834
|
+
}
|
|
835
|
+
printInitSystemSummary('surreal', true, profile)
|
|
836
|
+
console.log(`Existing SURREAL_PASS preview: ${existingPassword ?? 'none (not set)'}`)
|
|
837
|
+
return
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
const result = buildSurrealActions(profile, existingPassword, force)
|
|
841
|
+
applyActions(result.actions)
|
|
842
|
+
printInitSystemSummary('surreal', false, profile)
|
|
843
|
+
|
|
844
|
+
if (result.credentialStatus === 'preserved') {
|
|
845
|
+
console.log(
|
|
846
|
+
`Existing SURREAL_USER and SURREAL_PASS have been preserved in ${commonEnvPath}. Use --force to regenerate password.`,
|
|
847
|
+
)
|
|
848
|
+
} else {
|
|
849
|
+
console.log(`Generated new SURREAL_PASS and wrote ${commonEnvPath} (rotate if needed).`)
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
if (outputMode === 'json') {
|
|
853
|
+
printCliJson({
|
|
854
|
+
ok: true,
|
|
855
|
+
dryRun: false,
|
|
856
|
+
profile,
|
|
857
|
+
service: `suemo-surrealdb@${profile}.service`,
|
|
858
|
+
credentialStatus: result.credentialStatus,
|
|
859
|
+
}, flags)
|
|
860
|
+
return
|
|
861
|
+
}
|
|
862
|
+
})
|
|
863
|
+
|
|
864
|
+
const initFastembedCmd = init.sub('fastembed')
|
|
865
|
+
.meta({ description: 'Install fastembed systemd service from data/fastembed.py' })
|
|
866
|
+
.flags({
|
|
867
|
+
'dry-run': { type: 'boolean', description: 'Print generated files and planned commands', default: false },
|
|
868
|
+
json: { type: 'boolean', description: 'Machine-readable output mode' },
|
|
869
|
+
'verbose-json': { type: 'boolean', description: 'Include embeddings in JSON output (implies --json)' },
|
|
870
|
+
pretty: { type: 'boolean', description: 'Human-readable output (default)' },
|
|
871
|
+
})
|
|
872
|
+
.run(async ({ flags }) => {
|
|
873
|
+
const outputMode = resolveOutputModeOrExit(flags)
|
|
874
|
+
await initCliCommand('init fastembed', {
|
|
875
|
+
debug: flags.debug,
|
|
876
|
+
config: flags.config,
|
|
877
|
+
json: outputMode === 'json',
|
|
878
|
+
quiet: flags.quiet,
|
|
879
|
+
})
|
|
880
|
+
requireRootForInit('init fastembed')
|
|
881
|
+
|
|
882
|
+
requireCommands(['pacman', 'systemctl', 'install', 'chown', 'id'])
|
|
883
|
+
requireArchPackages(['python-fastembed', 'python-fastapi', 'uvicorn'])
|
|
884
|
+
|
|
885
|
+
const scriptContent = readFastembedScriptSource()
|
|
886
|
+
const actions = buildFastembedActions(scriptContent)
|
|
887
|
+
const dryRun = Boolean(flags['dry-run'])
|
|
888
|
+
|
|
889
|
+
if (dryRun) {
|
|
890
|
+
if (outputMode === 'json') {
|
|
891
|
+
printCliJson({ ok: true, dryRun: true, actions }, flags)
|
|
892
|
+
return
|
|
893
|
+
}
|
|
894
|
+
printDryRunActions(actions)
|
|
895
|
+
printInitSystemSummary('fastembed', true)
|
|
896
|
+
return
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
applyActions(actions)
|
|
900
|
+
printInitSystemSummary('fastembed', false)
|
|
901
|
+
|
|
902
|
+
if (outputMode === 'json') {
|
|
903
|
+
printCliJson({ ok: true, dryRun: false, service: 'suemo-fastembed.service' }, flags)
|
|
904
|
+
return
|
|
905
|
+
}
|
|
906
|
+
})
|
|
907
|
+
|
|
244
908
|
export const initCmd = init
|
|
245
909
|
.command(initConfigCmd)
|
|
246
910
|
.command(initSchemaCmd)
|
|
247
911
|
.command(initOpenCodeCmd)
|
|
912
|
+
.command(initSurrealCmd)
|
|
913
|
+
.command(initFastembedCmd)
|
|
248
914
|
.flags({
|
|
249
915
|
json: { type: 'boolean', description: 'Machine-readable output mode' },
|
|
250
916
|
'verbose-json': { type: 'boolean', description: 'Include embeddings in JSON output (implies --json)' },
|
|
@@ -262,6 +928,8 @@ export const initCmd = init
|
|
|
262
928
|
console.log(' suemo init config [--force]')
|
|
263
929
|
console.log(' suemo init schema [--yes]')
|
|
264
930
|
console.log(' suemo init opencode')
|
|
931
|
+
console.log(' suemo init surreal <2gb|6gb> [--force] [--dry-run]')
|
|
932
|
+
console.log(' suemo init fastembed [--dry-run]')
|
|
265
933
|
console.log(' suemo skill [reference]')
|
|
266
934
|
console.log('\nRun `suemo init --help` for full details.')
|
|
267
935
|
})
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"lib": ["ESNext"],
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"moduleDetection": "force",
|
|
7
|
+
"resolveJsonModule": true,
|
|
8
|
+
|
|
9
|
+
"target": "ESNext",
|
|
10
|
+
"noEmit": true,
|
|
11
|
+
"isolatedModules": true,
|
|
12
|
+
|
|
13
|
+
"rewriteRelativeImportExtensions": true,
|
|
14
|
+
"allowImportingTsExtensions": true,
|
|
15
|
+
"verbatimModuleSyntax": true,
|
|
16
|
+
|
|
17
|
+
"strict": true,
|
|
18
|
+
"noUncheckedIndexedAccess": true,
|
|
19
|
+
"exactOptionalPropertyTypes": true,
|
|
20
|
+
"noUnusedLocals": true,
|
|
21
|
+
|
|
22
|
+
"skipLibCheck": true,
|
|
23
|
+
|
|
24
|
+
"paths": {
|
|
25
|
+
"@/*": ["./*"]
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"include": ["src/**/*", "suemo.config.ts"]
|
|
29
|
+
}
|