saeeol 1.0.9 → 1.1.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/npm/bin/saeeol +42 -0
- package/npm/package.json +39 -0
- package/npm/postinstall.js +162 -0
- package/package.json +2 -2
- package/src/cli/cmd/mcp-refresh.ts +47 -0
- package/src/cli/cmd/mcp.ts +3 -1
- package/src/cli/cmd/tui/app-commands-core.tsx +11 -0
- package/src/cli/cmd/tui/app-commands-system.tsx +20 -0
- package/src/cli/cmd/tui/app-events.ts +43 -0
- package/src/cli/cmd/tui/app.tsx +4 -0
- package/src/cli/cmd/tui/component/dialog-model.tsx +2 -2
- package/src/cli/cmd/tui/component/prompt/use-prompt-memos.ts +1 -1
- package/src/cli/cmd/tui/component/use-connected.tsx +1 -1
- package/src/cli/cmd/tui/context/local.tsx +10 -3
- package/src/cli/cmd/tui/context/route.tsx +5 -1
- package/src/cli/cmd/tui/feature-plugins/sidebar/context.tsx +1 -1
- package/src/cli/cmd/tui/plugin/api.tsx +7 -3
- package/src/cli/cmd/tui/routes/local-models.tsx +151 -0
- package/src/cli/cmd/tui/routes/session/subagent-footer.tsx +1 -1
- package/src/cli/cmd/tui/util/model.ts +1 -1
- package/src/config/config-schema.ts +44 -0
- package/src/ltm/config.ts +124 -0
- package/src/ltm/events.ts +50 -0
- package/src/ltm/index.ts +12 -0
- package/src/ltm/memory/episodic.ts +83 -0
- package/src/ltm/memory/procedural.ts +102 -0
- package/src/ltm/memory/semantic.ts +80 -0
- package/src/ltm/pipeline.ts +155 -0
- package/src/ltm/retrieval.ts +62 -0
- package/src/ltm/scheduler.ts +55 -0
- package/src/ltm/store.ts +150 -0
- package/src/ltm/types.ts +108 -0
- package/src/mcp/index.ts +32 -1
- package/src/provider/custom-loaders.ts +12 -0
- package/src/provider/loader-local.ts +185 -0
- package/src/provider/local/embedder.ts +220 -0
- package/src/provider/local/events.ts +74 -0
- package/src/provider/local/gpu.ts +93 -0
- package/src/provider/local/hub.ts +174 -0
- package/src/provider/local/index.ts +10 -0
- package/src/provider/local/model-manager.ts +113 -0
- package/src/provider/local/orchestrator.ts +301 -0
- package/src/provider/local/rag.ts +112 -0
- package/src/provider/local/types.ts +142 -0
- package/src/provider/provider-conversion.ts +2 -0
- package/src/provider/provider-schema.ts +17 -2
- package/src/provider/provider-schemas.ts +10 -3
- package/src/provider/provider-state.ts +10 -2
- package/src/provider/provider.ts +2 -1
- package/src/saeeol/plugins/sidebar-usage.tsx +1 -1
- package/src/server/routes/instance/config.ts +1 -1
- package/src/server/routes/instance/httpapi/api.ts +2 -0
- package/src/server/routes/instance/httpapi/groups/local.ts +87 -0
- package/src/server/routes/instance/httpapi/groups/mcp.ts +10 -0
- package/src/server/routes/instance/httpapi/handlers/local.ts +95 -0
- package/src/server/routes/instance/httpapi/handlers/mcp.ts +5 -0
- package/src/server/routes/instance/httpapi/handlers/provider.ts +1 -1
- package/src/server/routes/instance/httpapi/server.ts +2 -0
- package/src/server/routes/instance/provider.ts +2 -2
- package/src/session/prompt-reminders.ts +29 -0
- package/test/fake/provider.ts +1 -0
- package/test/provider/local.test.ts +208 -0
- package/test/provider/provider-category.test.ts +190 -0
package/npm/bin/saeeol
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// saeeol bin wrapper — delegates to the downloaded platform binary
|
|
4
|
+
const { existsSync, chmodSync } = require("fs")
|
|
5
|
+
const { join, resolve } = require("path")
|
|
6
|
+
const { spawn } = require("child_process")
|
|
7
|
+
|
|
8
|
+
const binDir = join(__dirname, "..", "download")
|
|
9
|
+
function getBinaryName() {
|
|
10
|
+
const archMap = { x64: "x64", arm64: "arm64" }
|
|
11
|
+
const platformMap = { win32: "windows", darwin: "darwin", linux: "linux" }
|
|
12
|
+
const p = platformMap[process.platform] || process.platform
|
|
13
|
+
const a = archMap[process.arch] || arch
|
|
14
|
+
const ext = process.platform === "win32" ? ".exe" : ""
|
|
15
|
+
return `saeeol-${p}-${a}${ext}`
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const binaryName = getBinaryName()
|
|
19
|
+
const binaryPath = join(binDir, binaryName)
|
|
20
|
+
|
|
21
|
+
if (!existsSync(binaryPath)) {
|
|
22
|
+
console.error(`saeeol binary not found: ${binaryPath}`)
|
|
23
|
+
console.error(`Run: npm rebuild saeeol`)
|
|
24
|
+
console.error(`Or: https://github.com/byfabulist/saeeol/releases`)
|
|
25
|
+
process.exit(1)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (process.platform !== "win32") {
|
|
29
|
+
try { chmodSync(binaryPath, 0o755) } catch {}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const child = spawn(binaryPath, process.argv.slice(2), {
|
|
33
|
+
stdio: "inherit",
|
|
34
|
+
env: { ...process.env },
|
|
35
|
+
windowsHide: true,
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
child.on("exit", (code) => process.exit(code || 0))
|
|
39
|
+
child.on("error", (err) => {
|
|
40
|
+
console.error(`Failed to start saeeol: ${err.message}`)
|
|
41
|
+
process.exit(1)
|
|
42
|
+
})
|
package/npm/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "saeeol",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "AI agent engine for SAEEOL",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/byfabulist/saeeol"
|
|
9
|
+
},
|
|
10
|
+
"bin": {
|
|
11
|
+
"saeeol": "./bin/saeeol"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"bin/",
|
|
15
|
+
"postinstall.js"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"postinstall": "node postinstall.js"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {},
|
|
21
|
+
"devDependencies": {},
|
|
22
|
+
"peerDependencies": {},
|
|
23
|
+
"overrides": {},
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public",
|
|
26
|
+
"registry": "https://registry.npmjs.org"
|
|
27
|
+
},
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=18"
|
|
30
|
+
},
|
|
31
|
+
"keywords": [
|
|
32
|
+
"ai",
|
|
33
|
+
"agent",
|
|
34
|
+
"cli",
|
|
35
|
+
"coding",
|
|
36
|
+
"assistant",
|
|
37
|
+
"saeeol"
|
|
38
|
+
]
|
|
39
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// saeeol postinstall — downloads platform binary from GitHub Releases
|
|
4
|
+
const https = require("https")
|
|
5
|
+
const http = require("http")
|
|
6
|
+
const fs = require("fs")
|
|
7
|
+
const path = require("path")
|
|
8
|
+
const zlib = require("zlib")
|
|
9
|
+
const { execSync } = require("child_process")
|
|
10
|
+
|
|
11
|
+
const REPO = "byfabulist/saeeol"
|
|
12
|
+
const VERSION = require("./package.json").version
|
|
13
|
+
|
|
14
|
+
function getPlatformInfo() {
|
|
15
|
+
const archMap = { x64: "x64", arm64: "arm64" }
|
|
16
|
+
const platformMap = { win32: "windows", darwin: "darwin", linux: "linux" }
|
|
17
|
+
const p = platformMap[process.platform] || process.platform
|
|
18
|
+
const a = archMap[process.arch] || process.arch
|
|
19
|
+
const ext = process.platform === "win32" ? ".zip" : ".tar.gz"
|
|
20
|
+
const archiveName = `saeeol-${p}-${a}${ext}`
|
|
21
|
+
const binaryName = process.platform === "win32" ? "saeeol.exe" : "saeeol"
|
|
22
|
+
return { archiveName, binaryName, platform: p, arch: a }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function fetch(url, redirects = 5) {
|
|
26
|
+
return new Promise((resolve, reject) => {
|
|
27
|
+
const mod = url.startsWith("https") ? https : http
|
|
28
|
+
mod.get(url, { timeout: 60000 }, (res) => {
|
|
29
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
30
|
+
if (redirects <= 0) return reject(new Error("Too many redirects"))
|
|
31
|
+
return fetch(res.headers.location, redirects - 1).then(resolve, reject)
|
|
32
|
+
}
|
|
33
|
+
if (res.statusCode !== 200) {
|
|
34
|
+
return reject(new Error(`HTTP ${res.statusCode} for ${url}`))
|
|
35
|
+
}
|
|
36
|
+
const chunks = []
|
|
37
|
+
res.on("data", (chunk) => chunks.push(chunk))
|
|
38
|
+
res.on("end", () => resolve(Buffer.concat(chunks)))
|
|
39
|
+
res.on("error", reject)
|
|
40
|
+
}).on("error", reject)
|
|
41
|
+
})
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function extractTarGz(buffer, destDir, binaryName) {
|
|
45
|
+
// Use tar on non-windows, or platform tar on windows (git bash)
|
|
46
|
+
const tmpFile = path.join(destDir, "download.tar.gz")
|
|
47
|
+
fs.writeFileSync(tmpFile, buffer)
|
|
48
|
+
try {
|
|
49
|
+
if (process.platform === "win32") {
|
|
50
|
+
// Try PowerShell Expand-Archive for .tar.gz or just use tar if available
|
|
51
|
+
try {
|
|
52
|
+
execSync(`tar -xzf "${tmpFile}" -C "${destDir}"`, { stdio: "pipe", windowsHide: true })
|
|
53
|
+
} catch {
|
|
54
|
+
// Fallback: use zlib + tar parsing manually not practical, try powershell
|
|
55
|
+
throw new Error("tar command not available on Windows. Install Git Bash or add tar to PATH.")
|
|
56
|
+
}
|
|
57
|
+
} else {
|
|
58
|
+
execSync(`tar -xzf "${tmpFile}" -C "${destDir}"`, { stdio: "pipe" })
|
|
59
|
+
}
|
|
60
|
+
} finally {
|
|
61
|
+
try { fs.unlinkSync(tmpFile) } catch {}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function extractZip(buffer, destDir, binaryName) {
|
|
66
|
+
if (process.platform === "win32") {
|
|
67
|
+
const tmpFile = path.join(destDir, "download.zip")
|
|
68
|
+
fs.writeFileSync(tmpFile, buffer)
|
|
69
|
+
try {
|
|
70
|
+
execSync(`powershell -Command "Expand-Archive -Path '${tmpFile}' -DestinationPath '${destDir}' -Force"`, {
|
|
71
|
+
stdio: "pipe",
|
|
72
|
+
windowsHide: true,
|
|
73
|
+
})
|
|
74
|
+
} finally {
|
|
75
|
+
try { fs.unlinkSync(tmpFile) } catch {}
|
|
76
|
+
}
|
|
77
|
+
} else {
|
|
78
|
+
// On non-windows, try unzip
|
|
79
|
+
const tmpFile = path.join(destDir, "download.zip")
|
|
80
|
+
fs.writeFileSync(tmpFile, buffer)
|
|
81
|
+
try {
|
|
82
|
+
execSync(`unzip -o "${tmpFile}" -d "${destDir}"`, { stdio: "pipe" })
|
|
83
|
+
} finally {
|
|
84
|
+
try { fs.unlinkSync(tmpFile) } catch {}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function main() {
|
|
90
|
+
const { archiveName, binaryName, platform: p, arch: a } = getPlatformInfo()
|
|
91
|
+
const binDir = path.join(__dirname, "download")
|
|
92
|
+
|
|
93
|
+
// Check if binary already exists
|
|
94
|
+
const binaryPath = path.join(binDir, binaryName)
|
|
95
|
+
if (fs.existsSync(binaryPath)) {
|
|
96
|
+
console.log(`saeeol binary already exists at ${binaryPath}`)
|
|
97
|
+
return
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
fs.mkdirSync(binDir, { recursive: true })
|
|
101
|
+
|
|
102
|
+
const url = `https://github.com/${REPO}/releases/download/v${VERSION}/${archiveName}`
|
|
103
|
+
console.log(`Downloading saeeol v${VERSION} for ${p}-${a}...`)
|
|
104
|
+
console.log(` ${url}`)
|
|
105
|
+
|
|
106
|
+
let buffer
|
|
107
|
+
try {
|
|
108
|
+
buffer = await fetch(url)
|
|
109
|
+
} catch (err) {
|
|
110
|
+
// Fallback: try without version tag (latest)
|
|
111
|
+
const fallbackUrl = `https://github.com/${REPO}/releases/latest/download/${archiveName}`
|
|
112
|
+
console.log(`Version-specific download failed: ${err.message}`)
|
|
113
|
+
console.log(`Trying latest: ${fallbackUrl}`)
|
|
114
|
+
try {
|
|
115
|
+
buffer = await fetch(fallbackUrl)
|
|
116
|
+
} catch (err2) {
|
|
117
|
+
console.error(`Failed to download saeeol binary: ${err2.message}`)
|
|
118
|
+
console.error(`Please download manually from: https://github.com/${REPO}/releases`)
|
|
119
|
+
process.exit(0) // Don't fail npm install
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
console.log(`Downloaded ${(buffer.length / 1024 / 1024).toFixed(1)} MB`)
|
|
124
|
+
|
|
125
|
+
// Extract
|
|
126
|
+
if (archiveName.endsWith(".tar.gz")) {
|
|
127
|
+
await extractTarGz(buffer, binDir, binaryName)
|
|
128
|
+
} else {
|
|
129
|
+
await extractZip(buffer, binDir, binaryName)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Verify
|
|
133
|
+
if (!fs.existsSync(binaryPath)) {
|
|
134
|
+
// The archive might extract to a subdirectory, search for the binary
|
|
135
|
+
const files = fs.readdirSync(binDir, { recursive: true })
|
|
136
|
+
const found = files.find((f) => {
|
|
137
|
+
const basename = path.basename(f)
|
|
138
|
+
return basename === "saeeol" || basename === "saeeol.exe"
|
|
139
|
+
})
|
|
140
|
+
if (found) {
|
|
141
|
+
const fullPath = path.join(binDir, found)
|
|
142
|
+
fs.copyFileSync(fullPath, binaryPath)
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (fs.existsSync(binaryPath)) {
|
|
147
|
+
if (process.platform !== "win32") {
|
|
148
|
+
fs.chmodSync(binaryPath, 0o755)
|
|
149
|
+
}
|
|
150
|
+
console.log(`saeeol v${VERSION} installed successfully!`)
|
|
151
|
+
console.log(` Binary: ${binaryPath}`)
|
|
152
|
+
} else {
|
|
153
|
+
console.warn(`Warning: binary not found after extraction at ${binaryPath}`)
|
|
154
|
+
console.warn(`You may need to install manually from https://github.com/${REPO}/releases`)
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
main().catch((err) => {
|
|
159
|
+
console.error(`saeeol postinstall failed: ${err.message}`)
|
|
160
|
+
console.error(`Install manually: https://github.com/${REPO}/releases`)
|
|
161
|
+
process.exit(0) // Don't fail npm install
|
|
162
|
+
})
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"name": "saeeol",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -134,8 +134,8 @@
|
|
|
134
134
|
"@pierre/diffs": "1.1.0-beta.18",
|
|
135
135
|
"@saeeol/boxes": "workspace:*",
|
|
136
136
|
"@saeeol/core": "workspace:*",
|
|
137
|
-
"@saeeol/i18n": "workspace:*",
|
|
138
137
|
"@saeeol/gateway": "workspace:*",
|
|
138
|
+
"@saeeol/i18n": "workspace:*",
|
|
139
139
|
"@saeeol/indexing": "workspace:*",
|
|
140
140
|
"@saeeol/plugin": "workspace:*",
|
|
141
141
|
"@saeeol/script": "workspace:*",
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { cmd } from "./cmd"
|
|
2
|
+
import * as prompts from "@clack/prompts"
|
|
3
|
+
import { UI } from "../ui"
|
|
4
|
+
import { Instance } from "../../project/instance"
|
|
5
|
+
import { configuredServers, listState } from "./mcp-shared"
|
|
6
|
+
import { MCP } from "../../mcp"
|
|
7
|
+
import { AppRuntime } from "../../effect/app-runtime"
|
|
8
|
+
import { Effect } from "effect"
|
|
9
|
+
|
|
10
|
+
export const McpRefreshCommand = cmd({
|
|
11
|
+
command: "refresh",
|
|
12
|
+
aliases: ["rf"],
|
|
13
|
+
describe: "refresh MCP server list from config",
|
|
14
|
+
async handler() {
|
|
15
|
+
await Instance.provide({
|
|
16
|
+
directory: process.cwd(),
|
|
17
|
+
async fn() {
|
|
18
|
+
UI.empty()
|
|
19
|
+
prompts.intro("Refresh MCP Servers")
|
|
20
|
+
|
|
21
|
+
const spinner = prompts.spinner()
|
|
22
|
+
spinner.start("Reconnecting...")
|
|
23
|
+
|
|
24
|
+
const statuses = await AppRuntime.runPromise(
|
|
25
|
+
Effect.gen(function* () {
|
|
26
|
+
const mcp = yield* MCP.Service
|
|
27
|
+
return yield* mcp.refresh()
|
|
28
|
+
}),
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
spinner.stop("Done")
|
|
32
|
+
|
|
33
|
+
const { config } = await listState()
|
|
34
|
+
const servers = configuredServers(config)
|
|
35
|
+
|
|
36
|
+
for (const [name] of servers) {
|
|
37
|
+
const s = statuses[name]
|
|
38
|
+
if (!s) continue
|
|
39
|
+
const icon = s.status === "connected" ? "✓" : s.status === "disabled" ? "○" : "✗"
|
|
40
|
+
prompts.log.info(`${icon} ${name} ${UI.Style.TEXT_DIM}${s.status}`)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
prompts.outro(`${servers.length} server(s) refreshed`)
|
|
44
|
+
},
|
|
45
|
+
})
|
|
46
|
+
},
|
|
47
|
+
})
|
package/src/cli/cmd/mcp.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { McpListCommand } from "./mcp-list"
|
|
|
3
3
|
import { McpAuthCommand, McpAuthListCommand } from "./mcp-auth"
|
|
4
4
|
import { McpLogoutCommand } from "./mcp-logout"
|
|
5
5
|
import { McpAddCommand } from "./mcp-add"
|
|
6
|
+
import { McpRefreshCommand } from "./mcp-refresh"
|
|
6
7
|
import { McpDebugCommand } from "./mcp-debug"
|
|
7
8
|
|
|
8
9
|
export const McpCommand = cmd({
|
|
@@ -12,6 +13,7 @@ export const McpCommand = cmd({
|
|
|
12
13
|
yargs
|
|
13
14
|
.command(McpAddCommand)
|
|
14
15
|
.command(McpListCommand)
|
|
16
|
+
.command(McpRefreshCommand)
|
|
15
17
|
.command(McpAuthCommand)
|
|
16
18
|
.command(McpLogoutCommand)
|
|
17
19
|
.command(McpDebugCommand)
|
|
@@ -19,4 +21,4 @@ export const McpCommand = cmd({
|
|
|
19
21
|
async handler() {},
|
|
20
22
|
})
|
|
21
23
|
|
|
22
|
-
export { McpListCommand, McpAuthCommand, McpAuthListCommand, McpLogoutCommand, McpAddCommand, McpDebugCommand }
|
|
24
|
+
export { McpListCommand, McpAuthCommand, McpAuthListCommand, McpLogoutCommand, McpAddCommand, McpRefreshCommand, McpDebugCommand }
|
|
@@ -132,6 +132,17 @@ export function registerCoreCommands(deps: CoreCommandDeps): CommandOption[] {
|
|
|
132
132
|
dialog.replace(() => <DialogMcp />)
|
|
133
133
|
},
|
|
134
134
|
},
|
|
135
|
+
{
|
|
136
|
+
title: "Refresh MCP Servers",
|
|
137
|
+
value: "mcp.refresh",
|
|
138
|
+
category: t("cmd.cat.agent"),
|
|
139
|
+
slash: {
|
|
140
|
+
name: "mcp-refresh",
|
|
141
|
+
},
|
|
142
|
+
onSelect: async () => {
|
|
143
|
+
await local.mcp.refresh()
|
|
144
|
+
},
|
|
145
|
+
},
|
|
135
146
|
{
|
|
136
147
|
title: t("cmd.agent.cycle"),
|
|
137
148
|
value: "agent.cycle",
|
|
@@ -5,6 +5,7 @@ import open from "open"
|
|
|
5
5
|
import { DialogStatus } from "@tui/component/dialog-status"
|
|
6
6
|
import { DialogThemeList } from "@tui/component/dialog-theme-list"
|
|
7
7
|
import { DialogHelp } from "@tui/ui/dialog-help"
|
|
8
|
+
import * as Log from "@saeeol/core/util/log"
|
|
8
9
|
|
|
9
10
|
type Dialog = ReturnType<typeof import("@tui/ui/dialog").useDialog>
|
|
10
11
|
type KV = ReturnType<typeof import("@tui/context/kv").useKV>
|
|
@@ -111,6 +112,25 @@ export function registerSystemCommands(deps: SystemCommandDeps): CommandOption[]
|
|
|
111
112
|
},
|
|
112
113
|
category: t("cmd.cat.system"),
|
|
113
114
|
},
|
|
115
|
+
{
|
|
116
|
+
title: "Open Log File",
|
|
117
|
+
value: "app.logs",
|
|
118
|
+
slash: {
|
|
119
|
+
name: "logs",
|
|
120
|
+
},
|
|
121
|
+
onSelect: () => {
|
|
122
|
+
const logPath = Log.file()
|
|
123
|
+
if (logPath) {
|
|
124
|
+
open(logPath).catch(() => {
|
|
125
|
+
toast.show({ variant: "error", message: logPath, duration: 10000 })
|
|
126
|
+
})
|
|
127
|
+
} else {
|
|
128
|
+
toast.show({ variant: "error", message: "No log file available", duration: 3000 })
|
|
129
|
+
}
|
|
130
|
+
dialog.clear()
|
|
131
|
+
},
|
|
132
|
+
category: t("cmd.cat.system"),
|
|
133
|
+
},
|
|
114
134
|
{
|
|
115
135
|
title: t("cmd.system.docs"),
|
|
116
136
|
value: "docs.open",
|
|
@@ -5,6 +5,7 @@ import { DialogAlert } from "@tui/ui/dialog-alert"
|
|
|
5
5
|
import { errorMessage } from "@tui/app-config"
|
|
6
6
|
import * as SaeeolApp from "@/saeeol/cli/cmd/tui/app"
|
|
7
7
|
import { ProviderInstallEvent } from "@/provider/provider-events"
|
|
8
|
+
import * as Log from "@saeeol/core/util/log"
|
|
8
9
|
|
|
9
10
|
type Event = ReturnType<typeof import("@tui/context/event").useEvent>
|
|
10
11
|
type Command = ReturnType<typeof import("@tui/component/dialog-command").useCommandDialog>
|
|
@@ -26,6 +27,18 @@ export type EventDeps = {
|
|
|
26
27
|
exit: Exit
|
|
27
28
|
}
|
|
28
29
|
|
|
30
|
+
// Session error auto-recovery state
|
|
31
|
+
const MAX_SESSION_ERRORS = 3
|
|
32
|
+
const ERROR_WINDOW_MS = 60_000 // 1 minute window
|
|
33
|
+
const sessionErrors: { time: number; message: string }[] = []
|
|
34
|
+
|
|
35
|
+
function trimOldErrors() {
|
|
36
|
+
const cutoff = Date.now() - ERROR_WINDOW_MS
|
|
37
|
+
while (sessionErrors.length > 0 && sessionErrors[0].time < cutoff) {
|
|
38
|
+
sessionErrors.shift()
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
29
42
|
export function registerAppEvents(deps: EventDeps) {
|
|
30
43
|
const { event, command, toast, route, kv, dialog, sdk, exit } = deps
|
|
31
44
|
event.on(TuiEvent.CommandExecute.type, (evt) => {
|
|
@@ -59,6 +72,36 @@ export function registerAppEvents(deps: EventDeps) {
|
|
|
59
72
|
if (error && typeof error === "object" && error.name === "MessageAbortedError") return
|
|
60
73
|
if (SaeeolApp.handleSessionError(error, toast)) return
|
|
61
74
|
const message = errorMessage(error)
|
|
75
|
+
|
|
76
|
+
// Track error for auto-recovery
|
|
77
|
+
sessionErrors.push({ time: Date.now(), message })
|
|
78
|
+
trimOldErrors()
|
|
79
|
+
|
|
80
|
+
if (sessionErrors.length >= MAX_SESSION_ERRORS) {
|
|
81
|
+
// Auto-recovery: too many errors, start fresh session
|
|
82
|
+
sessionErrors.length = 0
|
|
83
|
+
const logPath = Log.file()
|
|
84
|
+
toast.show({
|
|
85
|
+
variant: "warning",
|
|
86
|
+
message: `Session unstable (${MAX_SESSION_ERRORS} errors in 1min). Starting new session.`,
|
|
87
|
+
duration: 5000,
|
|
88
|
+
})
|
|
89
|
+
route.navigate({ type: "home" })
|
|
90
|
+
dialog.clear()
|
|
91
|
+
// If still failing after auto-recovery, show log path
|
|
92
|
+
setTimeout(() => {
|
|
93
|
+
if (logPath) {
|
|
94
|
+
toast.show({
|
|
95
|
+
variant: "info",
|
|
96
|
+
title: "Log File",
|
|
97
|
+
message: logPath,
|
|
98
|
+
duration: 10000,
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
}, 6000)
|
|
102
|
+
return
|
|
103
|
+
}
|
|
104
|
+
|
|
62
105
|
toast.show({
|
|
63
106
|
variant: "error",
|
|
64
107
|
message,
|
package/src/cli/cmd/tui/app.tsx
CHANGED
|
@@ -30,6 +30,7 @@ import { CommandProvider, useCommandDialog } from "@tui/component/dialog-command
|
|
|
30
30
|
import { KeybindProvider, useKeybind } from "@tui/context/keybind"
|
|
31
31
|
import { ThemeProvider, useTheme } from "@tui/context/theme"
|
|
32
32
|
import { Home } from "@tui/routes/home"
|
|
33
|
+
import { LocalModels } from "@tui/routes/local-models"
|
|
33
34
|
import { Session } from "@tui/routes/session"
|
|
34
35
|
import { PromptHistoryProvider } from "@tui/component/prompt/history"
|
|
35
36
|
import { FrecencyProvider } from "@tui/component/prompt/frecency"
|
|
@@ -270,6 +271,9 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
|
|
|
270
271
|
<Match when={route.data.type === "saeeolclaw"}>
|
|
271
272
|
<SaeeolApp.SaeeolClawView />
|
|
272
273
|
</Match>
|
|
274
|
+
<Match when={route.data.type === "local-models"}>
|
|
275
|
+
<LocalModels />
|
|
276
|
+
</Match>
|
|
273
277
|
</Switch>
|
|
274
278
|
</Show>
|
|
275
279
|
{plugin()}
|
|
@@ -41,7 +41,7 @@ export function DialogModel(props: { providerID?: string }) {
|
|
|
41
41
|
|
|
42
42
|
const lookup = (providerID: string, modelID: string) => {
|
|
43
43
|
const provider = sync.data.provider.find((x) => x.id === providerID)
|
|
44
|
-
const model = provider?.models[modelID]
|
|
44
|
+
const model = provider?.models?.[modelID]
|
|
45
45
|
if (!provider || !model) return
|
|
46
46
|
return {
|
|
47
47
|
model,
|
|
@@ -71,7 +71,7 @@ export function DialogModel(props: { providerID?: string }) {
|
|
|
71
71
|
return items.flatMap((item) => {
|
|
72
72
|
const provider = sync.data.provider.find((x) => x.id === item.providerID)
|
|
73
73
|
if (!provider) return []
|
|
74
|
-
const model = provider.models[item.modelID]
|
|
74
|
+
const model = provider.models?.[item.modelID]
|
|
75
75
|
if (!model) return []
|
|
76
76
|
return [
|
|
77
77
|
{
|
|
@@ -142,7 +142,7 @@ export function usePromptMemos(deps: MemoDeps): PromptMemos {
|
|
|
142
142
|
if (!last) return
|
|
143
143
|
const tokens = last.tokens.input + last.tokens.output + last.tokens.reasoning + last.tokens.cache.read + last.tokens.cache.write
|
|
144
144
|
if (tokens <= 0) return
|
|
145
|
-
const model = sync.data.provider.find((item) => item.id === last.providerID)?.models[last.modelID]
|
|
145
|
+
const model = sync.data.provider.find((item) => item.id === last.providerID)?.models?.[last.modelID]
|
|
146
146
|
const contextLimit = model?.limit.context ?? 0
|
|
147
147
|
const pct = contextLimit ? Math.round((tokens / contextLimit) * 100) : 0
|
|
148
148
|
const cost = msg.reduce((sum, item) => sum + (item.role === "assistant" ? item.cost : 0), 0)
|
|
@@ -5,7 +5,7 @@ export function useConnected() {
|
|
|
5
5
|
const sync = useSync()
|
|
6
6
|
return createMemo(() =>
|
|
7
7
|
sync.data.provider.some(
|
|
8
|
-
(x) => (x.id !== "saeeol" && x.id !== "saeeol") || Object.values(x.models).some((y) => y.cost?.input !== 0),
|
|
8
|
+
(x) => (x.id !== "saeeol" && x.id !== "saeeol") || Object.values(x.models ?? {}).some((y) => y.cost?.input !== 0),
|
|
9
9
|
),
|
|
10
10
|
)
|
|
11
11
|
}
|
|
@@ -32,7 +32,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|
|
32
32
|
|
|
33
33
|
function isModelValid(model: { providerID: string; modelID: string }) {
|
|
34
34
|
const provider = sync.data.provider.find((x) => x.id === model.providerID)
|
|
35
|
-
return !!provider?.models[model.modelID]
|
|
35
|
+
return !!provider?.models?.[model.modelID]
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
function getFirstValidModel(...modelFns: (() => { providerID: string; modelID: string } | undefined)[]) {
|
|
@@ -280,7 +280,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|
|
280
280
|
}
|
|
281
281
|
}
|
|
282
282
|
const provider = sync.data.provider.find((x) => x.id === value.providerID)
|
|
283
|
-
const info = provider?.models[value.modelID]
|
|
283
|
+
const info = provider?.models?.[value.modelID]
|
|
284
284
|
return {
|
|
285
285
|
provider: provider?.name ?? value.providerID,
|
|
286
286
|
model: info?.name ?? value.modelID,
|
|
@@ -402,7 +402,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|
|
402
402
|
const m = currentModel()
|
|
403
403
|
if (!m) return []
|
|
404
404
|
const provider = sync.data.provider.find((x) => x.id === m.providerID)
|
|
405
|
-
const info = provider?.models[m.modelID]
|
|
405
|
+
const info = provider?.models?.[m.modelID]
|
|
406
406
|
if (!info?.variants) return []
|
|
407
407
|
return Object.keys(info.variants)
|
|
408
408
|
},
|
|
@@ -447,6 +447,13 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
|
|
|
447
447
|
await sdk.client.mcp.connect({ name })
|
|
448
448
|
}
|
|
449
449
|
},
|
|
450
|
+
async refresh() {
|
|
451
|
+
const workspace = project.workspace.current()
|
|
452
|
+
await (sdk.client as any).mcp._client.post({
|
|
453
|
+
url: "/mcp/refresh",
|
|
454
|
+
...(workspace ? { workspace } : {}),
|
|
455
|
+
})
|
|
456
|
+
},
|
|
450
457
|
}
|
|
451
458
|
createEffect(() => {
|
|
452
459
|
if (!model.ready) return
|
|
@@ -16,13 +16,17 @@ export type SaeeolClawRoute = {
|
|
|
16
16
|
type: "saeeolclaw"
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
export type LocalModelsRoute = {
|
|
20
|
+
type: "local-models"
|
|
21
|
+
}
|
|
22
|
+
|
|
19
23
|
export type PluginRoute = {
|
|
20
24
|
type: "plugin"
|
|
21
25
|
id: string
|
|
22
26
|
data?: Record<string, unknown>
|
|
23
27
|
}
|
|
24
28
|
|
|
25
|
-
export type Route = HomeRoute | SessionRoute | PluginRoute | SaeeolClawRoute
|
|
29
|
+
export type Route = HomeRoute | SessionRoute | PluginRoute | SaeeolClawRoute | LocalModelsRoute
|
|
26
30
|
|
|
27
31
|
export const { use: useRoute, provider: RouteProvider } = createSimpleContext({
|
|
28
32
|
name: "Route",
|
|
@@ -26,7 +26,7 @@ function View(props: { api: TuiPluginApi; session_id: string }) {
|
|
|
26
26
|
|
|
27
27
|
const tokens =
|
|
28
28
|
last.tokens.input + last.tokens.output + last.tokens.reasoning + last.tokens.cache.read + last.tokens.cache.write
|
|
29
|
-
const model = props.api.state.provider.find((item) => item.id === last.providerID)?.models[last.modelID]
|
|
29
|
+
const model = props.api.state.provider.find((item) => item.id === last.providerID)?.models?.[last.modelID]
|
|
30
30
|
return {
|
|
31
31
|
tokens,
|
|
32
32
|
percent: model?.limit.context ? Math.round((tokens / model.limit.context) * 100) : null,
|
|
@@ -96,11 +96,15 @@ function routeCurrent(route: ReturnType<typeof useRoute>): TuiPluginApi["route"]
|
|
|
96
96
|
}
|
|
97
97
|
}
|
|
98
98
|
if (route.data.type === "saeeolclaw") return { name: "saeeolclaw" }
|
|
99
|
+
if (route.data.type === "local-models") return { name: "local-models" }
|
|
99
100
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
101
|
+
if (route.data.type === "plugin") {
|
|
102
|
+
return {
|
|
103
|
+
name: route.data.id,
|
|
104
|
+
params: route.data.data,
|
|
105
|
+
}
|
|
103
106
|
}
|
|
107
|
+
return { name: "unknown" }
|
|
104
108
|
}
|
|
105
109
|
|
|
106
110
|
function mapOption<Value>(item: TuiDialogSelectOption<Value>): SelectOption<Value> {
|