saeeol 1.3.1 → 1.4.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/bin/saeeol.cjs +3 -1
- package/bunfig.toml +7 -0
- package/package.json +1 -1
- package/src/cli/cmd/tui/preflight.ts +138 -0
- package/src/cli/cmd/tui/thread.ts +20 -0
- package/src/server/routes/instance/httpapi/groups/ltm.ts +93 -0
- package/src/server/routes/instance/httpapi/handlers/ltm.ts +118 -0
- package/src/server/routes/instance/index.ts +96 -1
- package/test/smoke/.tui-debug-output.txt +1 -0
- package/test/smoke/.tui-debug-plain.txt +1 -0
- package/test/smoke/.tui-input-report.txt +110 -0
- package/test/smoke/.tui-leader-report.txt +70 -0
- package/test/smoke/.tui-scroll-report.txt +66 -0
- package/test/smoke/.tui-slash-report.txt +146 -0
- package/test/smoke/.tui-system-report.txt +62 -0
- package/test/smoke/.tui-walkthrough-report.txt +122 -0
- package/test/smoke/smoke-tui-pty.test.ts +123 -0
- package/test/smoke/smoke-tui.mjs +83 -0
- package/test/smoke/tui-walkthrough-driver.ts +232 -0
- package/test/smoke/tui-walkthrough-input.test.ts +286 -0
- package/test/smoke/tui-walkthrough-leader.test.ts +175 -0
- package/test/smoke/tui-walkthrough-scroll.test.ts +177 -0
- package/test/smoke/tui-walkthrough-slash.test.ts +302 -0
- package/test/smoke/tui-walkthrough-system.test.ts +210 -0
- package/test/smoke/tui-walkthrough.test.ts +520 -0
- package/.turbo/turbo-typecheck.log +0 -1
package/bin/saeeol.cjs
CHANGED
|
@@ -188,7 +188,9 @@ if (fs.existsSync(indexPath)) {
|
|
|
188
188
|
const which = childProcess.spawnSync(bunExe, ["--version"], { encoding: "utf8", timeout: 3000 })
|
|
189
189
|
if (which.status === 0) {
|
|
190
190
|
const result = childProcess.spawnSync(bunExe, [
|
|
191
|
-
"run", "--conditions=browser",
|
|
191
|
+
"run", "--conditions=browser",
|
|
192
|
+
"--jsx-import-source=@opentui/solid",
|
|
193
|
+
indexPath, ...process.argv.slice(2)
|
|
192
194
|
], { stdio: "inherit" })
|
|
193
195
|
const code = typeof result.status === "number" ? result.status : 0
|
|
194
196
|
process.exit(code)
|
package/bunfig.toml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
preload = ["@opentui/solid/preload"]
|
|
2
|
+
|
|
3
|
+
[test]
|
|
4
|
+
preload = ["@opentui/solid/preload", "./test/preload.ts"]
|
|
5
|
+
# timeout is not actually parsed from bunfig.toml (see src/bunfig.zig in oven-sh/bun)
|
|
6
|
+
# using --timeout in package.json scripts instead
|
|
7
|
+
# https://github.com/oven-sh/bun/issues/7789
|
package/package.json
CHANGED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* preflight.ts — TUI 부트 전 자기 검사
|
|
3
|
+
*
|
|
4
|
+
* Worker를 스폰하기 전에 빠르게 실패할 수 있는 조건들을 검사:
|
|
5
|
+
* 1. JSX 트랜스폼 — SolidJS/OpenTUI 런타임 로드 가능한지
|
|
6
|
+
* 2. Bun 버전 — 최소 버전 충족
|
|
7
|
+
* 3. 필수 의존성 — node_modules에 핵심 패키지 있는지
|
|
8
|
+
* 4. 디스크 공간 — 임시 파일/스냅샷 저장 가능한지
|
|
9
|
+
* 5. 포트 가용성 — 서버 바인딩 가능한지
|
|
10
|
+
* 6. 이전 크래시 로그 — 마지막 실행에서 크래시했는지
|
|
11
|
+
*
|
|
12
|
+
* 모든 체크는 동기적이고 100ms 이내 완료.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import * as Log from "@saeeol/core/util/log"
|
|
16
|
+
import { Filesystem } from "@/util/filesystem"
|
|
17
|
+
import path from "path"
|
|
18
|
+
import fs from "fs"
|
|
19
|
+
|
|
20
|
+
export interface PreflightResult {
|
|
21
|
+
severity: "error" | "warning"
|
|
22
|
+
message: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function preflight(): PreflightResult[] {
|
|
26
|
+
const results: PreflightResult[] = []
|
|
27
|
+
|
|
28
|
+
// 1. JSX 런타임 — SolidJS/OpenTUI 로드 가능한지
|
|
29
|
+
try {
|
|
30
|
+
require.resolve("@opentui/solid")
|
|
31
|
+
} catch {
|
|
32
|
+
// Bun 소스 모드에서는 require.resolve가 안 될 수 있음
|
|
33
|
+
// 대신 실제 import 테스트
|
|
34
|
+
try {
|
|
35
|
+
// jsxImportSource가 제대로 설정되었는지 간접 확인
|
|
36
|
+
// bun 실행 시 --jsx-import-source가 전달되었는지
|
|
37
|
+
const tsconfigPath = path.resolve(import.meta.dir, "../../../../tsconfig.json")
|
|
38
|
+
if (fs.existsSync(tsconfigPath)) {
|
|
39
|
+
const tsconfig = JSON.parse(fs.readFileSync(tsconfigPath, "utf8"))
|
|
40
|
+
const jsxSource = tsconfig.compilerOptions?.jsxImportSource
|
|
41
|
+
if (jsxSource && jsxSource !== "@opentui/solid") {
|
|
42
|
+
results.push({
|
|
43
|
+
severity: "error",
|
|
44
|
+
message: `jsxImportSource is "${jsxSource}", expected "@opentui/solid" — run with --jsx-import-source=@opentui/solid`,
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
} catch {
|
|
49
|
+
// tsconfig 읽기 실패 — 무시 (빌드된 바이너리면 tsconfig 없음)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 2. Bun 버전 체크 (소스 모드에서만)
|
|
54
|
+
if (typeof Bun !== "undefined") {
|
|
55
|
+
const version = Bun.version
|
|
56
|
+
const [major, minor] = version.split(".").map(Number)
|
|
57
|
+
if (major < 1 || (major === 1 && minor < 1)) {
|
|
58
|
+
results.push({
|
|
59
|
+
severity: "warning",
|
|
60
|
+
message: `Bun ${version} detected — TUI requires Bun 1.1.0+ for SolidJS JSX support`,
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// 3. 필수 패키지 존재 확인
|
|
66
|
+
const rootDir = path.resolve(import.meta.dir, "../../../../")
|
|
67
|
+
const requiredPackages = ["@opentui/core", "@opentui/solid"]
|
|
68
|
+
for (const pkg of requiredPackages) {
|
|
69
|
+
const pkgPath = path.join(rootDir, "node_modules", pkg, "package.json")
|
|
70
|
+
if (!fs.existsSync(pkgPath)) {
|
|
71
|
+
results.push({
|
|
72
|
+
severity: "error",
|
|
73
|
+
message: `Missing dependency: ${pkg} — run your package manager install`,
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// 4. 디스크 공간 — 임시 디렉토리 쓰기 가능한지
|
|
79
|
+
const tmpDir = path.resolve(import.meta.dir, "../.tui-tmp")
|
|
80
|
+
try {
|
|
81
|
+
fs.mkdirSync(tmpDir, { recursive: true })
|
|
82
|
+
const testFile = path.join(tmpDir, ".preflight-write-test")
|
|
83
|
+
fs.writeFileSync(testFile, "ok")
|
|
84
|
+
fs.unlinkSync(testFile)
|
|
85
|
+
} catch (err) {
|
|
86
|
+
results.push({
|
|
87
|
+
severity: "error",
|
|
88
|
+
message: `Cannot write to TUI temp directory: ${(err as Error).message}`,
|
|
89
|
+
})
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// 5. 이전 크래시 로그 표시
|
|
93
|
+
const crashLog = path.resolve(import.meta.dir, "../.tui-crash.log")
|
|
94
|
+
if (fs.existsSync(crashLog)) {
|
|
95
|
+
try {
|
|
96
|
+
const stat = fs.statSync(crashLog)
|
|
97
|
+
const ageMs = Date.now() - stat.mtimeMs
|
|
98
|
+
// 최근 10분 이내 크래시면 표시
|
|
99
|
+
if (ageMs < 10 * 60 * 1000) {
|
|
100
|
+
const content = fs.readFileSync(crashLog, "utf8").trim().slice(0, 200)
|
|
101
|
+
results.push({
|
|
102
|
+
severity: "warning",
|
|
103
|
+
message: `Previous TUI crash detected (${Math.round(ageMs / 1000)}s ago): ${content}`,
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
// 오래된 크래시 로그는 자동 삭제
|
|
107
|
+
fs.unlinkSync(crashLog)
|
|
108
|
+
} catch {
|
|
109
|
+
// 읽기 실패면 무시
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// 6. Worker 파일 존재 확인
|
|
114
|
+
// target()이 이미 체크하므로 여기서는 스킵
|
|
115
|
+
|
|
116
|
+
// 7. 터미널 capabilities — TUI가 필요한 기능이 있는지
|
|
117
|
+
if (process.env.TERM === "dumb") {
|
|
118
|
+
results.push({
|
|
119
|
+
severity: "warning",
|
|
120
|
+
message: `TERM=dumb — TUI may not render correctly. Set TERM=xterm-256color`,
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return results
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* 크래시 로그 기록 — TUI 종료 시 에러가 있으면 호출
|
|
129
|
+
*/
|
|
130
|
+
export function writeCrashLog(error: unknown): void {
|
|
131
|
+
try {
|
|
132
|
+
const crashLog = path.resolve(import.meta.dir, "../.tui-crash.log")
|
|
133
|
+
const message = error instanceof Error ? `${error.name}: ${error.message}\n${error.stack?.slice(0, 500)}` : String(error)
|
|
134
|
+
fs.writeFileSync(crashLog, message)
|
|
135
|
+
} catch {
|
|
136
|
+
// 크래시 로그 자체가 실패하면 무시
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
} from "@saeeol/core/util/saeeol-process"
|
|
27
27
|
import { validateSession } from "./validate-session"
|
|
28
28
|
import { spawn } from "node:child_process"
|
|
29
|
+
import { preflight, writeCrashLog } from "./preflight"
|
|
29
30
|
|
|
30
31
|
declare global {
|
|
31
32
|
const SAEEOL_WORKER_PATH: string
|
|
@@ -168,6 +169,24 @@ export const TuiThreadCommand = cmd({
|
|
|
168
169
|
// chdir so the thread and worker share the same directory key.
|
|
169
170
|
const next = resolveThreadDirectory(args.project)
|
|
170
171
|
const file = await target()
|
|
172
|
+
|
|
173
|
+
// Preflight checks before spawning Worker
|
|
174
|
+
const pf = preflight()
|
|
175
|
+
if (pf.length > 0) {
|
|
176
|
+
for (const f of pf) {
|
|
177
|
+
if (f.severity === "error") {
|
|
178
|
+
UI.error(`[preflight] ${f.message}`)
|
|
179
|
+
} else {
|
|
180
|
+
UI.println(`[preflight] ⚠ ${f.message}`)
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
const errors = pf.filter((f) => f.severity === "error")
|
|
184
|
+
if (errors.length > 0) {
|
|
185
|
+
UI.error(`TUI cannot start — fix ${errors.length} error(s) above`)
|
|
186
|
+
process.exitCode = 1
|
|
187
|
+
return
|
|
188
|
+
}
|
|
189
|
+
}
|
|
171
190
|
try {
|
|
172
191
|
process.chdir(next)
|
|
173
192
|
} catch {
|
|
@@ -196,6 +215,7 @@ export const TuiThreadCommand = cmd({
|
|
|
196
215
|
const client = Rpc.client<typeof rpc>(worker)
|
|
197
216
|
const error = (e: unknown) => {
|
|
198
217
|
Log.Default.error("process error", { error: errorMessage(e) })
|
|
218
|
+
writeCrashLog(e)
|
|
199
219
|
}
|
|
200
220
|
const reload = () => {
|
|
201
221
|
client.call("reload", undefined).catch((err) => {
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* groups/ltm.ts — LTM (Long-Term Memory) HTTP API group
|
|
3
|
+
*
|
|
4
|
+
* 외부 앱(chowriter 등)이 saeeol LTM에 기억을 저장/검색하는 엔드포인트.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Schema } from "effect"
|
|
8
|
+
import { HttpApi, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
|
|
9
|
+
import { Authorization } from "../middleware/authorization"
|
|
10
|
+
import { InstanceContextMiddleware } from "../middleware/instance-context"
|
|
11
|
+
import { WorkspaceRoutingMiddleware } from "../middleware/workspace-routing"
|
|
12
|
+
import { described } from "./metadata"
|
|
13
|
+
|
|
14
|
+
const root = "/ltm"
|
|
15
|
+
|
|
16
|
+
// Request schemas
|
|
17
|
+
const IngestPayload = Schema.Struct({
|
|
18
|
+
type: Schema.Literal("episodic", "semantic", "procedural"),
|
|
19
|
+
content: Schema.String,
|
|
20
|
+
summary: Schema.String,
|
|
21
|
+
projectID: Schema.optional(Schema.String),
|
|
22
|
+
sessionID: Schema.optional(Schema.String),
|
|
23
|
+
tags: Schema.optional(Schema.mutable(Schema.Array(Schema.String))),
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
const SearchPayload = Schema.Struct({
|
|
27
|
+
query: Schema.String,
|
|
28
|
+
topK: Schema.optional(Schema.Number),
|
|
29
|
+
minScore: Schema.optional(Schema.Number),
|
|
30
|
+
type: Schema.optional(Schema.Literal("episodic", "semantic", "procedural")),
|
|
31
|
+
projectID: Schema.optional(Schema.String),
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
const DeletePayload = Schema.Struct({
|
|
35
|
+
ids: Schema.mutable(Schema.Array(Schema.String)),
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
// Response schemas
|
|
39
|
+
const MemoryEntry = Schema.Struct({
|
|
40
|
+
id: Schema.String,
|
|
41
|
+
type: Schema.String,
|
|
42
|
+
summary: Schema.String,
|
|
43
|
+
content: Schema.String,
|
|
44
|
+
score: Schema.optional(Schema.Number),
|
|
45
|
+
metadata: Schema.Struct({
|
|
46
|
+
source: Schema.String,
|
|
47
|
+
timestamp: Schema.Number,
|
|
48
|
+
projectID: Schema.optional(Schema.String),
|
|
49
|
+
tags: Schema.mutable(Schema.Array(Schema.String)),
|
|
50
|
+
}),
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
const IngestResponse = Schema.Struct({
|
|
54
|
+
id: Schema.String,
|
|
55
|
+
success: Schema.Boolean,
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
const SearchResponse = Schema.Struct({
|
|
59
|
+
memories: Schema.mutable(Schema.Array(MemoryEntry)),
|
|
60
|
+
count: Schema.Number,
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
const DeleteResponse = Schema.Struct({
|
|
64
|
+
removed: Schema.Number,
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
export const LTMHttpApi = HttpApi.make("ltm")
|
|
68
|
+
.add(
|
|
69
|
+
HttpApiGroup.make(root, "ltm")
|
|
70
|
+
.add(
|
|
71
|
+
HttpApiEndpoint.post("ingest", `${root}/ingest`)
|
|
72
|
+
.setPayload(IngestPayload)
|
|
73
|
+
.addSuccess(IngestResponse)
|
|
74
|
+
.annotateMerge(OpenApi.annotations({ identifier: "ltm.ingest", summary: "Store a memory in LTM" })),
|
|
75
|
+
)
|
|
76
|
+
.add(
|
|
77
|
+
HttpApiEndpoint.post("search", `${root}/search`)
|
|
78
|
+
.setPayload(SearchPayload)
|
|
79
|
+
.addSuccess(SearchResponse)
|
|
80
|
+
.annotateMerge(OpenApi.annotations({ identifier: "ltm.search", summary: "Search LTM memories by query" })),
|
|
81
|
+
)
|
|
82
|
+
.add(
|
|
83
|
+
HttpApiEndpoint.post("delete", `${root}/delete`)
|
|
84
|
+
.setPayload(DeletePayload)
|
|
85
|
+
.addSuccess(DeleteResponse)
|
|
86
|
+
.annotateMerge(OpenApi.annotations({ identifier: "ltm.delete", summary: "Delete LTM memories by ID" })),
|
|
87
|
+
)
|
|
88
|
+
.add(
|
|
89
|
+
HttpApiEndpoint.get("status", `${root}/status`)
|
|
90
|
+
.addSuccess(Schema.Struct({ enabled: Schema.Boolean, memoryCount: Schema.Number }))
|
|
91
|
+
.annotateMerge(OpenApi.annotations({ identifier: "ltm.status", summary: "Get LTM status" })),
|
|
92
|
+
),
|
|
93
|
+
)
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* handlers/ltm.ts — LTM HTTP API handlers
|
|
3
|
+
*
|
|
4
|
+
* 외부 앱이 saeeol LTM에 기억을 저장/검색/삭제.
|
|
5
|
+
* 임베딩은 로컬 서버가 처리, 클라이언트는 텍스트만 보냄.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Effect } from "effect"
|
|
9
|
+
import * as Log from "@saeeol/core/util/log"
|
|
10
|
+
import * as HttpApiBuilder from "effect/unstable/httpapi"
|
|
11
|
+
import * as LTM from "@/ltm"
|
|
12
|
+
import * as Embedder from "@/provider/local/embedder"
|
|
13
|
+
import { LTMHttpApi } from "../groups/ltm"
|
|
14
|
+
|
|
15
|
+
const log = Log.create({ service: "server/handlers/ltm" })
|
|
16
|
+
|
|
17
|
+
export const ltmHandlers = HttpApiBuilder.group(LTMHttpApi, "ltm", (handlers) =>
|
|
18
|
+
Effect.gen(function* () {
|
|
19
|
+
const config = LTM.Config.DEFAULT_LTM_CONFIG
|
|
20
|
+
|
|
21
|
+
return handlers
|
|
22
|
+
.handle("ingest", ({ payload }) =>
|
|
23
|
+
Effect.gen(function* () {
|
|
24
|
+
if (!config.enabled) {
|
|
25
|
+
yield* log.info("LTM ingest skipped — disabled")
|
|
26
|
+
return HttpApiBuilder.fail({ status: 400 as const, body: { message: "LTM is disabled" } })
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const embedderStatus = Embedder.status()
|
|
30
|
+
if (!embedderStatus || embedderStatus.status !== "running") {
|
|
31
|
+
yield* log.info("LTM ingest skipped — embedder not running")
|
|
32
|
+
return HttpApiBuilder.fail({ status: 503 as const, body: { message: "Embedding server not running" } })
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const vector = yield* Effect.tryPromise(() => Embedder.embedOne(payload.summary))
|
|
36
|
+
const id = `${payload.type[0]}:${payload.projectID ?? "global"}:${Date.now()}`
|
|
37
|
+
const memory: LTM.Memory = {
|
|
38
|
+
id,
|
|
39
|
+
type: payload.type,
|
|
40
|
+
content: payload.content.slice(0, 2000),
|
|
41
|
+
summary: payload.summary,
|
|
42
|
+
vector,
|
|
43
|
+
metadata: {
|
|
44
|
+
source: `external:${payload.projectID ?? "unknown"}`,
|
|
45
|
+
timestamp: Date.now(),
|
|
46
|
+
projectID: payload.projectID,
|
|
47
|
+
sessionID: payload.sessionID,
|
|
48
|
+
tags: payload.tags ?? [],
|
|
49
|
+
},
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
yield* Effect.tryPromise(() => LTM.Store.upsert(memory))
|
|
53
|
+
yield* log.info("LTM ingest", { id, type: payload.type })
|
|
54
|
+
|
|
55
|
+
return { id, success: true }
|
|
56
|
+
}),
|
|
57
|
+
)
|
|
58
|
+
.handle("search", ({ payload }) =>
|
|
59
|
+
Effect.gen(function* () {
|
|
60
|
+
if (!config.enabled) {
|
|
61
|
+
return { memories: [], count: 0 }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const embedderStatus = Embedder.status()
|
|
65
|
+
if (!embedderStatus || embedderStatus.status !== "running") {
|
|
66
|
+
return { memories: [], count: 0 }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const vector = yield* Effect.tryPromise(() => Embedder.embedOne(payload.query))
|
|
70
|
+
const memories = yield* Effect.tryPromise(() =>
|
|
71
|
+
LTM.Store.search(vector, {
|
|
72
|
+
topK: payload.topK ?? config.retrieval.topK,
|
|
73
|
+
minScore: payload.minScore ?? config.retrieval.minScore,
|
|
74
|
+
type: payload.type,
|
|
75
|
+
}),
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
// projectID 필터 (store 레벨에서 안 되면 여기서)
|
|
79
|
+
const filtered = payload.projectID
|
|
80
|
+
? memories.filter((m) => m.metadata.projectID === payload.projectID)
|
|
81
|
+
: memories
|
|
82
|
+
|
|
83
|
+
// vector는 응답에서 제거 (용량 절약)
|
|
84
|
+
const lean = filtered.map((m) => ({
|
|
85
|
+
id: m.id,
|
|
86
|
+
type: m.type,
|
|
87
|
+
summary: m.summary,
|
|
88
|
+
content: m.content,
|
|
89
|
+
score: m.score,
|
|
90
|
+
metadata: {
|
|
91
|
+
source: m.metadata.source,
|
|
92
|
+
timestamp: m.metadata.timestamp,
|
|
93
|
+
projectID: m.metadata.projectID,
|
|
94
|
+
tags: m.metadata.tags,
|
|
95
|
+
},
|
|
96
|
+
}))
|
|
97
|
+
|
|
98
|
+
return { memories: lean, count: lean.length }
|
|
99
|
+
}),
|
|
100
|
+
)
|
|
101
|
+
.handle("delete", ({ payload }) =>
|
|
102
|
+
Effect.gen(function* () {
|
|
103
|
+
yield* Effect.tryPromise(() => LTM.Store.remove(payload.ids))
|
|
104
|
+
return { removed: payload.ids.length }
|
|
105
|
+
}),
|
|
106
|
+
)
|
|
107
|
+
.handle("status", () =>
|
|
108
|
+
Effect.gen(function* () {
|
|
109
|
+
const embedder = Embedder.status()
|
|
110
|
+
const count = yield* Effect.tryPromise(() => LTM.Store.count())
|
|
111
|
+
return {
|
|
112
|
+
enabled: config.enabled && embedder?.status === "running",
|
|
113
|
+
memoryCount: count,
|
|
114
|
+
}
|
|
115
|
+
}),
|
|
116
|
+
)
|
|
117
|
+
}),
|
|
118
|
+
)
|
|
@@ -27,7 +27,9 @@ import { EventRoutes } from "./event"
|
|
|
27
27
|
import { SyncRoutes } from "./sync"
|
|
28
28
|
import { InstanceMiddleware } from "./middleware"
|
|
29
29
|
import { jsonRequest } from "./trace"
|
|
30
|
-
import { register as registerSaeeolRoutes } from "@/saeeol/server/instance"
|
|
30
|
+
import { register as registerSaeeolRoutes } from "@/saeeol/server/instance"
|
|
31
|
+
import * as LTM from "@/ltm"
|
|
32
|
+
import * as Embedder from "@/provider/local/embedder"
|
|
31
33
|
|
|
32
34
|
export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono => {
|
|
33
35
|
const app = new Hono()
|
|
@@ -47,6 +49,99 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono => {
|
|
|
47
49
|
.route("/", EventRoutes())
|
|
48
50
|
.route("/mcp", McpRoutes())
|
|
49
51
|
.route("/tui", TuiRoutes())
|
|
52
|
+
// ── LTM (Long-Term Memory) ──
|
|
53
|
+
.post(
|
|
54
|
+
"/ltm/ingest",
|
|
55
|
+
describeRoute({
|
|
56
|
+
summary: "Store a memory in LTM",
|
|
57
|
+
operationId: "ltm.ingest",
|
|
58
|
+
responses: { 200: { description: "Memory stored" } },
|
|
59
|
+
}),
|
|
60
|
+
async (c) => {
|
|
61
|
+
const body = await c.req.json<{ type: "episodic" | "semantic" | "procedural"; content: string; summary: string; projectID?: string; sessionID?: string; tags?: string[] }>()
|
|
62
|
+
const embedderStatus = Embedder.status()
|
|
63
|
+
if (!embedderStatus || embedderStatus.status !== "running") {
|
|
64
|
+
return c.json({ error: "Embedding server not running" }, 503)
|
|
65
|
+
}
|
|
66
|
+
const vector = await Embedder.embedOne(body.summary)
|
|
67
|
+
const id = `${body.type[0]}:${body.projectID ?? "global"}:${Date.now()}`
|
|
68
|
+
const memory: LTM.Memory = {
|
|
69
|
+
id,
|
|
70
|
+
type: body.type,
|
|
71
|
+
content: body.content.slice(0, 2000),
|
|
72
|
+
summary: body.summary,
|
|
73
|
+
vector,
|
|
74
|
+
metadata: {
|
|
75
|
+
source: `external:${body.projectID ?? "unknown"}`,
|
|
76
|
+
timestamp: Date.now(),
|
|
77
|
+
projectID: body.projectID,
|
|
78
|
+
sessionID: body.sessionID,
|
|
79
|
+
tags: body.tags ?? [],
|
|
80
|
+
},
|
|
81
|
+
}
|
|
82
|
+
await LTM.Store.upsert(memory)
|
|
83
|
+
return c.json({ id, success: true })
|
|
84
|
+
},
|
|
85
|
+
)
|
|
86
|
+
.post(
|
|
87
|
+
"/ltm/search",
|
|
88
|
+
describeRoute({
|
|
89
|
+
summary: "Search LTM memories by query",
|
|
90
|
+
operationId: "ltm.search",
|
|
91
|
+
responses: { 200: { description: "Search results" } },
|
|
92
|
+
}),
|
|
93
|
+
async (c) => {
|
|
94
|
+
const body = await c.req.json<{ query: string; topK?: number; minScore?: number; type?: "episodic" | "semantic" | "procedural"; projectID?: string }>()
|
|
95
|
+
const embedderStatus = Embedder.status()
|
|
96
|
+
if (!embedderStatus || embedderStatus.status !== "running") {
|
|
97
|
+
return c.json({ memories: [], count: 0 })
|
|
98
|
+
}
|
|
99
|
+
const vector = await Embedder.embedOne(body.query)
|
|
100
|
+
const memories = await LTM.Store.search(vector, {
|
|
101
|
+
topK: body.topK ?? 5,
|
|
102
|
+
minScore: body.minScore ?? 0.7,
|
|
103
|
+
type: body.type,
|
|
104
|
+
})
|
|
105
|
+
const filtered = body.projectID
|
|
106
|
+
? memories.filter((m) => m.metadata.projectID === body.projectID)
|
|
107
|
+
: memories
|
|
108
|
+
const lean = filtered.map((m) => ({
|
|
109
|
+
id: m.id,
|
|
110
|
+
type: m.type,
|
|
111
|
+
summary: m.summary,
|
|
112
|
+
content: m.content,
|
|
113
|
+
score: m.score,
|
|
114
|
+
metadata: { source: m.metadata.source, timestamp: m.metadata.timestamp, projectID: m.metadata.projectID, tags: m.metadata.tags },
|
|
115
|
+
}))
|
|
116
|
+
return c.json({ memories: lean, count: lean.length })
|
|
117
|
+
},
|
|
118
|
+
)
|
|
119
|
+
.post(
|
|
120
|
+
"/ltm/delete",
|
|
121
|
+
describeRoute({
|
|
122
|
+
summary: "Delete LTM memories by ID",
|
|
123
|
+
operationId: "ltm.delete",
|
|
124
|
+
responses: { 200: { description: "Deleted" } },
|
|
125
|
+
}),
|
|
126
|
+
async (c) => {
|
|
127
|
+
const body = await c.req.json<{ ids: string[] }>()
|
|
128
|
+
await LTM.Store.remove(body.ids)
|
|
129
|
+
return c.json({ removed: body.ids.length })
|
|
130
|
+
},
|
|
131
|
+
)
|
|
132
|
+
.get(
|
|
133
|
+
"/ltm/status",
|
|
134
|
+
describeRoute({
|
|
135
|
+
summary: "Get LTM status",
|
|
136
|
+
operationId: "ltm.status",
|
|
137
|
+
responses: { 200: { description: "LTM status" } },
|
|
138
|
+
}),
|
|
139
|
+
async (c) => {
|
|
140
|
+
const embedder = Embedder.status()
|
|
141
|
+
const count = await LTM.Store.count()
|
|
142
|
+
return c.json({ enabled: embedder?.status === "running", memoryCount: count })
|
|
143
|
+
},
|
|
144
|
+
)
|
|
50
145
|
.post(
|
|
51
146
|
"/instance/dispose",
|
|
52
147
|
describeRoute({
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
[?9001h[?1004h[?25l[2J[m[H]0;● 새얼[?25h
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
[?9001h[?1004h[?25l[?25h
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
=== input-typed (280 bytes) ===
|
|
2
|
+
s service=default e=Export named 'jsx' not found in module 'C:\Users\PC\Desktop\SAEEOL\node_modules\.bun\@opentui+solid@0.2.2+37848aaf691d3d45\node_modules\@opentui\solid\jsx-runtime.d.ts'. exception
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
=== input-cursor-lr (280 bytes) ===
|
|
6
|
+
s service=default e=Export named 'jsx' not found in module 'C:\Users\PC\Desktop\SAEEOL\node_modules\.bun\@opentui+solid@0.2.2+37848aaf691d3d45\node_modules\@opentui\solid\jsx-runtime.d.ts'. exception
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
=== input-cursor-ud (280 bytes) ===
|
|
10
|
+
s service=default e=Export named 'jsx' not found in module 'C:\Users\PC\Desktop\SAEEOL\node_modules\.bun\@opentui+solid@0.2.2+37848aaf691d3d45\node_modules\@opentui\solid\jsx-runtime.d.ts'. exception
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
=== input-cursor-home-end (280 bytes) ===
|
|
14
|
+
s service=default e=Export named 'jsx' not found in module 'C:\Users\PC\Desktop\SAEEOL\node_modules\.bun\@opentui+solid@0.2.2+37848aaf691d3d45\node_modules\@opentui\solid\jsx-runtime.d.ts'. exception
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
=== input-ctrl-a (280 bytes) ===
|
|
18
|
+
s service=default e=Export named 'jsx' not found in module 'C:\Users\PC\Desktop\SAEEOL\node_modules\.bun\@opentui+solid@0.2.2+37848aaf691d3d45\node_modules\@opentui\solid\jsx-runtime.d.ts'. exception
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
=== input-ctrl-e (280 bytes) ===
|
|
22
|
+
s service=default e=Export named 'jsx' not found in module 'C:\Users\PC\Desktop\SAEEOL\node_modules\.bun\@opentui+solid@0.2.2+37848aaf691d3d45\node_modules\@opentui\solid\jsx-runtime.d.ts'. exception
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
=== input-ctrl-b (280 bytes) ===
|
|
26
|
+
s service=default e=Export named 'jsx' not found in module 'C:\Users\PC\Desktop\SAEEOL\node_modules\.bun\@opentui+solid@0.2.2+37848aaf691d3d45\node_modules\@opentui\solid\jsx-runtime.d.ts'. exception
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
=== input-ctrl-f (280 bytes) ===
|
|
30
|
+
s service=default e=Export named 'jsx' not found in module 'C:\Users\PC\Desktop\SAEEOL\node_modules\.bun\@opentui+solid@0.2.2+37848aaf691d3d45\node_modules\@opentui\solid\jsx-runtime.d.ts'. exception
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
=== input-alt-f (280 bytes) ===
|
|
34
|
+
s service=default e=Export named 'jsx' not found in module 'C:\Users\PC\Desktop\SAEEOL\node_modules\.bun\@opentui+solid@0.2.2+37848aaf691d3d45\node_modules\@opentui\solid\jsx-runtime.d.ts'. exception
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
=== input-alt-b (280 bytes) ===
|
|
38
|
+
s service=default e=Export named 'jsx' not found in module 'C:\Users\PC\Desktop\SAEEOL\node_modules\.bun\@opentui+solid@0.2.2+37848aaf691d3d45\node_modules\@opentui\solid\jsx-runtime.d.ts'. exception
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
=== input-ctrl-left (280 bytes) ===
|
|
42
|
+
s service=default e=Export named 'jsx' not found in module 'C:\Users\PC\Desktop\SAEEOL\node_modules\.bun\@opentui+solid@0.2.2+37848aaf691d3d45\node_modules\@opentui\solid\jsx-runtime.d.ts'. exception
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
=== input-ctrl-right (280 bytes) ===
|
|
46
|
+
s service=default e=Export named 'jsx' not found in module 'C:\Users\PC\Desktop\SAEEOL\node_modules\.bun\@opentui+solid@0.2.2+37848aaf691d3d45\node_modules\@opentui\solid\jsx-runtime.d.ts'. exception
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
=== input-select-lr (280 bytes) ===
|
|
50
|
+
s service=default e=Export named 'jsx' not found in module 'C:\Users\PC\Desktop\SAEEOL\node_modules\.bun\@opentui+solid@0.2.2+37848aaf691d3d45\node_modules\@opentui\solid\jsx-runtime.d.ts'. exception
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
=== input-select-ud (280 bytes) ===
|
|
54
|
+
s service=default e=Export named 'jsx' not found in module 'C:\Users\PC\Desktop\SAEEOL\node_modules\.bun\@opentui+solid@0.2.2+37848aaf691d3d45\node_modules\@opentui\solid\jsx-runtime.d.ts'. exception
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
=== input-alt-a (280 bytes) ===
|
|
58
|
+
s service=default e=Export named 'jsx' not found in module 'C:\Users\PC\Desktop\SAEEOL\node_modules\.bun\@opentui+solid@0.2.2+37848aaf691d3d45\node_modules\@opentui\solid\jsx-runtime.d.ts'. exception
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
=== input-alt-e (280 bytes) ===
|
|
62
|
+
s service=default e=Export named 'jsx' not found in module 'C:\Users\PC\Desktop\SAEEOL\node_modules\.bun\@opentui+solid@0.2.2+37848aaf691d3d45\node_modules\@opentui\solid\jsx-runtime.d.ts'. exception
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
=== input-backspace (280 bytes) ===
|
|
66
|
+
s service=default e=Export named 'jsx' not found in module 'C:\Users\PC\Desktop\SAEEOL\node_modules\.bun\@opentui+solid@0.2.2+37848aaf691d3d45\node_modules\@opentui\solid\jsx-runtime.d.ts'. exception
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
=== input-delete (280 bytes) ===
|
|
70
|
+
s service=default e=Export named 'jsx' not found in module 'C:\Users\PC\Desktop\SAEEOL\node_modules\.bun\@opentui+solid@0.2.2+37848aaf691d3d45\node_modules\@opentui\solid\jsx-runtime.d.ts'. exception
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
=== input-ctrl-k (280 bytes) ===
|
|
74
|
+
s service=default e=Export named 'jsx' not found in module 'C:\Users\PC\Desktop\SAEEOL\node_modules\.bun\@opentui+solid@0.2.2+37848aaf691d3d45\node_modules\@opentui\solid\jsx-runtime.d.ts'. exception
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
=== input-ctrl-u (280 bytes) ===
|
|
78
|
+
s service=default e=Export named 'jsx' not found in module 'C:\Users\PC\Desktop\SAEEOL\node_modules\.bun\@opentui+solid@0.2.2+37848aaf691d3d45\node_modules\@opentui\solid\jsx-runtime.d.ts'. exception
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
=== input-ctrl-w (280 bytes) ===
|
|
82
|
+
s service=default e=Export named 'jsx' not found in module 'C:\Users\PC\Desktop\SAEEOL\node_modules\.bun\@opentui+solid@0.2.2+37848aaf691d3d45\node_modules\@opentui\solid\jsx-runtime.d.ts'. exception
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
=== input-ctrl-d (280 bytes) ===
|
|
86
|
+
s service=default e=Export named 'jsx' not found in module 'C:\Users\PC\Desktop\SAEEOL\node_modules\.bun\@opentui+solid@0.2.2+37848aaf691d3d45\node_modules\@opentui\solid\jsx-runtime.d.ts'. exception
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
=== input-alt-d (280 bytes) ===
|
|
90
|
+
s service=default e=Export named 'jsx' not found in module 'C:\Users\PC\Desktop\SAEEOL\node_modules\.bun\@opentui+solid@0.2.2+37848aaf691d3d45\node_modules\@opentui\solid\jsx-runtime.d.ts'. exception
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
=== input-clear-ctrl-c (280 bytes) ===
|
|
94
|
+
s service=default e=Export named 'jsx' not found in module 'C:\Users\PC\Desktop\SAEEOL\node_modules\.bun\@opentui+solid@0.2.2+37848aaf691d3d45\node_modules\@opentui\solid\jsx-runtime.d.ts'. exception
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
=== input-undo-ctrl-minus (280 bytes) ===
|
|
98
|
+
s service=default e=Export named 'jsx' not found in module 'C:\Users\PC\Desktop\SAEEOL\node_modules\.bun\@opentui+solid@0.2.2+37848aaf691d3d45\node_modules\@opentui\solid\jsx-runtime.d.ts'. exception
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
=== input-redo (280 bytes) ===
|
|
102
|
+
s service=default e=Export named 'jsx' not found in module 'C:\Users\PC\Desktop\SAEEOL\node_modules\.bun\@opentui+solid@0.2.2+37848aaf691d3d45\node_modules\@opentui\solid\jsx-runtime.d.ts'. exception
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
=== input-submit (280 bytes) ===
|
|
106
|
+
s service=default e=Export named 'jsx' not found in module 'C:\Users\PC\Desktop\SAEEOL\node_modules\.bun\@opentui+solid@0.2.2+37848aaf691d3d45\node_modules\@opentui\solid\jsx-runtime.d.ts'. exception
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
=== input-cleanup (280 bytes) ===
|
|
110
|
+
s service=default e=Export named 'jsx' not found in module 'C:\Users\PC\Desktop\SAEEOL\node_modules\.bun\@opentui+solid@0.2.2+37848aaf691d3d45\node_modules\@opentui\solid\jsx-runtime.d.ts'. exception
|