saeeol 1.2.8 → 1.2.9

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
- "version": "1.2.8",
3
+ "version": "1.2.9",
4
4
  "name": "saeeol",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -50,8 +50,8 @@
50
50
  "@babel/core": "7.28.4",
51
51
  "@effect/language-service": "0.84.2",
52
52
  "@octokit/webhooks-types": "7.6.1",
53
- "@saeeol/script": "7.3.4",
54
- "@saeeol/core": "7.3.4",
53
+ "@saeeol/script": "7.3.5",
54
+ "@saeeol/core": "7.3.5",
55
55
  "@parcel/watcher-darwin-arm64": "2.5.1",
56
56
  "@parcel/watcher-darwin-x64": "2.5.1",
57
57
  "@parcel/watcher-linux-arm64-glibc": "2.5.1",
@@ -132,15 +132,15 @@
132
132
  "@opentui/solid": "0.2.2",
133
133
  "@parcel/watcher": "2.5.1",
134
134
  "@pierre/diffs": "1.1.0-beta.18",
135
- "@saeeol/boxes": "0.2.0",
136
- "@saeeol/core": "7.3.4",
137
- "@saeeol/gateway": "7.3.3",
138
- "@saeeol/i18n": "7.3.4",
139
- "@saeeol/indexing": "7.3.3",
140
- "@saeeol/plugin": "7.3.4",
141
- "@saeeol/script": "7.3.4",
142
- "@saeeol/sdk": "7.3.4",
143
- "@saeeol/telemetry": "7.3.3",
135
+ "@saeeol/boxes": "7.3.5",
136
+ "@saeeol/core": "7.3.5",
137
+ "@saeeol/gateway": "7.3.5",
138
+ "@saeeol/i18n": "7.3.5",
139
+ "@saeeol/indexing": "7.3.5",
140
+ "@saeeol/plugin": "7.3.5",
141
+ "@saeeol/script": "7.3.5",
142
+ "@saeeol/sdk": "7.3.5",
143
+ "@saeeol/telemetry": "7.3.5",
144
144
  "@solid-primitives/event-bus": "1.1.2",
145
145
  "@solid-primitives/scheduled": "1.5.2",
146
146
  "@standard-schema/spec": "1.0.0",
@@ -1,4 +1,4 @@
1
- import { useDialog } from "@tui/ui/dialog"
1
+ import { useDialog } from "@tui/ui/dialog"
2
2
  import { DialogSelect, type DialogSelectOption, type DialogSelectRef } from "@tui/ui/dialog-select"
