kaizenai 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/README.md +246 -0
- package/bin/kaizen +15 -0
- package/dist/client/apple-touch-icon.png +0 -0
- package/dist/client/assets/index-D-ORCGrq.js +603 -0
- package/dist/client/assets/index-r28mcHqz.css +32 -0
- package/dist/client/favicon.png +0 -0
- package/dist/client/fonts/body-medium.woff2 +0 -0
- package/dist/client/fonts/body-regular-italic.woff2 +0 -0
- package/dist/client/fonts/body-regular.woff2 +0 -0
- package/dist/client/fonts/body-semibold.woff2 +0 -0
- package/dist/client/index.html +22 -0
- package/dist/client/manifest-dark.webmanifest +24 -0
- package/dist/client/manifest.webmanifest +24 -0
- package/dist/client/pwa-192.png +0 -0
- package/dist/client/pwa-512.png +0 -0
- package/dist/client/pwa-icon.svg +15 -0
- package/dist/client/pwa-splash.png +0 -0
- package/dist/client/pwa-splash.svg +15 -0
- package/package.json +103 -0
- package/src/server/acp-shared.ts +315 -0
- package/src/server/agent.ts +1120 -0
- package/src/server/attachments.ts +133 -0
- package/src/server/backgrounds.ts +74 -0
- package/src/server/cli-runtime.ts +333 -0
- package/src/server/cli-supervisor.ts +81 -0
- package/src/server/cli.ts +68 -0
- package/src/server/codex-app-server-protocol.ts +453 -0
- package/src/server/codex-app-server.ts +1350 -0
- package/src/server/cursor-acp.ts +819 -0
- package/src/server/discovery.ts +322 -0
- package/src/server/event-store.ts +1369 -0
- package/src/server/events.ts +244 -0
- package/src/server/external-open.ts +272 -0
- package/src/server/gemini-acp.ts +844 -0
- package/src/server/gemini-cli.ts +525 -0
- package/src/server/generate-title.ts +36 -0
- package/src/server/git-manager.ts +79 -0
- package/src/server/git-repository.ts +101 -0
- package/src/server/harness-types.ts +20 -0
- package/src/server/keybindings.ts +177 -0
- package/src/server/machine-name.ts +22 -0
- package/src/server/paths.ts +112 -0
- package/src/server/process-utils.ts +22 -0
- package/src/server/project-icon.ts +344 -0
- package/src/server/project-metadata.ts +10 -0
- package/src/server/provider-catalog.ts +85 -0
- package/src/server/provider-settings.ts +155 -0
- package/src/server/quick-response.ts +153 -0
- package/src/server/read-models.ts +275 -0
- package/src/server/recovery.ts +507 -0
- package/src/server/restart.ts +30 -0
- package/src/server/server.ts +244 -0
- package/src/server/terminal-manager.ts +350 -0
- package/src/server/theme-settings.ts +179 -0
- package/src/server/update-manager.ts +230 -0
- package/src/server/usage/base-provider-usage.ts +57 -0
- package/src/server/usage/claude-usage.ts +558 -0
- package/src/server/usage/codex-usage.ts +144 -0
- package/src/server/usage/cursor-browser.ts +120 -0
- package/src/server/usage/cursor-cookies.ts +390 -0
- package/src/server/usage/cursor-usage.ts +490 -0
- package/src/server/usage/gemini-usage.ts +24 -0
- package/src/server/usage/provider-usage.ts +61 -0
- package/src/server/usage/test-helpers.ts +9 -0
- package/src/server/usage/types.ts +54 -0
- package/src/server/usage/utils.ts +325 -0
- package/src/server/ws-router.ts +717 -0
- package/src/shared/branding.ts +83 -0
- package/src/shared/dev-ports.ts +43 -0
- package/src/shared/ports.ts +2 -0
- package/src/shared/protocol.ts +152 -0
- package/src/shared/tools.ts +251 -0
- package/src/shared/types.ts +1028 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { watch, type FSWatcher } from "node:fs"
|
|
2
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises"
|
|
3
|
+
import { homedir } from "node:os"
|
|
4
|
+
import path from "node:path"
|
|
5
|
+
import { getThemeSettingsFilePath, LOG_PREFIX } from "../shared/branding"
|
|
6
|
+
import {
|
|
7
|
+
DEFAULT_THEME_SETTINGS,
|
|
8
|
+
type ColorTheme,
|
|
9
|
+
type CustomAppearance,
|
|
10
|
+
type ThemePreference,
|
|
11
|
+
type ThemeSettingsSnapshot,
|
|
12
|
+
} from "../shared/types"
|
|
13
|
+
|
|
14
|
+
const VALID_THEME_PREFERENCES: ThemePreference[] = ["light", "dark", "system", "custom"]
|
|
15
|
+
const VALID_COLOR_THEMES: ColorTheme[] = ["default", "tokyo-night", "catppuccin", "dracula", "nord", "everforest", "rose-pine"]
|
|
16
|
+
const VALID_CUSTOM_APPEARANCES: CustomAppearance[] = ["light", "dark", "system"]
|
|
17
|
+
|
|
18
|
+
export class ThemeSettingsManager {
|
|
19
|
+
readonly filePath: string
|
|
20
|
+
private watcher: FSWatcher | null = null
|
|
21
|
+
private snapshot: ThemeSettingsSnapshot
|
|
22
|
+
private readonly listeners = new Set<(snapshot: ThemeSettingsSnapshot) => void>()
|
|
23
|
+
|
|
24
|
+
constructor(filePath = getThemeSettingsFilePath(homedir())) {
|
|
25
|
+
this.filePath = filePath
|
|
26
|
+
this.snapshot = createDefaultSnapshot(this.filePath)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async initialize() {
|
|
30
|
+
await mkdir(path.dirname(this.filePath), { recursive: true })
|
|
31
|
+
const file = Bun.file(this.filePath)
|
|
32
|
+
if (!(await file.exists())) {
|
|
33
|
+
await writeFile(this.filePath, `${JSON.stringify(DEFAULT_THEME_SETTINGS, null, 2)}\n`, "utf8")
|
|
34
|
+
}
|
|
35
|
+
await this.reload()
|
|
36
|
+
this.startWatching()
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
dispose() {
|
|
40
|
+
this.watcher?.close()
|
|
41
|
+
this.watcher = null
|
|
42
|
+
this.listeners.clear()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
getSnapshot() {
|
|
46
|
+
return this.snapshot
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
onChange(listener: (snapshot: ThemeSettingsSnapshot) => void) {
|
|
50
|
+
this.listeners.add(listener)
|
|
51
|
+
return () => {
|
|
52
|
+
this.listeners.delete(listener)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async reload() {
|
|
57
|
+
const nextSnapshot = await readThemeSettingsSnapshot(this.filePath)
|
|
58
|
+
this.setSnapshot(nextSnapshot)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async write(settings: ThemeSettingsSnapshot["settings"]) {
|
|
62
|
+
const nextSnapshot = normalizeThemeSettings(settings, this.filePath)
|
|
63
|
+
await mkdir(path.dirname(this.filePath), { recursive: true })
|
|
64
|
+
await writeFile(this.filePath, `${JSON.stringify(nextSnapshot.settings, null, 2)}\n`, "utf8")
|
|
65
|
+
this.setSnapshot(nextSnapshot)
|
|
66
|
+
return nextSnapshot
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private setSnapshot(snapshot: ThemeSettingsSnapshot) {
|
|
70
|
+
this.snapshot = snapshot
|
|
71
|
+
for (const listener of this.listeners) {
|
|
72
|
+
listener(snapshot)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private startWatching() {
|
|
77
|
+
this.watcher?.close()
|
|
78
|
+
try {
|
|
79
|
+
this.watcher = watch(path.dirname(this.filePath), { persistent: false }, (_eventType, filename) => {
|
|
80
|
+
if (filename && filename !== path.basename(this.filePath)) {
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
void this.reload().catch((error: unknown) => {
|
|
84
|
+
console.warn(`${LOG_PREFIX} Failed to reload theme settings:`, error)
|
|
85
|
+
})
|
|
86
|
+
})
|
|
87
|
+
} catch (error) {
|
|
88
|
+
console.warn(`${LOG_PREFIX} Failed to watch theme settings file:`, error)
|
|
89
|
+
this.watcher = null
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export async function readThemeSettingsSnapshot(filePath: string): Promise<ThemeSettingsSnapshot> {
|
|
95
|
+
try {
|
|
96
|
+
const text = await readFile(filePath, "utf8")
|
|
97
|
+
if (!text.trim()) {
|
|
98
|
+
return createDefaultSnapshot(filePath, "Theme settings file was empty. Using defaults.")
|
|
99
|
+
}
|
|
100
|
+
const parsed = JSON.parse(text) as Partial<ThemeSettingsSnapshot["settings"]>
|
|
101
|
+
return normalizeThemeSettings(parsed, filePath)
|
|
102
|
+
} catch (error) {
|
|
103
|
+
if ((error as NodeJS.ErrnoException)?.code === "ENOENT") {
|
|
104
|
+
return createDefaultSnapshot(filePath)
|
|
105
|
+
}
|
|
106
|
+
if (error instanceof SyntaxError) {
|
|
107
|
+
return createDefaultSnapshot(filePath, "Theme settings file is invalid JSON. Using defaults.")
|
|
108
|
+
}
|
|
109
|
+
throw error
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function normalizeThemeSettings(
|
|
114
|
+
value: Partial<ThemeSettingsSnapshot["settings"]> | null | undefined,
|
|
115
|
+
filePath = getThemeSettingsFilePath(homedir())
|
|
116
|
+
): ThemeSettingsSnapshot {
|
|
117
|
+
const source = value && typeof value === "object" && !Array.isArray(value) ? value : {}
|
|
118
|
+
|
|
119
|
+
const themePreference: ThemePreference = VALID_THEME_PREFERENCES.includes(source.themePreference as ThemePreference)
|
|
120
|
+
? source.themePreference as ThemePreference
|
|
121
|
+
: DEFAULT_THEME_SETTINGS.themePreference
|
|
122
|
+
|
|
123
|
+
const colorTheme: ColorTheme = VALID_COLOR_THEMES.includes(source.colorTheme as ColorTheme)
|
|
124
|
+
? source.colorTheme as ColorTheme
|
|
125
|
+
: DEFAULT_THEME_SETTINGS.colorTheme
|
|
126
|
+
|
|
127
|
+
const customAppearance: CustomAppearance = VALID_CUSTOM_APPEARANCES.includes(source.customAppearance as CustomAppearance)
|
|
128
|
+
? source.customAppearance as CustomAppearance
|
|
129
|
+
: DEFAULT_THEME_SETTINGS.customAppearance
|
|
130
|
+
|
|
131
|
+
const backgroundImage: string | null =
|
|
132
|
+
typeof source.backgroundImage === "string" ? source.backgroundImage : DEFAULT_THEME_SETTINGS.backgroundImage
|
|
133
|
+
|
|
134
|
+
const backgroundOpacity: number =
|
|
135
|
+
typeof source.backgroundOpacity === "number" && source.backgroundOpacity >= 0 && source.backgroundOpacity <= 1
|
|
136
|
+
? source.backgroundOpacity
|
|
137
|
+
: DEFAULT_THEME_SETTINGS.backgroundOpacity
|
|
138
|
+
|
|
139
|
+
const backgroundBlur: number =
|
|
140
|
+
typeof source.backgroundBlur === "number" && source.backgroundBlur >= 0
|
|
141
|
+
? source.backgroundBlur
|
|
142
|
+
: DEFAULT_THEME_SETTINGS.backgroundBlur
|
|
143
|
+
|
|
144
|
+
const showProjectIconsInSidebar: boolean =
|
|
145
|
+
typeof source.showProjectIconsInSidebar === "boolean"
|
|
146
|
+
? source.showProjectIconsInSidebar
|
|
147
|
+
: DEFAULT_THEME_SETTINGS.showProjectIconsInSidebar
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
settings: {
|
|
151
|
+
themePreference,
|
|
152
|
+
colorTheme,
|
|
153
|
+
customAppearance,
|
|
154
|
+
backgroundImage,
|
|
155
|
+
backgroundOpacity,
|
|
156
|
+
backgroundBlur,
|
|
157
|
+
showProjectIconsInSidebar,
|
|
158
|
+
},
|
|
159
|
+
warning: null,
|
|
160
|
+
filePathDisplay: formatDisplayPath(filePath),
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function createDefaultSnapshot(filePath: string, warning: string | null = null): ThemeSettingsSnapshot {
|
|
165
|
+
return {
|
|
166
|
+
settings: { ...DEFAULT_THEME_SETTINGS },
|
|
167
|
+
warning,
|
|
168
|
+
filePathDisplay: formatDisplayPath(filePath),
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function formatDisplayPath(filePath: string) {
|
|
173
|
+
const homePath = homedir()
|
|
174
|
+
if (filePath === homePath) return "~"
|
|
175
|
+
if (filePath.startsWith(`${homePath}${path.sep}`)) {
|
|
176
|
+
return `~${filePath.slice(homePath.length)}`
|
|
177
|
+
}
|
|
178
|
+
return filePath
|
|
179
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import type { UpdateInstallResult, UpdateSnapshot } from "../shared/types"
|
|
2
|
+
import { PACKAGE_NAME } from "../shared/branding"
|
|
3
|
+
import { compareVersions, type UpdateInstallAttemptResult } from "./cli-runtime"
|
|
4
|
+
|
|
5
|
+
const UPDATE_CACHE_TTL_MS = 5 * 60 * 1000
|
|
6
|
+
|
|
7
|
+
export interface UpdateManagerDeps {
|
|
8
|
+
currentVersion: string
|
|
9
|
+
fetchLatestVersion: (packageName: string) => Promise<string>
|
|
10
|
+
installVersion: (packageName: string, version: string) => UpdateInstallAttemptResult
|
|
11
|
+
devMode?: boolean
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class UpdateManager {
|
|
15
|
+
private readonly deps: UpdateManagerDeps
|
|
16
|
+
private readonly listeners = new Set<(snapshot: UpdateSnapshot) => void>()
|
|
17
|
+
private snapshot: UpdateSnapshot
|
|
18
|
+
private checkPromise: Promise<UpdateSnapshot> | null = null
|
|
19
|
+
private installPromise: Promise<UpdateInstallResult> | null = null
|
|
20
|
+
|
|
21
|
+
constructor(deps: UpdateManagerDeps) {
|
|
22
|
+
this.deps = deps
|
|
23
|
+
this.snapshot = {
|
|
24
|
+
currentVersion: deps.currentVersion,
|
|
25
|
+
latestVersion: deps.devMode ? `${deps.currentVersion}-dev` : null,
|
|
26
|
+
status: deps.devMode ? "available" : "idle",
|
|
27
|
+
updateAvailable: Boolean(deps.devMode),
|
|
28
|
+
lastCheckedAt: deps.devMode ? Date.now() : null,
|
|
29
|
+
error: null,
|
|
30
|
+
installAction: "restart",
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
getSnapshot() {
|
|
35
|
+
return this.snapshot
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
onChange(listener: (snapshot: UpdateSnapshot) => void) {
|
|
39
|
+
this.listeners.add(listener)
|
|
40
|
+
return () => {
|
|
41
|
+
this.listeners.delete(listener)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async checkForUpdates(options: { force?: boolean } = {}) {
|
|
46
|
+
if (this.deps.devMode) {
|
|
47
|
+
return this.snapshot
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (this.snapshot.status === "updating" || this.snapshot.status === "restart_pending") {
|
|
51
|
+
return this.snapshot
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (this.checkPromise) {
|
|
55
|
+
return this.checkPromise
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (!options.force && this.snapshot.lastCheckedAt && Date.now() - this.snapshot.lastCheckedAt < UPDATE_CACHE_TTL_MS) {
|
|
59
|
+
return this.snapshot
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
this.setSnapshot({
|
|
63
|
+
...this.snapshot,
|
|
64
|
+
status: "checking",
|
|
65
|
+
error: null,
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
const checkPromise = this.runCheck()
|
|
69
|
+
this.checkPromise = checkPromise
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
return await checkPromise
|
|
73
|
+
} finally {
|
|
74
|
+
if (this.checkPromise === checkPromise) {
|
|
75
|
+
this.checkPromise = null
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async installUpdate(): Promise<UpdateInstallResult> {
|
|
81
|
+
if (this.deps.devMode) {
|
|
82
|
+
this.setSnapshot({
|
|
83
|
+
...this.snapshot,
|
|
84
|
+
status: "updating",
|
|
85
|
+
error: null,
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
this.setSnapshot({
|
|
89
|
+
...this.snapshot,
|
|
90
|
+
status: "restart_pending",
|
|
91
|
+
updateAvailable: false,
|
|
92
|
+
error: null,
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
ok: true,
|
|
97
|
+
action: "restart",
|
|
98
|
+
errorCode: null,
|
|
99
|
+
userTitle: null,
|
|
100
|
+
userMessage: null,
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (this.snapshot.status === "updating" || this.snapshot.status === "restart_pending") {
|
|
105
|
+
return {
|
|
106
|
+
ok: this.snapshot.updateAvailable,
|
|
107
|
+
action: "restart",
|
|
108
|
+
errorCode: null,
|
|
109
|
+
userTitle: null,
|
|
110
|
+
userMessage: null,
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (this.installPromise) {
|
|
115
|
+
return this.installPromise
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const installPromise = this.runInstall()
|
|
119
|
+
this.installPromise = installPromise
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
return await installPromise
|
|
123
|
+
} finally {
|
|
124
|
+
if (this.installPromise === installPromise) {
|
|
125
|
+
this.installPromise = null
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private async runCheck() {
|
|
131
|
+
try {
|
|
132
|
+
const latestVersion = await this.deps.fetchLatestVersion(PACKAGE_NAME)
|
|
133
|
+
const updateAvailable = compareVersions(this.snapshot.currentVersion, latestVersion) < 0
|
|
134
|
+
const nextSnapshot: UpdateSnapshot = {
|
|
135
|
+
...this.snapshot,
|
|
136
|
+
latestVersion,
|
|
137
|
+
updateAvailable,
|
|
138
|
+
status: updateAvailable ? "available" : "up_to_date",
|
|
139
|
+
lastCheckedAt: Date.now(),
|
|
140
|
+
error: null,
|
|
141
|
+
}
|
|
142
|
+
this.setSnapshot(nextSnapshot)
|
|
143
|
+
return nextSnapshot
|
|
144
|
+
} catch (error) {
|
|
145
|
+
const nextSnapshot: UpdateSnapshot = {
|
|
146
|
+
...this.snapshot,
|
|
147
|
+
status: "error",
|
|
148
|
+
lastCheckedAt: Date.now(),
|
|
149
|
+
error: error instanceof Error ? error.message : String(error),
|
|
150
|
+
}
|
|
151
|
+
this.setSnapshot(nextSnapshot)
|
|
152
|
+
return nextSnapshot
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private async runInstall(): Promise<UpdateInstallResult> {
|
|
157
|
+
if (!this.snapshot.updateAvailable) {
|
|
158
|
+
const snapshot = await this.checkForUpdates({ force: true })
|
|
159
|
+
if (!snapshot.updateAvailable) {
|
|
160
|
+
return {
|
|
161
|
+
ok: false,
|
|
162
|
+
action: "restart",
|
|
163
|
+
errorCode: null,
|
|
164
|
+
userTitle: null,
|
|
165
|
+
userMessage: null,
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
this.setSnapshot({
|
|
171
|
+
...this.snapshot,
|
|
172
|
+
status: "updating",
|
|
173
|
+
error: null,
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
const targetVersion = this.snapshot.latestVersion
|
|
177
|
+
if (!targetVersion) {
|
|
178
|
+
this.setSnapshot({
|
|
179
|
+
...this.snapshot,
|
|
180
|
+
status: "error",
|
|
181
|
+
error: "Unable to determine which version to install.",
|
|
182
|
+
})
|
|
183
|
+
return {
|
|
184
|
+
ok: false,
|
|
185
|
+
action: "restart",
|
|
186
|
+
errorCode: "install_failed",
|
|
187
|
+
userTitle: "Update failed",
|
|
188
|
+
userMessage: "Kaizen could not determine which version to install.",
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const installed = this.deps.installVersion(PACKAGE_NAME, targetVersion)
|
|
193
|
+
if (!installed.ok) {
|
|
194
|
+
this.setSnapshot({
|
|
195
|
+
...this.snapshot,
|
|
196
|
+
status: "error",
|
|
197
|
+
error: installed.userMessage ?? "Unable to install the latest version.",
|
|
198
|
+
})
|
|
199
|
+
return {
|
|
200
|
+
ok: false,
|
|
201
|
+
action: "restart",
|
|
202
|
+
errorCode: installed.errorCode,
|
|
203
|
+
userTitle: installed.userTitle,
|
|
204
|
+
userMessage: installed.userMessage,
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
this.setSnapshot({
|
|
209
|
+
...this.snapshot,
|
|
210
|
+
currentVersion: this.snapshot.latestVersion ?? this.snapshot.currentVersion,
|
|
211
|
+
status: "restart_pending",
|
|
212
|
+
updateAvailable: false,
|
|
213
|
+
error: null,
|
|
214
|
+
})
|
|
215
|
+
return {
|
|
216
|
+
ok: true,
|
|
217
|
+
action: "restart",
|
|
218
|
+
errorCode: null,
|
|
219
|
+
userTitle: null,
|
|
220
|
+
userMessage: null,
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
private setSnapshot(snapshot: UpdateSnapshot) {
|
|
225
|
+
this.snapshot = snapshot
|
|
226
|
+
for (const listener of this.listeners) {
|
|
227
|
+
listener(snapshot)
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs"
|
|
2
|
+
import path from "node:path"
|
|
3
|
+
import type { AgentProvider, ProviderUsageEntry } from "../../shared/types"
|
|
4
|
+
import { PROVIDERS } from "../../shared/types"
|
|
5
|
+
|
|
6
|
+
function providerUsageRequestTimesPath(dataDir: string) {
|
|
7
|
+
return path.join(dataDir, "provider-usage-request-times.json")
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function loadProviderUsageRequestTimes(dataDir: string): Partial<Record<AgentProvider, number>> {
|
|
11
|
+
try {
|
|
12
|
+
const filePath = providerUsageRequestTimesPath(dataDir)
|
|
13
|
+
if (!existsSync(filePath)) return {}
|
|
14
|
+
const parsed = JSON.parse(readFileSync(filePath, "utf8"))
|
|
15
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {}
|
|
16
|
+
const result: Partial<Record<AgentProvider, number>> = {}
|
|
17
|
+
for (const provider of PROVIDERS.map((entry) => entry.id)) {
|
|
18
|
+
const value = (parsed as Record<string, unknown>)[provider]
|
|
19
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
20
|
+
result[provider] = value
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return result
|
|
24
|
+
} catch {
|
|
25
|
+
return {}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export abstract class BaseProviderUsage {
|
|
30
|
+
protected readonly dataDir: string
|
|
31
|
+
|
|
32
|
+
constructor(dataDir: string) {
|
|
33
|
+
this.dataDir = dataDir
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
protected recordRequestTime(provider: AgentProvider, at = Date.now()): void {
|
|
37
|
+
try {
|
|
38
|
+
const next = loadProviderUsageRequestTimes(this.dataDir)
|
|
39
|
+
next[provider] = at
|
|
40
|
+
writeFileSync(providerUsageRequestTimesPath(this.dataDir), JSON.stringify(next))
|
|
41
|
+
} catch {
|
|
42
|
+
// best-effort
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
protected readLastRequestedAt(provider: AgentProvider): number {
|
|
47
|
+
return loadProviderUsageRequestTimes(this.dataDir)[provider] ?? 0
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
protected shouldSkipRefresh(provider: AgentProvider, minInterval: number, force = false): boolean {
|
|
51
|
+
if (force) return false
|
|
52
|
+
return Date.now() - this.readLastRequestedAt(provider) < minInterval
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
abstract readonly provider: AgentProvider
|
|
56
|
+
abstract loadPersistedEntry(): ProviderUsageEntry | null
|
|
57
|
+
}
|