snow-flow 10.0.114 → 10.0.116

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": "10.0.114",
3
+ "version": "10.0.116",
4
4
  "name": "snow-flow",
5
5
  "description": "Snow-Flow - ServiceNow Multi-Agent Development Framework powered by AI",
6
6
  "license": "Elastic-2.0",
@@ -115,12 +115,12 @@ export function tui(input: {
115
115
  onUpgrade?: () => Promise<{ success: boolean; version?: string; error?: string }>
116
116
  }) {
117
117
  _onUpgrade = input.onUpgrade
118
- const isRemote = !!process.env.OPENCODE_REMOTE_TUI
118
+ const skipThemeDetection = !!process.env.OPENCODE_SKIP_THEME_DETECTION || !!process.env.OPENCODE_REMOTE_TUI
119
119
  // promise to prevent immediate exit
120
120
  return new Promise<void>(async (resolve) => {
121
121
  let mode: "dark" | "light" = "dark"
122
- if (isRemote) {
123
- console.log("[snow-flow] remote mode, skipping theme detection")
122
+ if (skipThemeDetection) {
123
+ console.log("[snow-flow] skipping theme detection")
124
124
  } else {
125
125
  console.log("[snow-flow] detecting theme...")
126
126
  mode = await getTerminalBackgroundColor()
@@ -184,7 +184,7 @@ export function tui(input: {
184
184
  targetFps: 60,
185
185
  gatherStats: false,
186
186
  exitOnCtrlC: false,
187
- ...(isRemote ? { remote: true } : {}),
187
+ // No remote: true let opentui render normally to the PTY
188
188
  useKittyKeyboard: process.env.OPENCODE_DISABLE_KITTY_KEYBOARD ? undefined : {},
189
189
  consoleOptions: {
190
190
  keyBindings: [{ name: "y", ctrl: true, action: "copy-selection" }],
@@ -675,9 +675,7 @@ function App() {
675
675
  kv.set("telemetry_enabled", !enabled)
676
676
  await Config.updateGlobal({ telemetry: !enabled }).catch(() => {})
677
677
  toast.show({
678
- message: enabled
679
- ? "Telemetry disabled. Takes effect next session."
680
- : "Telemetry enabled. Thank you!",
678
+ message: enabled ? "Telemetry disabled. Takes effect next session." : "Telemetry enabled. Thank you!",
681
679
  variant: "info",
682
680
  })
683
681
  dialog.clear()
@@ -86,15 +86,23 @@ export namespace Provider {
86
86
  options?: Record<string, any>
87
87
  }>
88
88
 
89
- function createSSEStream(content: string, model: string, usage?: { prompt_tokens: number; completion_tokens: number; total_tokens: number }): string[] {
89
+ function createSSEStream(
90
+ content: string,
91
+ model: string,
92
+ usage?: { prompt_tokens: number; completion_tokens: number; total_tokens: number },
93
+ ): string[] {
90
94
  const id = `chatcmpl-${crypto.randomUUID()}`
91
95
  const chunks: string[] = []
92
96
 
93
97
  // Role chunk
94
- chunks.push(`data: ${JSON.stringify({
95
- id, object: "chat.completion.chunk", model,
96
- choices: [{ index: 0, delta: { role: "assistant", content: "" }, finish_reason: null }],
97
- })}\n\n`)
98
+ chunks.push(
99
+ `data: ${JSON.stringify({
100
+ id,
101
+ object: "chat.completion.chunk",
102
+ model,
103
+ choices: [{ index: 0, delta: { role: "assistant", content: "" }, finish_reason: null }],
104
+ })}\n\n`,
105
+ )
98
106
 
99
107
  // Content chunks (~10 chars per chunk for smooth streaming
100
108
  const words = content.split(/(\s+)/)
@@ -102,26 +110,38 @@ export namespace Provider {
102
110
  for (const word of words) {
103
111
  current += word
104
112
  if (current.length >= 10 || word.includes("\n")) {
105
- chunks.push(`data: ${JSON.stringify({
106
- id, object: "chat.completion.chunk", model,
107
- choices: [{ index: 0, delta: { content: current }, finish_reason: null }],
108
- })}\n\n`)
113
+ chunks.push(
114
+ `data: ${JSON.stringify({
115
+ id,
116
+ object: "chat.completion.chunk",
117
+ model,
118
+ choices: [{ index: 0, delta: { content: current }, finish_reason: null }],
119
+ })}\n\n`,
120
+ )
109
121
  current = ""
110
122
  }
111
123
  }
112
124
  if (current) {
113
- chunks.push(`data: ${JSON.stringify({
114
- id, object: "chat.completion.chunk", model,
115
- choices: [{ index: 0, delta: { content: current }, finish_reason: null }],
116
- })}\n\n`)
125
+ chunks.push(
126
+ `data: ${JSON.stringify({
127
+ id,
128
+ object: "chat.completion.chunk",
129
+ model,
130
+ choices: [{ index: 0, delta: { content: current }, finish_reason: null }],
131
+ })}\n\n`,
132
+ )
117
133
  }
118
134
 
119
135
  // Finish chunk
120
- chunks.push(`data: ${JSON.stringify({
121
- id, object: "chat.completion.chunk", model,
122
- choices: [{ index: 0, delta: {}, finish_reason: "stop" }],
123
- usage: usage || { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },
124
- })}\n\n`)
136
+ chunks.push(
137
+ `data: ${JSON.stringify({
138
+ id,
139
+ object: "chat.completion.chunk",
140
+ model,
141
+ choices: [{ index: 0, delta: {}, finish_reason: "stop" }],
142
+ usage: usage || { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },
143
+ })}\n\n`,
144
+ )
125
145
 
126
146
  chunks.push("data: [DONE]\n\n")
127
147
  return chunks
@@ -538,6 +558,82 @@ export namespace Provider {
538
558
  },
539
559
  }
540
560
  },
561
+ ollama: async (input) => {
562
+ const host = (Env.get("OLLAMA_HOST") || "http://localhost:11434").replace(/\/+$/, "")
563
+ try {
564
+ const response = await fetch(`${host}/api/tags`, {
565
+ signal: AbortSignal.timeout(2000),
566
+ })
567
+ if (!response.ok) return { autoload: false }
568
+ const data = (await response.json()) as { models?: Array<{ name: string; details?: { parameter_size?: string; family?: string } }> }
569
+ if (!data.models?.length) return { autoload: false }
570
+
571
+ for (const model of data.models) {
572
+ if (input.models[model.name]) continue
573
+ input.models[model.name] = fromModelsDevModel(
574
+ { id: "ollama", name: "Ollama", npm: "@ai-sdk/openai-compatible", api: `${host}/v1`, env: ["OLLAMA_HOST"], models: {} },
575
+ {
576
+ id: model.name,
577
+ name: model.name,
578
+ family: model.details?.family ?? model.name.split(":")[0],
579
+ attachment: false,
580
+ reasoning: false,
581
+ tool_call: true,
582
+ temperature: true,
583
+ release_date: "2024-01-01",
584
+ modalities: { input: ["text"], output: ["text"] },
585
+ cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 },
586
+ limit: { context: 131072, output: 32768 },
587
+ options: {},
588
+ },
589
+ )
590
+ }
591
+ return {
592
+ autoload: true,
593
+ options: { baseURL: `${host}/v1`, apiKey: "ollama" },
594
+ }
595
+ } catch {
596
+ return { autoload: false }
597
+ }
598
+ },
599
+ lmstudio: async (input) => {
600
+ const host = (Env.get("LMSTUDIO_HOST") || "http://127.0.0.1:1234").replace(/\/+$/, "")
601
+ try {
602
+ const response = await fetch(`${host}/v1/models`, {
603
+ signal: AbortSignal.timeout(2000),
604
+ })
605
+ if (!response.ok) return { autoload: false }
606
+ const data = (await response.json()) as { data?: Array<{ id: string }> }
607
+ if (!data.data?.length) return { autoload: false }
608
+
609
+ for (const model of data.data) {
610
+ if (input.models[model.id]) continue
611
+ input.models[model.id] = fromModelsDevModel(
612
+ { id: "lmstudio", name: "LM Studio", npm: "@ai-sdk/openai-compatible", api: `${host}/v1`, env: ["LMSTUDIO_HOST"], models: {} },
613
+ {
614
+ id: model.id,
615
+ name: model.id,
616
+ family: model.id.split("/").pop()?.split("-")[0] ?? model.id,
617
+ attachment: false,
618
+ reasoning: false,
619
+ tool_call: true,
620
+ temperature: true,
621
+ release_date: "2024-01-01",
622
+ modalities: { input: ["text"], output: ["text"] },
623
+ cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 },
624
+ limit: { context: 131072, output: 32768 },
625
+ options: {},
626
+ },
627
+ )
628
+ }
629
+ return {
630
+ autoload: true,
631
+ options: { baseURL: `${host}/v1`, apiKey: "lm-studio" },
632
+ }
633
+ } catch {
634
+ return { autoload: false }
635
+ }
636
+ },
541
637
  cerebras: async () => {
542
638
  return {
543
639
  autoload: false,
@@ -565,7 +661,12 @@ export namespace Provider {
565
661
  })
566
662
  if (response.ok) {
567
663
  const data = await response.json()
568
- if (data.success && data.instance?.instanceUrl && data.instance?.clientId && data.instance?.clientSecret) {
664
+ if (
665
+ data.success &&
666
+ data.instance?.instanceUrl &&
667
+ data.instance?.clientId &&
668
+ data.instance?.clientSecret
669
+ ) {
569
670
  await Auth.set("servicenow", {
570
671
  type: "servicenow-oauth",
571
672
  instance: data.instance.instanceUrl,
@@ -777,6 +878,16 @@ export namespace Provider {
777
878
  const modelsDev = await ModelsDev.get()
778
879
  const database = mapValues(modelsDev, fromModelsDevProvider)
779
880
 
881
+ // Ensure local providers exist in the database so CUSTOM_LOADERS can discover them
882
+ for (const local of [
883
+ { id: "ollama", name: "Ollama", npm: "@ai-sdk/openai-compatible", api: "http://localhost:11434/v1", env: ["OLLAMA_HOST"] },
884
+ { id: "lmstudio", name: "LM Studio", npm: "@ai-sdk/openai-compatible", api: "http://127.0.0.1:1234/v1", env: ["LMSTUDIO_HOST"] },
885
+ ] as const) {
886
+ if (!database[local.id]) {
887
+ database[local.id] = fromModelsDevProvider({ id: local.id, name: local.name, npm: local.npm, api: local.api, env: [...local.env], models: {} })
888
+ }
889
+ }
890
+
780
891
  const disabled = new Set(config.disabled_providers ?? [])
781
892
  const enabled = config.enabled_providers ? new Set(config.enabled_providers) : null
782
893
 
@@ -1201,10 +1312,10 @@ export namespace Provider {
1201
1312
  // Error response
1202
1313
  else if (unwrapped && unwrapped.success === false && unwrapped.error) {
1203
1314
  log.warn("servicenow-llm: ServiceNow error", { error: unwrapped.error })
1204
- return new Response(
1205
- JSON.stringify({ error: { message: unwrapped.error, type: "servicenow_error" } }),
1206
- { status: 500, headers: { "content-type": "application/json" } },
1207
- )
1315
+ return new Response(JSON.stringify({ error: { message: unwrapped.error, type: "servicenow_error" } }), {
1316
+ status: 500,
1317
+ headers: { "content-type": "application/json" },
1318
+ })
1208
1319
  }
1209
1320
  }
1210
1321
  // Direct OpenAI format (no wrapper)
@@ -1234,7 +1345,7 @@ export namespace Provider {
1234
1345
  headers: {
1235
1346
  "content-type": "text/event-stream",
1236
1347
  "cache-control": "no-cache",
1237
- "connection": "keep-alive",
1348
+ connection: "keep-alive",
1238
1349
  },
1239
1350
  })
1240
1351
  } else {
package/src/pty/index.ts CHANGED
@@ -40,6 +40,7 @@ export namespace Pty {
40
40
  cwd: z.string().optional(),
41
41
  title: z.string().optional(),
42
42
  env: z.record(z.string(), z.string()).optional(),
43
+ mode: z.enum(["shell", "tui"]).optional(),
43
44
  })
44
45
 
45
46
  export type CreateInput = z.infer<typeof CreateInput>
@@ -95,15 +96,35 @@ export namespace Pty {
95
96
 
96
97
  export async function create(input: CreateInput) {
97
98
  const id = Identifier.create("pty", false)
98
- const command = input.command || Shell.preferred()
99
- const args = input.args || []
100
- if (command.endsWith("sh")) {
99
+ let command = input.command || Shell.preferred()
100
+ let args = input.args || []
101
+ let cwd = input.cwd || Instance.directory
102
+ let extraEnv: Record<string, string> = {}
103
+
104
+ if (input.mode === "tui") {
105
+ const packageRoot = new URL("../../", import.meta.url).pathname.replace(/\/$/, "")
106
+ command = "bun"
107
+ args = [
108
+ "run",
109
+ "--conditions=browser",
110
+ "src/index.ts",
111
+ "--connect",
112
+ `http://127.0.0.1:${process.env.PORT || "4096"}`,
113
+ ]
114
+ cwd = packageRoot
115
+ extraEnv = {
116
+ OPENCODE_SKIP_THEME_DETECTION: "1",
117
+ OPENCODE_DISABLE_KITTY_KEYBOARD: "1",
118
+ COLORTERM: "truecolor",
119
+ FORCE_COLOR: "3",
120
+ }
121
+ } else if (command.endsWith("sh")) {
101
122
  args.push("-l")
102
123
  }
103
124
 
104
- const cwd = input.cwd || Instance.directory
105
125
  const env = {
106
126
  ...process.env,
127
+ ...extraEnv,
107
128
  ...input.env,
108
129
  TERM: "xterm-256color",
109
130
  SNOW_CODE_TERMINAL: "1",
@@ -40,21 +40,25 @@ async function spawnTui(cols: number, rows: number, env?: Record<string, string>
40
40
  const { spawn } = await import("bun-pty")
41
41
  const id = crypto.randomUUID()
42
42
 
43
- const ptyProcess = spawn("bun", ["run", "--conditions=browser", "src/index.ts", "--connect", "http://localhost:4096"], {
44
- name: "xterm-256color",
45
- cols,
46
- rows,
47
- cwd: new URL("../../../", import.meta.url).pathname.replace(/\/$/, ""),
48
- env: {
49
- ...process.env,
50
- ...env,
51
- TERM: "xterm-256color",
52
- COLORTERM: "truecolor",
53
- FORCE_COLOR: "3",
54
- OPENCODE_REMOTE_TUI: "1",
55
- OPENCODE_DISABLE_KITTY_KEYBOARD: "1",
43
+ const ptyProcess = spawn(
44
+ "bun",
45
+ ["run", "--conditions=browser", "src/index.ts", "--connect", "http://localhost:4096"],
46
+ {
47
+ name: "xterm-256color",
48
+ cols,
49
+ rows,
50
+ cwd: new URL("../../../", import.meta.url).pathname.replace(/\/$/, ""),
51
+ env: {
52
+ ...process.env,
53
+ ...env,
54
+ TERM: "xterm-256color",
55
+ COLORTERM: "truecolor",
56
+ FORCE_COLOR: "3",
57
+ OPENCODE_SKIP_THEME_DETECTION: "1",
58
+ OPENCODE_DISABLE_KITTY_KEYBOARD: "1",
59
+ },
56
60
  },
57
- })
61
+ )
58
62
 
59
63
  const session: TuiSession = { pty: ptyProcess, lastActivity: Date.now() }
60
64
  sessions.set(id, session)