3
3
  import {
4
4
  createContext,
@@ -74,7 +74,6 @@ function init() {
74
74
  for (const option of entries()) {
75
75
  if (!isEnabled(option)) continue
76
76
  if (option.keybind && keybind.match(option.keybind, evt)) {
77
- // Require double-tap for agent cycle keybinds
78
77
  if (DOUBLE_TAB_KEYS.has(option.keybind)) {
79
78
  const now = Date.now()
80
79
  const match = option.keybind === lastTabKey && now - lastTabTime < DOUBLE_TAB_WINDOW
@@ -187,4 +186,4 @@ function DialogCommand(props: { options: CommandOption[]; suggestedOptions: Comm
187
186
  return [...props.suggestedOptions, ...props.options]
188
187
  }
189
188
  return <DialogSelect ref={(r) => (ref = r)} title={t("cmd.title")} options={list()} />
190
- }
189
+ }
@@ -27,7 +27,6 @@ export function DialogMcp() {
27
27
  const [loading, setLoading] = createSignal<string | null>(null)
28
28
 
29
29
  const options = createMemo(() => {
30
- // Track sync data and loading state to trigger re-render when they change
31
30
  const mcpData = sync.data.mcp
32
31
  const loadingMcp = loading()
33
32
 
@@ -56,7 +55,6 @@ export function DialogMcp() {
56
55
  setLoading(option.value)
57
56
  try {
58
57
  await local.mcp.toggle(option.value)
59
- // Refresh MCP status from server
60
58
  const status = await sdk.client.mcp.status()
61
59
  if (status.data) {
62
60
  sync.set("mcp", status.data)
@@ -36,7 +36,6 @@ export function DialogStash(props: { onSelect: (entry: StashEntry) => void }) {
36
36
 
37
37
  const options = createMemo(() => {
38
38
  const entries = stash.list()
39
- // Show most recent first
40
39
  return entries
41
40
  .map((entry, index) => {
42
41
  const isDeleting = toDelete() === index
@@ -1,4 +1,4 @@
1
- import { createContext, Show, useContext, type ParentProps } from "solid-js"
1
+ import { createContext, Show, useContext, type ParentProps } from "solid-js"
2
2
 
3
3
  export function createSimpleContext<T, Props extends Record<string, any>>(input: {
4
4
  name: string
@@ -10,7 +10,6 @@ export function createSimpleContext<T, Props extends Record<string, any>>(input:
10
10
  provider: (props: ParentProps<Props>) => {
11
11
  const init = input.init(props)
12
12
  return (
13
- // @ts-expect-error
14
13
  <Show when={init.ready === undefined || init.ready === true}>
15
14
  <ctx.Provider value={init}>{props.children}</ctx.Provider>
16
15
  </Show>
@@ -22,4 +21,4 @@ export function createSimpleContext<T, Props extends Record<string, any>>(input:
22
21
  return value
23
22
  },
24
23
  }
25
- }
24
+ }
@@ -1,4 +1,4 @@
1
- import { createSaeeolClient } from "@saeeol/sdk/v2"
1
+ import { createSaeeolClient } from "@saeeol/sdk/v2"
2
2
  import type { GlobalEvent } from "@saeeol/sdk/v2"
3
3
  import { createSimpleContext } from "./helper"
4
4
  import { createGlobalEmitter } from "@solid-primitives/event-bus"
@@ -100,8 +100,6 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
100
100
  if (queue.length > 0) flush()
101
101
  attempt += 1
102
102
  if (abort.signal.aborted || ctrl.signal.aborted) break
103
-
104
- // Exponential backoff
105
103
  const backoff = Math.min(retryDelay * 2 ** (attempt - 1), maxRetryDelay)
106
104
  await new Promise((resolve) => setTimeout(resolve, backoff))
107
105
  }
@@ -139,4 +137,4 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
139
137
  url: props.url,
140
138
  }
141
139
  },
142
- })
140
+ })
@@ -565,7 +565,6 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
565
565
  })
566
566
  .then(() => {
567
567
  if (store.status !== "complete") setStore("status", "partial")
568
- // non-blocking
569
568
  void Promise.all([
570
569
  ...(args.continue ? [] : [sessionListPromise.then((sessions) => setStore("session", reconcile(sessions)))]),
571
570
  consoleStatePromise.then((consoleState) => setStore("console_state", reconcile(consoleState))),
@@ -234,7 +234,6 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
234
234
  return {
235
235
  theme: new Proxy({} as Theme, {
236
236
  get(_target, prop) {
237
- // @ts-expect-error
238
237
  return values()[prop]
239
238
  },
240
239
  }),
@@ -99,7 +99,6 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
99
99
  if (agent?.color) {
100
100
  const color = agent.color
101
101
  if (color.startsWith("#")) return RGBA.fromHex(color)
102
- // already validated by config, just satisfying TS here
103
102
  return theme[color as keyof typeof theme] as RGBA
104
103
  }
105
104
  return colors()[index % colors().length]
@@ -440,10 +439,8 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
440
439
  async toggle(name: string) {
441
440
  const status = sync.data.mcp[name]
442
441
  if (status?.status === "connected") {
443
- // Disable: disconnect the MCP
444
442
  await sdk.client.mcp.disconnect({ name })
445
443
  } else {
446
- // Enable/Retry: connect the MCP (handles disabled, failed, and other states)
447
444
  await sdk.client.mcp.connect({ name })
448
445
  }
449
446
  },
package/src/ltm/config.ts CHANGED
@@ -1,4 +1,4 @@
1
- /** LTM — hardware auto-detection + deterministic LLM parameter calculation */
1
+ /** LTM — hardware auto-detection + deterministic LLM parameter calculation */
2
2
 
3
3
  import os from "os"
4
4
  import { Effect } from "effect"
@@ -10,8 +10,6 @@ import type { HardwareProfile, LLMBakeParams, LTMConfig } from "./types"
10
10
 
11
11
  const log = Log.create({ service: "ltm/config" })
12
12
 
13
- // ── Default LTM config ──
14
-
15
13
  export const DEFAULT_LTM_CONFIG: LTMConfig = {
16
14
  enabled: false,
17
15
  embeddingModel: "auto",
@@ -23,8 +21,6 @@ export const DEFAULT_LTM_CONFIG: LTMConfig = {
23
21
  retrieval: { topK: 5, minScore: 0.7, maxTokens: 2000 },
24
22
  }
25
23
 
26
- // ── Hardware profiling ──
27
-
28
24
  /** Collect system hardware information */
29
25
  export async function profileHardware(): Promise<HardwareProfile> {
30
26
  const gpu = await Effect.runPromise(GPU.profile)
@@ -51,8 +47,6 @@ export function hardwareHash(hw: HardwareProfile): string {
51
47
  return h.toString(36)
52
48
  }
53
49
 
54
- // ── Embedding model selection ──
55
-
56
50
  /** Auto-select embedding model based on available VRAM */
57
51
  export function selectEmbeddingModel(hw: HardwareProfile) {
58
52
  const vram = hw.availableVRAMMB
@@ -63,8 +57,6 @@ export function selectEmbeddingModel(hw: HardwareProfile) {
63
57
  return RAG.EMBEDDING_MODELS[5] // all-minilm-l6, 384d, 80MB
64
58
  }
65
59
 
66
- // ── Bake (deterministic parameter calculation) ──
67
-
68
60
  /**
69
61
  * Compute hardware-based LLM parameters and persist to disk.
70
62
  * Once computed, the same values are reused as long as hardware doesn't change.
@@ -96,8 +88,6 @@ export async function bake(hw: HardwareProfile): Promise<LLMBakeParams> {
96
88
  return params
97
89
  }
98
90
 
99
- // ── Persistence ──
100
-
101
91
  import path from "path"
102
92
  import { mkdir, readFile, writeFile } from "fs/promises"
103
93
 
@@ -121,4 +111,4 @@ async function readBake(): Promise<LLMBakeParams | undefined> {
121
111
  async function writeBake(params: LLMBakeParams): Promise<void> {
122
112
  await mkdir(bakeDir(), { recursive: true })
123
113
  await writeFile(bakePath(), JSON.stringify(params, null, 2))
124
- }
114
+ }
@@ -1,4 +1,4 @@
1
- /** LTM procedural memory — user coding preferences and patterns (English, LLM-to-LLM) */
1
+ /** LTM procedural memory — user coding preferences and patterns (English, LLM-to-LLM) */
2
2
 
3
3
  import * as Log from "@saeeol/core/util/log"
4
4
  import * as Embedder from "@/provider/local/embedder"
@@ -20,8 +20,6 @@ export function extractStyleSignals(
20
20
  ): StyleSignal[] {
21
21
  const signals: StyleSignal[] = []
22
22
  const ext = filePath.split(".").pop() ?? ""
23
-
24
- // Comment language
25
23
  if (content.includes("//") && (ext === "ts" || ext === "js")) {
26
24
  const hasKoreanComment = /[ㄱ-ㅎㅏ-ㅣ가-힣]/.test(content)
27
25
  signals.push({
@@ -30,15 +28,11 @@ export function extractStyleSignals(
30
28
  evidence: hasKoreanComment ? "comments in Korean" : "comments in English",
31
29
  })
32
30
  }
33
-
34
- // Indentation
35
31
  if (content.includes(" ") && !content.includes("\t")) {
36
32
  signals.push({ language: ext, pattern: "indent", evidence: "2-space indentation" })
37
33
  } else if (content.includes("\t")) {
38
34
  signals.push({ language: ext, pattern: "indent", evidence: "tab indentation" })
39
35
  }
40
-
41
- // Semicolons
42
36
  if (ext === "ts" || ext === "js") {
43
37
  const hasSemicolons = /;\s*\n/.test(content)
44
38
  signals.push({
@@ -47,8 +41,6 @@ export function extractStyleSignals(
47
41
  evidence: hasSemicolons ? "uses semicolons" : "no semicolons",
48
42
  })
49
43
  }
50
-
51
- // Quote style
52
44
  const singleQuotes = (content.match(/'/g) ?? []).length
53
45
  const doubleQuotes = (content.match(/"/g) ?? []).length
54
46
  if (singleQuotes > doubleQuotes * 2) {
@@ -56,8 +48,6 @@ export function extractStyleSignals(
56
48
  } else if (doubleQuotes > singleQuotes * 2) {
57
49
  signals.push({ language: ext, pattern: "quotes", evidence: "prefers double quotes" })
58
50
  }
59
-
60
- // Naming: camelCase vs snake_case
61
51
  const camelCase = (content.match(/[a-z][A-Z]/g) ?? []).length
62
52
  const snakeCase = (content.match(/_[a-z]/g) ?? []).length
63
53
  if (camelCase > snakeCase * 3) {
@@ -99,4 +89,4 @@ export async function fromStyleSignals(
99
89
  }
100
90
 
101
91
  return memories
102
- }
92
+ }
@@ -1,4 +1,4 @@
1
- /** LTM — background collection pipeline */
1
+ /** LTM — background collection pipeline */
2
2
 
3
3
  import { Effect } from "effect"
4
4
  import * as Log from "@saeeol/core/util/log"
@@ -19,8 +19,6 @@ let running = false
19
19
  let config: LTMConfig | undefined
20
20
  let unsubscribers: Array<() => void> = []
21
21
 
22
- // ── Pipeline lifecycle ──
23
-
24
22
  /** Start the pipeline */
25
23
  export async function start(cfg: LTMConfig, bake: LLMBakeParams): Promise<void> {
26
24
  if (running) return
@@ -31,24 +29,18 @@ export async function start(cfg: LTMConfig, bake: LLMBakeParams): Promise<void>
31
29
 
32
30
  log.info("pipeline starting", { model: bake.embeddingModel })
33
31
 
34
- // Start embedding server
35
32
  const server = await Embedder.start(bake)
36
33
  if (server.status !== "running") {
37
34
  log.error("embedding server failed to start, pipeline disabled")
38
35
  running = false
39
36
  return
40
37
  }
41
-
42
- // Prune old memories
43
- if (cfg.episodic.enabled) {
44
- const pruned = await Store.prune(cfg.episodic.retainDays * 24 * 60 * 60 * 1000)
38
+ if (cfg.episodic.enabled) { const pruned = await Store.prune(cfg.episodic.retainDays * 24 * 60 * 60 * 1000)
45
39
  if (pruned > 0) {
46
40
  log.info("pruned old episodic memories", { count: pruned })
47
41
  void Bus.publish(LTMEvent.MemoryPruned, { count: pruned, type: "episodic" })
48
42
  }
49
43
  }
50
-
51
- // Enforce memory limit
52
44
  const count = await Store.count()
53
45
  if (count > cfg.maxMemories) {
54
46
  const excess = count - cfg.maxMemories
@@ -58,8 +50,6 @@ export async function start(cfg: LTMConfig, bake: LLMBakeParams): Promise<void>
58
50
  await Store.remove(toRemove)
59
51
  log.info("trimmed memories to max", { removed: toRemove.length })
60
52
  }
61
-
62
- // Bind BusEvent subscriptions
63
53
  bindSubscriptions()
64
54
 
65
55
  log.info("pipeline started", { memoryCount: await Store.count() })
@@ -69,8 +59,6 @@ export async function start(cfg: LTMConfig, bake: LLMBakeParams): Promise<void>
69
59
  export async function stop(): Promise<void> {
70
60
  if (!running) return
71
61
  running = false
72
-
73
- // Unsubscribe
74
62
  for (const unsub of unsubscribers) {
75
63
  try { unsub() } catch { /* ignore */ }
76
64
  }
@@ -86,8 +74,6 @@ export function isActive(): boolean {
86
74
  return running
87
75
  }
88
76
 
89
- // ── BusEvent subscription binding ──
90
-
91
77
  function bindSubscriptions() {
92
78
  if (!config) return
93
79
 
@@ -169,8 +155,6 @@ function bindSubscriptions() {
169
155
  log.info("bus subscriptions bound", { count: unsubscribers.length })
170
156
  }
171
157
 
172
- // ── Direct-call handlers (called explicitly from external modules) ──
173
-
174
158
  /** Conversation message → episodic memory */
175
159
  export async function onMessageCompleted(
176
160
  sessionID: string,
@@ -254,4 +238,4 @@ export async function onCodeEdit(
254
238
  for (const memory of memories) {
255
239
  await Store.upsert(memory)
256
240
  }
257
- }
241
+ }
@@ -1,4 +1,4 @@
1
- /** LTM — VRAM/task scheduler */
1
+ /** LTM — VRAM/task scheduler */
2
2
 
3
3
  import * as Log from "@saeeol/core/util/log"
4
4
  import type { HardwareProfile } from "./types"
@@ -18,8 +18,6 @@ export interface Allocation {
18
18
  /** Scheduling strategy */
19
19
  export type Strategy = "concurrent" | "alternating" | "cpu-fallback" | "no-gpu"
20
20
 
21
- // ── VRAM gauge ──
22
-
23
21
  /** Query current VRAM allocation state */
24
22
  export async function allocation(): Promise<Allocation> {
25
23
  const gpu = await Effect.runPromise(GPU.profile)
@@ -40,14 +38,11 @@ export async function strategy(): Promise<Strategy> {
40
38
  return "cpu-fallback"
41
39
  }
42
40
 
43
- // ── Execution decisions ──
44
-
45
41
  /** Whether embedding background work can run */
46
42
  export async function canRunEmbedding(): Promise<boolean> {
47
43
  const strat = await strategy()
48
44
  // no-gpu or cpu-fallback: always allowed (CPU processing)
49
45
  if (strat === "no-gpu" || strat === "cpu-fallback") return true
50
- // concurrent: always allowed
51
46
  if (strat === "concurrent") return true
52
47
  // alternating: need at least 2GB VRAM
53
48
  const alloc = await allocation()
@@ -61,20 +56,13 @@ export async function canRunConcurrent(hw: HardwareProfile): Promise<boolean> {
61
56
  return false
62
57
  }
63
58
 
64
- // ── LLM ↔ embedding time-sharing ──
65
-
66
59
  /** Called when LLM requests VRAM — pauses embedding if needed */
67
60
  export async function requestLLM(vramNeededMB: number): Promise<boolean> {
68
61
  const strat = await strategy()
69
-
70
- // concurrent strategy: both can run
71
62
  if (strat === "concurrent") return true
72
- // no-gpu: GPU not needed
73
63
  if (strat === "no-gpu") return true
74
64
 
75
65
  const alloc = await allocation()
76
-
77
- // Sufficient VRAM — proceed as-is
78
66
  if (alloc.available >= vramNeededMB) {
79
67
  log.info("LLM request: enough VRAM", { available: alloc.available, needed: vramNeededMB })
80
68
  return true
@@ -126,4 +114,4 @@ export async function resumeEmbedding(bake: {
126
114
  } catch (e) {
127
115
  log.error("failed to resume embedding server", { error: e })
128
116
  }
129
- }
117
+ }
package/src/ltm/store.ts CHANGED
@@ -1,4 +1,4 @@
1
- /** LTM — filesystem-based vector store */
1
+ /** LTM — filesystem-based vector store */
2
2
 
3
3
  import path from "path"
4
4
  import { mkdir, readFile, writeFile, readdir, rm, stat } from "fs/promises"
@@ -8,8 +8,6 @@ import type { Memory, MemoryType } from "./types"
8
8
 
9
9
  const log = Log.create({ service: "ltm/store" })
10
10
 
11
- // ── Cosine similarity ──
12
-
13
11
  function cosine(a: number[], b: number[]): number {
14
12
  let dot = 0
15
13
  let na = 0
@@ -24,14 +22,11 @@ function cosine(a: number[], b: number[]): number {
24
22
  return denom === 0 ? 0 : dot / denom
25
23
  }
26
24
 
27
- // ── File paths ──
28
-
29
25
  function storeDir(): string {
30
26
  return path.join(Global.Path.data, "ltm", "memories")
31
27
  }
32
28
 
33
29
  function memoryPath(id: string): string {
34
- // Sanitize special characters for safe filenames
35
30
  const safe = id.replace(/[:<>\"|?*]/g, "_")
36
31
  return path.join(storeDir(), `${safe}.json`)
37
32
  }
@@ -44,8 +39,6 @@ async function ensure(): Promise<void> {
44
39
  await mkdir(storeDir(), { recursive: true })
45
40
  }
46
41
 
47
- // ── Index ──
48
-
49
42
  interface Index {
50
43
  memories: Array<{ id: string; type: MemoryType; timestamp: number; source: string }>
51
44
  }
@@ -64,8 +57,6 @@ async function writeIndex(idx: Index): Promise<void> {
64
57
  await writeFile(indexPath(), JSON.stringify(idx, null, 2))
65
58
  }
66
59
 
67
- // ── Public API ──
68
-
69
60
  export async function upsert(memory: Memory): Promise<void> {
70
61
  await ensure()
71
62
  await writeFile(memoryPath(memory.id), JSON.stringify(memory, null, 2))
@@ -149,4 +140,4 @@ export async function prune(olderThanMs: number): Promise<number> {
149
140
  export async function count(): Promise<number> {
150
141
  const idx = await readIndex()
151
142
  return idx.memories.length
152
- }
143
+ }
package/src/ltm/types.ts CHANGED
@@ -1,14 +1,12 @@
1
- /** LTM — long-term memory type definitions */
1
+ /** LTM — long-term memory type definitions */
2
2
 
3
3
  import { Schema } from "effect"
4
4
  import { optionalOmitUndefined } from "@/util/schema"
5
5
 
6
- // ── Memory types ──
7
6
 
8
7
  export const MemoryType = Schema.Literals(["episodic", "semantic", "procedural"])
9
8
  export type MemoryType = Schema.Schema.Type<typeof MemoryType>
10
9
 
11
- // ── Memory entry ──
12
10
 
13
11
  export const MemoryMetadata = Schema.Struct({
14
12
  source: Schema.String,
@@ -30,7 +28,6 @@ export const Memory = Schema.Struct({
30
28
  })
31
29
  export type Memory = Schema.Schema.Type<typeof Memory>
32
30
 
33
- // ── Embedding server ──
34
31
 
35
32
  export const EmbedderStatus = Schema.Literals(["stopped", "starting", "running", "error"])
36
33
  export type EmbedderStatus = Schema.Schema.Type<typeof EmbedderStatus>
@@ -45,7 +42,6 @@ export const EmbeddingServer = Schema.Struct({
45
42
  })
46
43
  export type EmbeddingServer = Schema.Schema.Type<typeof EmbeddingServer>
47
44
 
48
- // ── Hardware profile ──
49
45
 
50
46
  export const HardwareProfile = Schema.Struct({
51
47
  gpuCount: Schema.Number,
@@ -57,7 +53,6 @@ export const HardwareProfile = Schema.Struct({
57
53
  })
58
54
  export type HardwareProfile = Schema.Schema.Type<typeof HardwareProfile>
59
55
 
60
- // ── LLM parameters (deterministic) ──
61
56
 
62
57
  export const LLMBakeParams = Schema.Struct({
63
58
  /** Embedding model ID */
@@ -79,7 +74,6 @@ export const LLMBakeParams = Schema.Struct({
79
74
  })
80
75
  export type LLMBakeParams = Schema.Schema.Type<typeof LLMBakeParams>
81
76
 
82
- // ── LTM configuration ──
83
77
 
84
78
  export const LTMConfig = Schema.Struct({
85
79
  enabled: Schema.Boolean,
@@ -105,4 +99,4 @@ export const LTMConfig = Schema.Struct({
105
99
  maxTokens: Schema.Number,
106
100
  }),
107
101
  })
108
- export type LTMConfig = Schema.Schema.Type<typeof LTMConfig>
102
+ export type LTMConfig = Schema.Schema.Type<typeof LTMConfig>
@@ -1,4 +1,4 @@
1
- /** Local embedding server — Ollama-based embedding model lifecycle management */
1
+ /** Local embedding server — Ollama-based embedding model lifecycle management */
2
2
 
3
3
  import { Effect } from "effect"
4
4
  import * as Log from "@saeeol/core/util/log"
@@ -17,8 +17,6 @@ const log = Log.create({ service: "local/embedder" })
17
17
 
18
18
  let server: EmbeddingServer | undefined
19
19
 
20
- // ── Ollama status checks ──
21
-
22
20
  async function isOllamaRunning(endpoint: string): Promise<boolean> {
23
21
  try {
24
22
  const res = await fetch(`${endpoint}/api/tags`, { signal: AbortSignal.timeout(3000) })
@@ -53,8 +51,6 @@ async function isModelInstalled(endpoint: string, model: string): Promise<boolea
53
51
  }
54
52
  }
55
53
 
56
- // ── Pull model from Ollama ──
57
-
58
54
  async function pullModel(endpoint: string, model: string): Promise<void> {
59
55
  log.info("pulling embedding model", { model })
60
56
  const ollamaModel = getOllamaModelName(model)
@@ -91,8 +87,6 @@ function getOllamaModelName(modelId: string): string {
91
87
  return map[modelId] ?? modelId
92
88
  }
93
89
 
94
- // ── Embedding API call ──
95
-
96
90
  async function embedViaOllama(endpoint: string, model: string, texts: string[]): Promise<number[][]> {
97
91
  const ollamaModel = getOllamaModelName(model)
98
92
  const res = await fetch(`${endpoint}/api/embed`, {
@@ -114,16 +108,12 @@ async function embedViaOllama(endpoint: string, model: string, texts: string[]):
114
108
  return data.embeddings
115
109
  }
116
110
 
117
- // ── VRAM estimation ──
118
-
119
111
  function estimateEmbeddingVRAM(modelId: string): number {
120
112
  const model = RAG.EMBEDDING_MODELS.find((m) => m.id === modelId)
121
113
  if (!model) return 200
122
114
  return Math.ceil(model.sizeBytes * 1.2 / (1024 * 1024))
123
115
  }
124
116
 
125
- // ── Public API ──
126
-
127
117
  /** Start the embedding server */
128
118
  export async function start(bake: LLMBakeParams): Promise<EmbeddingServer> {
129
119
  if (server && server.status === "running") {
@@ -146,8 +136,6 @@ export async function start(bake: LLMBakeParams): Promise<EmbeddingServer> {
146
136
  }
147
137
 
148
138
  void Bus.publish(LTMEvent.EmbedderStatusChanged, { status: "starting", model: bake.embeddingModel })
149
-
150
- // Check if Ollama is running
151
139
  const running = await isOllamaRunning(endpoint)
152
140
  if (!running) {
153
141
  log.warn("Ollama not running, embedding server unavailable", { endpoint })
@@ -155,16 +143,12 @@ export async function start(bake: LLMBakeParams): Promise<EmbeddingServer> {
155
143
  void Bus.publish(LTMEvent.EmbedderStatusChanged, { status: "error", model: bake.embeddingModel })
156
144
  return server
157
145
  }
158
-
159
- // Check if model is installed
160
146
  const ollamaModel = getOllamaModelName(bake.embeddingModel)
161
147
  const installed = await isModelInstalled(endpoint, ollamaModel)
162
148
  if (!installed) {
163
149
  log.info("model not installed, pulling", { model: ollamaModel })
164
150
  await pullModel(endpoint, ollamaModel)
165
151
  }
166
-
167
- // Check if model is loaded (auto-loaded on first call)
168
152
  const loaded = await isModelLoaded(endpoint, ollamaModel)
169
153
  if (!loaded) {
170
154
  // Warmup call — load model into memory
@@ -217,4 +201,4 @@ export async function embedOne(text: string): Promise<number[]> {
217
201
  /** VRAM usage in MB */
218
202
  export function vramUsage(): number {
219
203
  return server?.vramMB ?? 0
220
- }
204
+ }
@@ -1,4 +1,4 @@
1
- import { Provider } from "@/provider/provider"
1
+ import { Provider } from "@/provider/provider"
2
2
  import * as Log from "@saeeol/core/util/log"
3
3
  import { Context, Effect, Layer, Record } from "effect"
4
4
  import * as Stream from "effect/Stream"
@@ -119,11 +119,8 @@ const live: Layer.Layer<
119
119
  system.push(
120
120
  [
121
121
  ...(isOpenaiOauth ? [] : [SystemPrompt.soul()]),
122
- // use agent prompt otherwise provider prompt
123
122
  ...(input.agent.prompt ? [input.agent.prompt] : SystemPrompt.provider(input.model)),
124
- // any custom prompt passed into this call
125
123
  ...input.system,
126
- // any custom prompt from last user message
127
124
  ...(input.user.system ? [input.user.system] : []),
128
125
  ]
129
126
  .filter((x) => x)
@@ -435,7 +432,6 @@ const live: Layer.Layer<
435
432
  specificationVersion: "v3" as const,
436
433
  async transformParams(args) {
437
434
  if (args.type === "stream") {
438
- // @ts-expect-error
439
435
  args.params.prompt = ProviderTransform.message(args.params.prompt, input.model, options)
440
436
  }
441
437
  return args.params
@@ -501,4 +497,4 @@ export function hasToolCalls(messages: ModelMessage[]): boolean {
501
497
  return false
502
498
  }
503
499
 
504
- export * as LLM from "./llm"
500
+ export * as LLM from "./llm"
@@ -1,4 +1,4 @@
1
- import type { NamedError } from "@saeeol/core/util/error"
1
+ import type { NamedError } from "@saeeol/core/util/error"
2
2
  import { Cause, Clock, Duration, Effect, Schedule } from "effect"
3
3
  import { MessageV2 } from "../message/message-v2"
4
4
  import { isSaeeolError } from "@/saeeol/errors"
@@ -36,10 +36,8 @@ export function delay(attempt: number, error?: MessageV2.APIError) {
36
36
  if (retryAfter) {
37
37
  const parsedSeconds = Number.parseFloat(retryAfter)
38
38
  if (!Number.isNaN(parsedSeconds)) {
39
- // convert seconds to milliseconds
40
39
  return cap(Math.ceil(parsedSeconds * 1000))
41
40
  }
42
- // Try parsing as HTTP date format
43
41
  const parsed = Date.parse(retryAfter) - Date.now()
44
42
  if (!Number.isNaN(parsed) && parsed > 0) {
45
43
  return cap(Math.ceil(parsed))
@@ -68,8 +66,6 @@ export function retryable(error: Err) {
68
66
  if (error.data.responseBody?.includes("FreeUsageLimitError")) return undefined
69
67
  return error.data.message.includes("Overloaded") ? "Provider is overloaded" : error.data.message
70
68
  }
71
-
72
- // Check for rate limit patterns in plain text error messages
73
69
  const msg = error.data?.message
74
70
  if (typeof msg === "string") {
75
71
  const lower = msg.toLowerCase()
@@ -146,4 +142,4 @@ export function policy(opts: {
146
142
  )
147
143
  }
148
144
 
149
- export * as SessionRetry from "./retry"
145
+ export * as SessionRetry from "./retry"
@@ -0,0 +1,101 @@
1
+ /**
2
+ * session-events.ts — SyncEvent/BusEvent 정의 단일 소스
3
+ *
4
+ * core/session.ts와 core/session-types.ts가 공유.
5
+ * SaeeolSession overlay를 import하지 않아 순환 의존 없음.
6
+ * TurnOpen/TurnClose는 overlay 의존이 필요하므로 각 소비자에서 직접 추가.
7
+ */
8
+
9
+ import { BusEvent } from "@/bus/bus-event"
10
+ import { SyncEvent } from "../../sync"
11
+ import { SessionID } from "./schema"
12
+ import { Snapshot } from "@/snapshot"
13
+ import { MessageV2 } from "../message/message-v2"
14
+ import { Info, Summary, Revert } from "./session-types"
15
+ import { ArchivedTimestamp } from "./session-types"
16
+ import { Schema } from "effect"
17
+ import { Permission } from "@/permission"
18
+ import { ProjectID } from "../../project/schema"
19
+ import { WorkspaceID } from "../../control-plane/schema"
20
+ import { NonNegativeInt } from "@/util/schema"
21
+
22
+ // ── Event schemas ──
23
+
24
+ const UpdatedShare = Schema.Struct({
25
+ url: Schema.optional(Schema.NullOr(Schema.String)),
26
+ })
27
+
28
+ const UpdatedTime = Schema.Struct({
29
+ created: Schema.optional(Schema.NullOr(NonNegativeInt)),
30
+ updated: Schema.optional(Schema.NullOr(NonNegativeInt)),
31
+ compacting: Schema.optional(Schema.NullOr(NonNegativeInt)),
32
+ archived: Schema.optional(Schema.NullOr(ArchivedTimestamp)),
33
+ })
34
+
35
+ const UpdatedInfo = Schema.Struct({
36
+ id: Schema.optional(Schema.NullOr(SessionID)),
37
+ slug: Schema.optional(Schema.NullOr(Schema.String)),
38
+ projectID: Schema.optional(Schema.NullOr(ProjectID)),
39
+ workspaceID: Schema.optional(Schema.NullOr(WorkspaceID)),
40
+ directory: Schema.optional(Schema.NullOr(Schema.String)),
41
+ path: Schema.optional(Schema.NullOr(Schema.String)),
42
+ parentID: Schema.optional(Schema.NullOr(SessionID)),
43
+ summary: Schema.optional(Schema.NullOr(Summary)),
44
+ share: Schema.optional(UpdatedShare),
45
+ title: Schema.optional(Schema.NullOr(Schema.String)),
46
+ version: Schema.optional(Schema.NullOr(Schema.String)),
47
+ time: Schema.optional(UpdatedTime),
48
+ permission: Schema.optional(Schema.NullOr(Permission.Ruleset)),
49
+ revert: Schema.optional(Schema.NullOr(Revert)),
50
+ })
51
+
52
+ export const CreatedEventSchema = Schema.Struct({
53
+ sessionID: SessionID,
54
+ info: Info,
55
+ })
56
+
57
+ export const UpdatedEventSchema = Schema.Struct({
58
+ sessionID: SessionID,
59
+ info: UpdatedInfo,
60
+ })
61
+
62
+ // ── Event definitions (singleton registration) ──
63
+
64
+ export const SyncEvents = {
65
+ Created: SyncEvent.define({
66
+ type: "session.created",
67
+ version: 1,
68
+ aggregate: "sessionID",
69
+ schema: CreatedEventSchema,
70
+ }),
71
+ Updated: SyncEvent.define({
72
+ type: "session.updated",
73
+ version: 1,
74
+ aggregate: "sessionID",
75
+ schema: UpdatedEventSchema,
76
+ busSchema: CreatedEventSchema,
77
+ }),
78
+ Deleted: SyncEvent.define({
79
+ type: "session.deleted",
80
+ version: 1,
81
+ aggregate: "sessionID",
82
+ schema: CreatedEventSchema,
83
+ }),
84
+ }
85
+
86
+ export const BusEvents = {
87
+ Diff: BusEvent.define(
88
+ "session.diff",
89
+ Schema.Struct({
90
+ sessionID: SessionID,
91
+ diff: Schema.Array(Snapshot.FileDiff),
92
+ }),
93
+ ),
94
+ Error: BusEvent.define(
95
+ "session.error",
96
+ Schema.Struct({
97
+ sessionID: Schema.optional(SessionID),
98
+ error: MessageV2.Assistant.fields.error,
99
+ }),
100
+ ),
101
+ }
@@ -19,6 +19,7 @@ import { SaeeolSession } from "@/saeeol/session"
19
19
  import { Effect, Schema, Types } from "effect"
20
20
  import { zod } from "@/util/effect-zod"
21
21
  import { NonNegativeInt, optionalOmitUndefined, withStatics } from "@/util/schema"
22
+ import { SyncEvents, BusEvents } from "./session-events"
22
23
 
23
24
  const log = Log.create({ service: "session" })
24
25
 
@@ -70,11 +71,11 @@ export function sessionPath(worktree: string, cwd: string) {
70
71
  return path.relative(path.resolve(worktree), cwd).replaceAll("\\", "/")
71
72
  }
72
73
 
73
- const Summary = Schema.Struct({ additions: NonNegativeInt, deletions: NonNegativeInt, files: NonNegativeInt, diffs: optionalOmitUndefined(Schema.Array(Snapshot.SummaryFileDiff)) })
74
+ export const Summary = Schema.Struct({ additions: NonNegativeInt, deletions: NonNegativeInt, files: NonNegativeInt, diffs: optionalOmitUndefined(Schema.Array(Snapshot.SummaryFileDiff)) })
74
75
  const Share = Schema.Struct({ url: Schema.String })
75
76
  export const ArchivedTimestamp = Schema.Finite
76
77
  const Time = Schema.Struct({ created: NonNegativeInt, updated: NonNegativeInt, compacting: optionalOmitUndefined(NonNegativeInt), archived: optionalOmitUndefined(ArchivedTimestamp) })
77
- const Revert = Schema.Struct({ messageID: MessageID, partID: optionalOmitUndefined(PartID), snapshot: optionalOmitUndefined(Schema.String), diff: optionalOmitUndefined(Schema.String) })
78
+ export const Revert = Schema.Struct({ messageID: MessageID, partID: optionalOmitUndefined(PartID), snapshot: optionalOmitUndefined(Schema.String), diff: optionalOmitUndefined(Schema.String) })
78
79
 
79
80
  export const Info = Schema.Struct({
80
81
  id: SessionID, slug: Schema.String, projectID: ProjectID, workspaceID: optionalOmitUndefined(WorkspaceID),
@@ -125,11 +126,8 @@ const UpdatedInfo = Schema.Struct({
125
126
  const UpdatedEventSchema = Schema.Struct({ sessionID: SessionID, info: UpdatedInfo })
126
127
 
127
128
  export const Event = {
128
- Created: SyncEvent.define({ type: "session.created", version: 1, aggregate: "sessionID", schema: CreatedEventSchema }),
129
- Updated: SyncEvent.define({ type: "session.updated", version: 1, aggregate: "sessionID", schema: UpdatedEventSchema, busSchema: CreatedEventSchema }),
130
- Deleted: SyncEvent.define({ type: "session.deleted", version: 1, aggregate: "sessionID", schema: CreatedEventSchema }),
131
- Diff: BusEvent.define("session.diff", Schema.Struct({ sessionID: SessionID, diff: Schema.Array(Snapshot.FileDiff) })),
132
- Error: BusEvent.define("session.error", Schema.Struct({ sessionID: Schema.optional(SessionID), error: MessageV2.Assistant.fields.error })),
129
+ ...SyncEvents,
130
+ ...BusEvents,
133
131
  TurnOpen: SaeeolSession.Event.TurnOpen,
134
132
  TurnClose: SaeeolSession.Event.TurnClose,
135
133
  }
@@ -1,4 +1,4 @@
1
- import { Slug } from "@saeeol/core/util/slug"
1
+ import { Slug } from "@saeeol/core/util/slug"
2
2
  import path from "path"
3
3
  import { BusEvent } from "@/bus/bus-event"
4
4
  import { Bus } from "@/bus"
@@ -33,6 +33,7 @@ import { fn } from "@/util/fn"
33
33
  import { Effect, Layer, Option, Context, Schema, Types } from "effect"
34
34
  import { zod } from "@/util/effect-zod"
35
35
  import { NonNegativeInt, optionalOmitUndefined, withStatics } from "@/util/schema"
36
+ import { SyncEvents, BusEvents } from "./session-events"
36
37
 
37
38
  const log = Log.create({ service: "session" })
38
39
 
@@ -279,7 +280,12 @@ const UpdatedEventSchema = Schema.Struct({
279
280
  info: UpdatedInfo,
280
281
  })
281
282
 
282
- export { Event } from "./session-types"
283
+ export const Event = {
284
+ ...SyncEvents,
285
+ ...BusEvents,
286
+ TurnOpen: SaeeolSession.Event.TurnOpen,
287
+ TurnClose: SaeeolSession.Event.TurnClose,
288
+ }
283
289
 
284
290
  export function plan(input: { slug: string; time: { created: number } }, instance: InstanceContext) {
285
291
  const base = instance.project.vcs
@@ -310,11 +316,8 @@ export const getUsage = (input: {
310
316
  input.usage.inputTokenDetails?.cacheWriteTokens ??
311
317
  input.metadata?.["anthropic"]?.["cacheCreationInputTokens"] ??
312
318
  // google-vertex-anthropic returns metadata under "vertex" key
313
- // (AnthropicMessagesLanguageModel custom provider key from 'vertex.anthropic.messages')
314
319
  input.metadata?.["vertex"]?.["cacheCreationInputTokens"] ??
315
- // @ts-expect-error
316
320
  input.metadata?.["bedrock"]?.["usage"]?.["cacheWriteInputTokens"] ??
317
- // @ts-expect-error
318
321
  input.metadata?.["venice"]?.["usage"]?.["cacheCreationInputTokens"] ??
319
322
  0,
320
323
  ),
@@ -802,9 +805,6 @@ function* listByProject(
802
805
  )
803
806
  }
804
807
  } else if (input.scope !== "project" && !Flag.SAEEOL_EXPERIMENTAL_WORKSPACES) {
805
- // if (input.directory) {
806
- // conditions.push(eq(SessionTable.directory, input.directory))
807
- // }
808
808
  }
809
809
  if (input.roots) {
810
810
  conditions.push(isNull(SessionTable.parent_id))
@@ -907,4 +907,4 @@ export const updatePartDelta = fn(
907
907
  (input) => runPromise((svc) => svc.updatePartDelta(input)),
908
908
  )
909
909
 
910
- export * as Session from "./session"
910
+ export * as Session from "./session"
@@ -1,4 +1,4 @@
1
- /** Error types + fromError + OutputFormat — extracted from message-v2.ts */
1
+ /** Error types + fromError + OutputFormat — extracted from message-v2.ts */
2
2
 
3
3
  import { APICallError, LoadAPIKeyError } from "ai"
4
4
  import { NamedError } from "@saeeol/core/util/error"
@@ -37,8 +37,6 @@ const _Format = Schema.Union([OutputFormatText, OutputFormatJsonSchema]).annotat
37
37
  export { _Format }
38
38
  export const Format = Object.assign(_Format, { zod: zod(_Format) })
39
39
  export type OutputFormat = Schema.Schema.Type<typeof _Format>
40
-
41
- // Assistant error union (Zod)
42
40
  import z from "zod"
43
41
  const AssistantErrorZod = z.discriminatedUnion("name", [
44
42
  AuthError.Schema, NamedError.Unknown.Schema, OutputLengthError.Schema, AbortedError.Schema,
@@ -46,8 +44,6 @@ const AssistantErrorZod = z.discriminatedUnion("name", [
46
44
  ])
47
45
  export type AssistantError = z.infer<typeof AssistantErrorZod>
48
46
  export { AssistantErrorZod }
49
-
50
- // Assistant error union (Effect Schema)
51
47
  export const AssistantErrorSchema = Schema.Union([
52
48
  AuthError.EffectSchema,
53
49
  Schema.Struct({ name: Schema.Literal("UnknownError"), data: Schema.Struct({ message: Schema.String }) }).annotate({ identifier: "UnknownError" }),
@@ -80,4 +76,4 @@ export function fromError(e: unknown, ctx: { providerID: ProviderID; aborted?: b
80
76
  } catch {}
81
77
  return new NamedError.Unknown({ message: JSON.stringify(e) }, { cause: e }).toObject()
82
78
  }
83
- }
79
+ }
@@ -1,4 +1,4 @@
1
- /** Part schemas + ToolState + Input schemas — extracted from message-v2.ts */
1
+ /** Part schemas + ToolState + Input schemas — extracted from message-v2.ts */
2
2
 
3
3
  import { SessionID, MessageID, PartID } from "../core/schema"
4
4
  import { LSP } from "@/lsp/lsp"
@@ -81,9 +81,7 @@ export const AgentPartInput = Schema.Struct({ id: Schema.optional(PartID), type:
81
81
  export type AgentPartInput = Types.DeepMutable<Schema.Schema.Type<typeof AgentPartInput>>
82
82
  export const SubtaskPartInput = Schema.Struct({ id: Schema.optional(PartID), type: Schema.Literal("subtask"), prompt: Schema.String, description: Schema.String, agent: Schema.String, model: Schema.optional(Schema.Struct({ providerID: ProviderID, modelID: ModelID })), command: Schema.optional(Schema.String) }).annotate({ identifier: "SubtaskPartInput" }).pipe(withStatics((s) => ({ zod: zod(s) })))
83
83
  export type SubtaskPartInput = Types.DeepMutable<Schema.Schema.Type<typeof SubtaskPartInput>>
84
-
85
- // Part union
86
84
  const _Part = Schema.Union([TextPart, SubtaskPart, ReasoningPart, FilePart, ToolPart, StepStartPart, StepFinishPart, SnapshotPart, PatchPart, AgentPart, RetryPart, CompactionPart]).annotate({ discriminator: "type", identifier: "Part" })
87
85
  export const Part = Object.assign(_Part, { zod: zod(_Part) as unknown as z.ZodType<TextPart | SubtaskPart | ReasoningPart | FilePart | ToolPart | StepStartPart | StepFinishPart | SnapshotPart | PatchPart | AgentPart | RetryPart | CompactionPart> })
88
86
  export type Part = TextPart | SubtaskPart | ReasoningPart | FilePart | ToolPart | StepStartPart | StepFinishPart | SnapshotPart | PatchPart | AgentPart | RetryPart | CompactionPart
89
- export { _Part }
87
+ export { _Part }
@@ -1,5 +1,4 @@
1
- // @ts-ignore
2
- globalThis.AI_SDK_LOG_WARNINGS = false
1
+ globalThis.AI_SDK_LOG_WARNINGS = false
3
2
 
4
3
  import { Effect, Layer, Scope, Latch, Context } from "effect"
5
4
  import { ChildProcessSpawner } from "effect/unstable/process"
@@ -208,4 +207,4 @@ export const prompt = (input: PromptInput) => runPromise((svc) => svc.prompt(inp
208
207
  export const loopExport = (input: LoopInput) => runPromise((svc) => svc.loop(input))
209
208
  export const cancel = (sessionID: SessionID) => runPromise((svc) => svc.cancel(sessionID))
210
209
 
211
- export * as SessionPrompt from "./prompt"
210
+ export * as SessionPrompt from "./prompt"
@@ -37,8 +37,6 @@ export const ApplyPatchTool = Tool.define(
37
37
  if (!params.patchText) {
38
38
  return yield* Effect.fail(new Error("patchText is required"))
39
39
  }
40
-
41
- // Parse the patch to get hunks
42
40
  let hunks: Patch.Hunk[]
43
41
  try {
44
42
  const parseResult = Patch.parsePatch(params.patchText)
@@ -56,8 +54,6 @@ export const ApplyPatchTool = Tool.define(
56
54
  }
57
55
 
58
56
  const instance = yield* InstanceState.context
59
-
60
- // Validate file paths and check permissions
61
57
  const fileChanges: Array<{
62
58
  filePath: string
63
59
  oldContent: string
@@ -109,7 +105,6 @@ export const ApplyPatchTool = Tool.define(
109
105
  }
110
106
 
111
107
  case "update": {
112
- // Check if file exists for update
113
108
  const stats = yield* afs.stat(filePath).pipe(Effect.catch(() => Effect.succeed(undefined)))
114
109
  if (!stats || stats.type === "Directory") {
115
110
  return yield* Effect.fail(
@@ -132,8 +127,6 @@ export const ApplyPatchTool = Tool.define(
132
127
  let newContent = oldContent
133
128
  let bom = source.bom
134
129
  let encoding = read.encoding
135
-
136
- // Apply the update chunks to get new content
137
130
  try {
138
131
  const fileUpdate = Patch.deriveNewContentsFromChunks(filePath, hunk.chunks)
139
132
  newContent = fileUpdate.content
@@ -205,8 +198,6 @@ export const ApplyPatchTool = Tool.define(
205
198
  }
206
199
  }
207
200
  }
208
-
209
- // Build per-file metadata for UI rendering (used for both permission and result)
210
201
  const files = fileChanges.map((change) => ({
211
202
  filePath: change.filePath,
212
203
  relativePath: path.relative(instance.worktree, change.movePath ?? change.filePath).replaceAll("\\", "/"),
@@ -216,8 +207,6 @@ export const ApplyPatchTool = Tool.define(
216
207
  deletions: change.deletions,
217
208
  movePath: change.movePath,
218
209
  }))
219
-
220
- // Check permissions if needed
221
210
  const relativePaths = fileChanges.map((c) => path.relative(instance.worktree, c.filePath).replaceAll("\\", "/"))
222
211
  yield* ctx.ask({
223
212
  permission: "edit",
@@ -229,15 +218,12 @@ export const ApplyPatchTool = Tool.define(
229
218
  files,
230
219
  },
231
220
  })
232
-
233
- // Apply the changes
234
221
  const updates: Array<{ file: string; event: "add" | "change" | "unlink" }> = []
235
222
 
236
223
  for (const change of fileChanges) {
237
224
  const edited = change.type === "delete" ? undefined : (change.movePath ?? change.filePath)
238
225
  switch (change.type) {
239
226
  case "add":
240
- // Create parent directories (recursive: true is safe on existing/root dirs)
241
227
  yield* EncodedIO.write(change.filePath, Bom.join(change.newContent, change.bom), change.encoding)
242
228
  updates.push({ file: change.filePath, event: "add" })
243
229
  break
@@ -249,7 +235,6 @@ export const ApplyPatchTool = Tool.define(
249
235
 
250
236
  case "move":
251
237
  if (change.movePath) {
252
- // Create parent directories (recursive: true is safe on existing/root dirs)
253
238
  yield* EncodedIO.write(change.movePath!, Bom.join(change.newContent, change.bom), change.encoding)
254
239
  yield* afs.remove(change.filePath)
255
240
  updates.push({ file: change.filePath, event: "unlink" })
@@ -270,21 +255,15 @@ export const ApplyPatchTool = Tool.define(
270
255
  yield* bus.publish(File.Event.Edited, { file: edited })
271
256
  }
272
257
  }
273
-
274
- // Publish file change events
275
258
  for (const update of updates) {
276
259
  yield* bus.publish(FileWatcher.Event.Updated, update)
277
260
  }
278
-
279
- // Notify LSP of file changes and collect diagnostics
280
261
  for (const change of fileChanges) {
281
262
  if (change.type === "delete") continue
282
263
  const target = change.movePath ?? change.filePath
283
264
  yield* lsp.touchFile(target, "document")
284
265
  }
285
266
  const diagnostics = yield* lsp.diagnostics()
286
-
287
- // Generate output summary
288
267
  const summaryLines = fileChanges.map((change) => {
289
268
  if (change.type === "add") {
290
269
  return `A ${path.relative(instance.worktree, change.filePath).replaceAll("\\", "/")}`
@@ -1,4 +1,4 @@
1
- import { levenshtein, SINGLE_CANDIDATE_SIMILARITY_THRESHOLD, MULTIPLE_CANDIDATES_SIMILARITY_THRESHOLD } from "./edit-utils"
1
+ import { levenshtein, SINGLE_CANDIDATE_SIMILARITY_THRESHOLD, MULTIPLE_CANDIDATES_SIMILARITY_THRESHOLD } from "./edit-utils"
2
2
 
3
3
  export type Replacer = (content: string, find: string) => Generator<string, void, unknown>
4
4
 
@@ -151,7 +151,6 @@ export const WhitespaceNormalizedReplacer: Replacer = function* (content, find)
151
151
  const match = line.match(regex)
152
152
  if (match) yield match[0]
153
153
  } catch {
154
- // Invalid regex pattern, skip
155
154
  }
156
155
  }
157
156
  }
@@ -285,4 +284,4 @@ export const ContextAwareReplacer: Replacer = function* (content, find) {
285
284
  }
286
285
  }
287
286
  }
288
- }
287
+ }
@@ -1,4 +1,4 @@
1
- import { Effect, Schema } from "effect"
1
+ import { Effect, Schema } from "effect"
2
2
  import { Npm } from "@saeeol/core/npm"
3
3
  import * as Bus from "@/bus"
4
4
  import * as Tool from "../core/tool"
@@ -98,8 +98,6 @@ export const PackageTool = Tool.define(
98
98
  ]
99
99
  return { title: "Available Providers", output: lines.join("\n"), metadata: {} }
100
100
  }
101
-
102
- // action === "install"
103
101
  const packages = params.packages
104
102
  if (!packages || packages.length === 0) {
105
103
  return {
@@ -165,4 +163,4 @@ export const PackageTool = Tool.define(
165
163
  }),
166
164
  }
167
165
  }),
168
- )
166
+ )
@@ -7,9 +7,6 @@ import { Bus } from "../../bus"
7
7
  import { TuiEvent } from "../../cli/cmd/tui/event"
8
8
  import DESCRIPTION from "./warpgrep.txt"
9
9
 
10
- // FREE_PERIOD_TODO: Remove SAEEOL_WARPGREP_PROXY_URL constant and the proxy
11
- // fallback below. After the free period ends, require MORPH_API_KEY and
12
- // return an error when it is missing.
13
10
  const SAEEOL_WARPGREP_PROXY_URL = "https://api.saeeol.ai/api/gateway"
14
11
 
15
12
  const Parameters = Schema.Struct({
@@ -36,8 +33,6 @@ export const CodebaseSearchTool = Tool.define(
36
33
 
37
34
  const apiKey = process.env["MORPH_API_KEY"]
38
35
 
39
- // FREE_PERIOD_TODO: Remove proxy fallback — require apiKey, error if missing:
40
- // if (!apiKey) return { title: ..., output: "Set MORPH_API_KEY to use codebase search.", metadata: {} }
41
36
  const client = new WarpGrepClient({
42
37
  morphApiKey: apiKey ?? "saeeol-free",
43
38
  ...(apiKey ? {} : { morphApiUrl: SAEEOL_WARPGREP_PROXY_URL }),
@@ -52,9 +47,6 @@ export const CodebaseSearchTool = Tool.define(
52
47
  )
53
48
 
54
49
  if (!result.success || !result.contexts?.length) {
55
- // FREE_PERIOD_TODO: When the proxy stops serving free requests, errors
56
- // from the proxy (401/402/429) will surface here. The message below
57
- // tells the user exactly what to do.
58
50
  const isAuthOrRateLimit =
59
51
  result.error && /401|402|429|rate.limit|free.period|unauthorized/i.test(result.error)
60
52
  const apiKeyMsg =
@@ -1,4 +1,4 @@
1
- import { Effect, Schema } from "effect"
1
+ import { Effect, Schema } from "effect"
2
2
  import { HttpClient, HttpClientRequest } from "effect/unstable/http"
3
3
  import * as Tool from "../core/tool"
4
4
  import TurndownService from "turndown"
@@ -49,8 +49,6 @@ export const WebFetchTool = Tool.define(
49
49
  })
50
50
 
51
51
  const timeout = Math.min((params.timeout ?? DEFAULT_TIMEOUT / 1000) * 1000, MAX_TIMEOUT)
52
-
53
- // Build Accept header based on requested format with q parameters for fallbacks
54
52
  let acceptHeader = "*/*"
55
53
  switch (params.format) {
56
54
  case "markdown":
@@ -92,8 +90,6 @@ export const WebFetchTool = Tool.define(
92
90
  ),
93
91
  Effect.timeoutOrElse({ duration: timeout, orElse: () => Effect.die(new Error("Request timed out")) }),
94
92
  )
95
-
96
- // Check content length
97
93
  const contentLength = response.headers["content-length"]
98
94
  if (contentLength && parseInt(contentLength) > MAX_RESPONSE_SIZE) {
99
95
  throw new Error("Response too large (exceeds 5MB limit)")
@@ -125,8 +121,6 @@ export const WebFetchTool = Tool.define(
125
121
  }
126
122
 
127
123
  const content = new TextDecoder().decode(arrayBuffer)
128
-
129
- // Handle content based on requested format and actual content type
130
124
  switch (params.format) {
131
125
  case "markdown":
132
126
  if (contentType.includes("text/html")) {
@@ -167,12 +161,10 @@ async function extractTextFromHTML(html: string) {
167
161
  skipContent = true
168
162
  },
169
163
  text() {
170
- // Skip text content inside these elements
171
164
  },
172
165
  })
173
166
  .on("*", {
174
167
  element(element) {
175
- // Reset skip flag when entering other elements
176
168
  if (!["script", "style", "noscript", "iframe", "object", "embed"].includes(element.tagName)) {
177
169
  skipContent = false
178
170
  }
@@ -199,4 +191,4 @@ function convertHTMLToMarkdown(html: string): string {
199
191
  })
200
192
  turndownService.remove(["script", "style", "meta", "link"])
201
193
  return turndownService.turndown(html)
202
- }
194
+ }