@xelauvas/xela-cli 0.3.0 → 0.4.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.
Files changed (136) hide show
  1. package/local/README.md +112 -0
  2. package/local/start-qwen.sh +47 -0
  3. package/local/start-tunnel.sh +36 -0
  4. package/local/xela-local.sh +139 -0
  5. package/package.json +19 -5
  6. package/run.sh +33 -0
  7. package/src/Tool.ts +1 -1
  8. package/src/_shims/_generated_stubs/_universal_stub.mjs +168 -0
  9. package/src/_shims/_generated_stubs/stub_346f96741420f62caece15b196e233c6.mjs +19 -0
  10. package/src/_shims/_generated_stubs/stub_4b79af2a6ac1af5a53a55fd666b2bbb4.mjs +3 -0
  11. package/src/_shims/_generated_stubs/stub_94345ddeadacc184ff895597ec5abf71.mjs +6 -0
  12. package/src/_shims/_generated_stubs/stub_99fa0bc272643c549d8a2ec6d578436f.mjs +3 -0
  13. package/src/_shims/_generated_stubs/stub_e3451b131fcaf272a2a75270b9a427a3.mjs +9 -0
  14. package/src/_shims/_generated_stubs/text_stub.mjs +1 -0
  15. package/src/_shims/bun_bundle.js +79 -4
  16. package/src/_shims/cjs_stub.cjs +32 -10
  17. package/src/_shims/loader.js +104 -5
  18. package/src/_shims/openai_adapter.ts +446 -230
  19. package/src/_shims/register.js +16 -3
  20. package/src/assistant/gate.ts +15 -0
  21. package/src/assistant/index.ts +89 -0
  22. package/src/bootstrap/state.ts +6 -1
  23. package/src/bridge/peerSessions.ts +18 -0
  24. package/src/cli/print.ts +5 -0
  25. package/src/commands/assistant/assistant.ts +17 -0
  26. package/src/commands/assistant/index.ts +11 -0
  27. package/src/commands/brief.ts +3 -1
  28. package/src/commands/buddy/buddy.ts +88 -0
  29. package/src/commands/buddy/index.ts +12 -0
  30. package/src/commands/model/model.tsx +8 -23
  31. package/src/commands/peers/index.ts +11 -0
  32. package/src/commands/peers/peers.ts +24 -0
  33. package/src/commands/proactive/proactive.ts +64 -0
  34. package/src/commands/proactive.ts +12 -0
  35. package/src/commands/workflows/index.ts +12 -0
  36. package/src/commands/workflows/workflows.ts +36 -0
  37. package/src/commands.ts +4 -8
  38. package/src/components/Messages.tsx +1 -1
  39. package/src/components/ModelPicker.tsx +4 -34
  40. package/src/components/Spinner.tsx +54 -1
  41. package/src/components/StructuredDiff/colorDiff.ts +3 -1
  42. package/src/components/messages/SnipBoundaryMessage.tsx +15 -0
  43. package/src/components/messages/UserCrossSessionMessage.tsx +26 -0
  44. package/src/components/permissions/PermissionRequest.tsx +4 -2
  45. package/src/components/permissions/ReviewArtifactPermissionRequest/ReviewArtifactPermissionRequest.tsx +96 -0
  46. package/src/constants/prompts.ts +26 -0
  47. package/src/ink/terminal.ts +9 -1
  48. package/src/jobs/classifier.ts +103 -0
  49. package/src/memdir/paths.ts +13 -1
  50. package/src/proactive/index.ts +158 -0
  51. package/src/proactive/useProactive.ts +78 -0
  52. package/src/screens/REPL.tsx +28 -8
  53. package/src/services/api/claude.ts +6 -1
  54. package/src/services/compact/autoCompact.ts +18 -4
  55. package/src/services/compact/cachedMCConfig.ts +1 -0
  56. package/src/services/compact/cachedMicrocompact.ts +113 -0
  57. package/src/services/compact/compact.ts +22 -2
  58. package/src/services/compact/reactiveCompact.ts +251 -0
  59. package/src/services/compact/snipCompact.ts +282 -0
  60. package/src/services/compact/snipProjection.ts +50 -0
  61. package/src/services/contextCollapse/index.ts +322 -0
  62. package/src/services/contextCollapse/operations.ts +187 -0
  63. package/src/services/contextCollapse/persist.ts +40 -0
  64. package/src/services/extractMemories/extractMemories.ts +11 -2
  65. package/src/services/skillSearch/featureCheck.ts +12 -0
  66. package/src/services/skillSearch/localSearch.ts +98 -0
  67. package/src/services/skillSearch/prefetch.ts +125 -0
  68. package/src/services/skillSearch/remoteSkillLoader.ts +9 -0
  69. package/src/services/skillSearch/remoteSkillState.ts +21 -0
  70. package/src/services/skillSearch/signals.ts +7 -0
  71. package/src/services/skillSearch/telemetry.ts +9 -0
  72. package/src/setup.ts +10 -5
  73. package/src/skills/bundled/dream.ts +31 -0
  74. package/src/skills/bundled/hunter.ts +45 -0
  75. package/src/skills/bundled/index.ts +10 -23
  76. package/src/skills/bundled/runSkillGenerator.ts +61 -0
  77. package/src/skills/mcpSkills.ts +101 -0
  78. package/src/tasks/LocalWorkflowTask/LocalWorkflowTask.ts +301 -0
  79. package/src/tasks/MonitorMcpTask/MonitorMcpTask.ts +273 -0
  80. package/src/tasks.ts +22 -8
  81. package/src/tools/AgentTool/runAgent.ts +11 -0
  82. package/src/tools/BriefTool/BriefTool.ts +2 -0
  83. package/src/tools/CtxInspectTool/CtxInspectTool.ts +102 -0
  84. package/src/tools/DiscoverSkillsTool/DiscoverSkillsTool.ts +78 -0
  85. package/src/tools/DiscoverSkillsTool/prompt.ts +3 -0
  86. package/src/tools/ListPeersTool/ListPeersTool.ts +89 -0
  87. package/src/tools/MCPTool/MCPTool.ts +6 -0
  88. package/src/tools/MonitorTool/MonitorTool.ts +273 -0
  89. package/src/tools/MonitorTool/UI.tsx +25 -0
  90. package/src/tools/MonitorTool/events.ts +84 -0
  91. package/src/tools/MonitorTool/prompt.ts +45 -0
  92. package/src/tools/OverflowTestTool/OverflowTestTool.ts +73 -0
  93. package/src/tools/PushNotificationTool/PushNotificationTool.ts +119 -0
  94. package/src/tools/ReviewArtifactTool/ReviewArtifactTool.ts +93 -0
  95. package/src/tools/ReviewArtifactTool/prompt.ts +5 -0
  96. package/src/tools/SendUserFileTool/SendUserFileTool.ts +87 -0
  97. package/src/tools/SendUserFileTool/prompt.ts +7 -0
  98. package/src/tools/SleepTool/SleepTool.ts +126 -0
  99. package/src/tools/SnipTool/SnipTool.ts +90 -0
  100. package/src/tools/SnipTool/prompt.ts +5 -0
  101. package/src/tools/SubscribePRTool/SubscribePRTool.ts +115 -0
  102. package/src/tools/TerminalCaptureTool/TerminalCaptureTool.ts +129 -0
  103. package/src/tools/TerminalCaptureTool/prompt.ts +8 -0
  104. package/src/tools/ToolSearchTool/prompt.ts +1 -1
  105. package/src/tools/VerifyPlanExecutionTool/VerifyPlanExecutionTool.ts +99 -0
  106. package/src/tools/WebBrowserTool/WebBrowserPanel.tsx +10 -0
  107. package/src/tools/WebBrowserTool/WebBrowserTool.ts +190 -0
  108. package/src/tools/WebBrowserTool/prompt.ts +16 -0
  109. package/src/tools/WorkflowTool/UI.tsx +27 -0
  110. package/src/tools/WorkflowTool/WorkflowTool.ts +401 -0
  111. package/src/tools/WorkflowTool/bundled/index.ts +88 -0
  112. package/src/tools/WorkflowTool/constants.ts +7 -0
  113. package/src/tools/WorkflowTool/createWorkflowCommand.ts +84 -0
  114. package/src/tools/WorkflowTool/prompt.ts +38 -0
  115. package/src/tools/WorkflowTool/runtime.ts +331 -0
  116. package/src/tools.ts +36 -31
  117. package/src/utils/betas.ts +5 -0
  118. package/src/utils/context.ts +9 -0
  119. package/src/utils/conversationRecovery.ts +1 -1
  120. package/src/utils/forkedAgent.ts +4 -0
  121. package/src/utils/logoV2Utils.ts +7 -3
  122. package/src/utils/messageQueueManager.ts +23 -9
  123. package/src/utils/model/model.ts +10 -9
  124. package/src/utils/model/modelOptions.ts +0 -21
  125. package/src/utils/model/validateModel.ts +0 -19
  126. package/src/utils/permissions/permissionSetup.ts +5 -0
  127. package/src/utils/permissions/permissions.ts +25 -0
  128. package/src/utils/permissions/yolo-classifier-prompts/auto_mode_system_prompt.txt +38 -0
  129. package/src/utils/permissions/yolo-classifier-prompts/permissions_external.txt +25 -0
  130. package/src/utils/permissions/yoloClassifier.ts +28 -6
  131. package/src/utils/plugins/officialMarketplaceStartupCheck.ts +9 -0
  132. package/src/utils/systemThemeWatcher.ts +62 -0
  133. package/src/utils/udsClient.ts +115 -0
  134. package/src/utils/udsMessaging.ts +157 -0
  135. package/src/utils/yaml.ts +251 -5
  136. package/src/utils/model/providerModels.ts +0 -168
@@ -0,0 +1,331 @@
1
+ import vm from 'node:vm'
2
+ import { DEFAULT_CONCURRENCY } from './constants.js'
3
+
4
+ export type WorkflowMeta = {
5
+ name: string
6
+ description: string
7
+ phases?: Array<{ title: string; detail?: string }>
8
+ }
9
+
10
+ export type WorkflowAgentOptions = {
11
+ label?: string
12
+ phase?: string
13
+ schema?: unknown
14
+ model?: string
15
+ subagent_type?: string
16
+ allowedTools?: string[]
17
+ maxTurns?: number
18
+ }
19
+
20
+ export type WorkflowHost = {
21
+ /** Run one subagent to completion and return its final text. */
22
+ runAgent: (
23
+ prompt: string,
24
+ options: WorkflowAgentOptions,
25
+ agentIndex: number,
26
+ ) => Promise<string>
27
+ onPhase?: (title: string) => void
28
+ onLog?: (line: string) => void
29
+ signal: AbortSignal
30
+ maxAgents: number
31
+ }
32
+
33
+ export class WorkflowScriptError extends Error {}
34
+
35
+ const FORBIDDEN = [
36
+ { re: /^\s*import\s/m, what: 'import statements' },
37
+ { re: /\brequire\s*\(/, what: 'require()' },
38
+ { re: /\bprocess\s*\./, what: 'process' },
39
+ { re: /\beval\s*\(/, what: 'eval()' },
40
+ { re: /\bFunction\s*\(/, what: 'Function()' },
41
+ ]
42
+
43
+ /**
44
+ * Splits the script into its `export const meta = {...}` literal and the body.
45
+ * The literal is evaluated in an empty context; anything that is not a plain
46
+ * object literal fails there, which is the "pure literal" rule.
47
+ */
48
+ export function parseWorkflowScript(script: string): {
49
+ meta: WorkflowMeta
50
+ body: string
51
+ } {
52
+ for (const { re, what } of FORBIDDEN) {
53
+ if (re.test(script)) {
54
+ throw new WorkflowScriptError(`Workflow scripts cannot use ${what}`)
55
+ }
56
+ }
57
+ const match = /export\s+const\s+meta\s*=\s*/.exec(script)
58
+ if (!match) {
59
+ throw new WorkflowScriptError(
60
+ 'Workflow script must begin with `export const meta = {...}`',
61
+ )
62
+ }
63
+ const start = match.index + match[0].length
64
+ const end = findBalancedEnd(script, start)
65
+ if (end < 0) {
66
+ throw new WorkflowScriptError('Unterminated meta object literal')
67
+ }
68
+ const literal = script.slice(start, end + 1)
69
+ let meta: unknown
70
+ try {
71
+ meta = vm.runInNewContext(`(${literal})`, Object.create(null), {
72
+ timeout: 1000,
73
+ })
74
+ } catch (e) {
75
+ throw new WorkflowScriptError(
76
+ `meta must be a pure object literal: ${(e as Error).message}`,
77
+ )
78
+ }
79
+ if (
80
+ !meta ||
81
+ typeof meta !== 'object' ||
82
+ typeof (meta as WorkflowMeta).name !== 'string' ||
83
+ typeof (meta as WorkflowMeta).description !== 'string'
84
+ ) {
85
+ throw new WorkflowScriptError(
86
+ 'meta needs string `name` and `description` fields',
87
+ )
88
+ }
89
+ const body = script.slice(0, match.index) + script.slice(end + 1)
90
+ return {
91
+ meta: JSON.parse(JSON.stringify(meta)) as WorkflowMeta,
92
+ body: body.replace(/^\s*;/, ''),
93
+ }
94
+ }
95
+
96
+ function findBalancedEnd(text: string, openIndex: number): number {
97
+ if (text[openIndex] !== '{') return -1
98
+ let depth = 0
99
+ let quote: string | null = null
100
+ for (let i = openIndex; i < text.length; i++) {
101
+ const ch = text[i]!
102
+ if (quote) {
103
+ if (ch === '\\') i++
104
+ else if (ch === quote) quote = null
105
+ continue
106
+ }
107
+ if (ch === '"' || ch === "'" || ch === '`') quote = ch
108
+ else if (ch === '{') depth++
109
+ else if (ch === '}') {
110
+ depth--
111
+ if (depth === 0) return i
112
+ }
113
+ }
114
+ return -1
115
+ }
116
+
117
+ /** Pulls the first JSON object/array out of a model answer (tolerates prose and code fences). */
118
+ export function extractJson(text: string): unknown {
119
+ const fenced = /```(?:json)?\s*([\s\S]*?)```/i.exec(text)
120
+ const candidates = [fenced?.[1], text].filter(
121
+ (s): s is string => typeof s === 'string',
122
+ )
123
+ for (const candidate of candidates) {
124
+ const trimmed = candidate.trim()
125
+ try {
126
+ return JSON.parse(trimmed)
127
+ } catch {
128
+ // fall through to a bracket scan
129
+ }
130
+ for (const open of ['{', '[']) {
131
+ const close = open === '{' ? '}' : ']'
132
+ const first = trimmed.indexOf(open)
133
+ const last = trimmed.lastIndexOf(close)
134
+ if (first >= 0 && last > first) {
135
+ try {
136
+ return JSON.parse(trimmed.slice(first, last + 1))
137
+ } catch {
138
+ // try the other bracket
139
+ }
140
+ }
141
+ }
142
+ }
143
+ throw new WorkflowScriptError(
144
+ `Agent did not return JSON: ${text.slice(0, 200)}`,
145
+ )
146
+ }
147
+
148
+ async function runWithConcurrency<T>(
149
+ thunks: Array<() => Promise<T>>,
150
+ concurrency: number,
151
+ signal: AbortSignal,
152
+ onError: (index: number, err: unknown) => void,
153
+ ): Promise<Array<T | null>> {
154
+ const results: Array<T | null> = new Array(thunks.length).fill(null)
155
+ let next = 0
156
+ const worker = async (): Promise<void> => {
157
+ while (next < thunks.length) {
158
+ if (signal.aborted) return
159
+ const index = next++
160
+ try {
161
+ results[index] = await thunks[index]!()
162
+ } catch (err) {
163
+ onError(index, err)
164
+ results[index] = null
165
+ }
166
+ }
167
+ }
168
+ const workers = Array.from(
169
+ { length: Math.max(1, Math.min(concurrency, thunks.length)) },
170
+ () => worker(),
171
+ )
172
+ await Promise.all(workers)
173
+ return results
174
+ }
175
+
176
+ /**
177
+ * Runs the workflow body inside a vm context with the script API as globals.
178
+ * Resolves to whatever the script returns.
179
+ */
180
+ export async function runWorkflowScript(
181
+ body: string,
182
+ args: unknown,
183
+ host: WorkflowHost,
184
+ ): Promise<unknown> {
185
+ let agentCount = 0
186
+ const log = (...parts: unknown[]): void => {
187
+ host.onLog?.(
188
+ parts
189
+ .map(p => (typeof p === 'string' ? p : safeStringify(p)))
190
+ .join(' '),
191
+ )
192
+ }
193
+
194
+ const agent = async (
195
+ prompt: unknown,
196
+ options: WorkflowAgentOptions = {},
197
+ ): Promise<unknown> => {
198
+ if (typeof prompt !== 'string' || prompt.trim() === '') {
199
+ throw new WorkflowScriptError('agent(prompt) needs a non-empty string')
200
+ }
201
+ if (host.signal.aborted) throw new WorkflowScriptError('Workflow aborted')
202
+ if (agentCount >= host.maxAgents) {
203
+ throw new WorkflowScriptError(
204
+ `Workflow exceeded max_agents (${host.maxAgents}); raise max_agents or launch fewer agents`,
205
+ )
206
+ }
207
+ const index = agentCount++
208
+ if (options.phase) host.onPhase?.(options.phase)
209
+ let fullPrompt = prompt
210
+ if (options.schema) {
211
+ fullPrompt +=
212
+ `\n\nRespond with ONLY a JSON value matching this JSON Schema, no prose, no markdown fences:\n` +
213
+ safeStringify(options.schema)
214
+ }
215
+ const text = await host.runAgent(fullPrompt, options, index)
216
+ return options.schema ? extractJson(text) : text
217
+ }
218
+
219
+ const parallel = async (
220
+ items: unknown,
221
+ options: { concurrency?: number } = {},
222
+ ): Promise<unknown[]> => {
223
+ if (!Array.isArray(items)) {
224
+ throw new WorkflowScriptError('parallel() expects an array of () => Promise')
225
+ }
226
+ const thunks = items.map((item, i) => {
227
+ if (typeof item === 'function') return item as () => Promise<unknown>
228
+ if (item && typeof (item as Promise<unknown>).then === 'function') {
229
+ return () => item as Promise<unknown>
230
+ }
231
+ throw new WorkflowScriptError(
232
+ `parallel(): item ${i} is neither a function nor a promise`,
233
+ )
234
+ })
235
+ return runWithConcurrency(
236
+ thunks,
237
+ options.concurrency ?? DEFAULT_CONCURRENCY,
238
+ host.signal,
239
+ (i, err) => log(`parallel[${i}] failed: ${errorText(err)}`),
240
+ )
241
+ }
242
+
243
+ const pipeline = async (
244
+ items: unknown,
245
+ ...stages: unknown[]
246
+ ): Promise<unknown[]> => {
247
+ if (!Array.isArray(items)) {
248
+ throw new WorkflowScriptError('pipeline() expects an array as first argument')
249
+ }
250
+ if (stages.length === 0 || stages.some(s => typeof s !== 'function')) {
251
+ throw new WorkflowScriptError('pipeline() needs one or more stage functions')
252
+ }
253
+ const fns = stages as Array<(input: unknown) => Promise<unknown>>
254
+ const thunks = items.map(item => async () => {
255
+ let value: unknown = item
256
+ for (const stage of fns) {
257
+ value = await stage(value)
258
+ }
259
+ return value
260
+ })
261
+ return runWithConcurrency(thunks, DEFAULT_CONCURRENCY, host.signal, (i, err) =>
262
+ log(`pipeline[${i}] failed: ${errorText(err)}`),
263
+ )
264
+ }
265
+
266
+ const phase = async (title: unknown, fn?: unknown): Promise<unknown> => {
267
+ if (typeof title === 'string') host.onPhase?.(title)
268
+ if (typeof fn === 'function') return (fn as () => Promise<unknown>)()
269
+ return undefined
270
+ }
271
+
272
+ const sleep = (ms: unknown): Promise<void> =>
273
+ new Promise(resolve => {
274
+ const t = setTimeout(resolve, Math.max(0, Number(ms) || 0))
275
+ t.unref()
276
+ })
277
+
278
+ const sandbox: Record<string, unknown> = {
279
+ agent,
280
+ parallel,
281
+ pipeline,
282
+ phase,
283
+ sleep,
284
+ log,
285
+ console: { log, info: log, warn: log, error: log },
286
+ args: args === undefined ? undefined : JSON.parse(JSON.stringify(args)),
287
+ JSON,
288
+ Math,
289
+ Date,
290
+ Promise,
291
+ Array,
292
+ Object,
293
+ String,
294
+ Number,
295
+ Boolean,
296
+ RegExp,
297
+ Error,
298
+ Set,
299
+ Map,
300
+ setTimeout,
301
+ clearTimeout,
302
+ structuredClone,
303
+ }
304
+ const context = vm.createContext(sandbox, { name: 'workflow' })
305
+ const wrapped = `(async () => {\n${body}\n})()`
306
+ const script = new vm.Script(wrapped, { filename: 'workflow.js' })
307
+ const resultPromise = script.runInContext(context) as Promise<unknown>
308
+ return await Promise.race([
309
+ resultPromise,
310
+ new Promise<never>((_, reject) => {
311
+ if (host.signal.aborted) reject(new WorkflowScriptError('Workflow aborted'))
312
+ host.signal.addEventListener(
313
+ 'abort',
314
+ () => reject(new WorkflowScriptError('Workflow aborted')),
315
+ { once: true },
316
+ )
317
+ }),
318
+ ])
319
+ }
320
+
321
+ export function errorText(err: unknown): string {
322
+ return err instanceof Error ? err.message : String(err)
323
+ }
324
+
325
+ export function safeStringify(value: unknown): string {
326
+ try {
327
+ return JSON.stringify(value, null, 2) ?? String(value)
328
+ } catch {
329
+ return String(value)
330
+ }
331
+ }
package/src/tools.ts CHANGED
@@ -23,9 +23,7 @@ const SuggestBackgroundPRTool =
23
23
  .SuggestBackgroundPRTool
24
24
  : null
25
25
  const SleepTool =
26
- feature('PROACTIVE') || feature('KAIROS')
27
- ? require('./tools/SleepTool/SleepTool.js').SleepTool
28
- : null
26
+ feature('PROACTIVE') || feature('KAIROS') ? SleepToolImpl : null
29
27
  const cronTools = feature('AGENT_TRIGGERS')
30
28
  ? [
31
29
  require('./tools/ScheduleCronTool/CronCreateTool.js').CronCreateTool,
@@ -36,20 +34,15 @@ const cronTools = feature('AGENT_TRIGGERS')
36
34
  const RemoteTriggerTool = feature('AGENT_TRIGGERS_REMOTE')
37
35
  ? require('./tools/RemoteTriggerTool/RemoteTriggerTool.js').RemoteTriggerTool
38
36
  : null
39
- const MonitorTool = feature('MONITOR_TOOL')
40
- ? require('./tools/MonitorTool/MonitorTool.js').MonitorTool
41
- : null
42
- const SendUserFileTool = feature('KAIROS')
43
- ? require('./tools/SendUserFileTool/SendUserFileTool.js').SendUserFileTool
44
- : null
37
+ // Static import (not require): the Monitor shares the notification queue and
38
+ // task registry with the main loop, and the CJS require shim would hand it
39
+ // private copies of those modules.
40
+ const MonitorTool = feature('MONITOR_TOOL') ? MonitorToolImpl : null
41
+ const SendUserFileTool =
42
+ feature('KAIROS') || feature('KAIROS_PUSH_NOTIFICATION') ? SendUserFileToolImpl : null
45
43
  const PushNotificationTool =
46
- feature('KAIROS') || feature('KAIROS_PUSH_NOTIFICATION')
47
- ? require('./tools/PushNotificationTool/PushNotificationTool.js')
48
- .PushNotificationTool
49
- : null
50
- const SubscribePRTool = feature('KAIROS_GITHUB_WEBHOOKS')
51
- ? require('./tools/SubscribePRTool/SubscribePRTool.js').SubscribePRTool
52
- : null
44
+ feature('KAIROS') || feature('KAIROS_PUSH_NOTIFICATION') ? PushNotificationToolImpl : null
45
+ const SubscribePRTool = feature('KAIROS_GITHUB_WEBHOOKS') ? SubscribePRToolImpl : null
53
46
  /* eslint-enable custom-rules/no-process-env-top-level, @typescript-eslint/no-require-imports */
54
47
  import { TaskOutputTool } from './tools/TaskOutputTool/TaskOutputTool.js'
55
48
  import { WebSearchTool } from './tools/WebSearchTool/WebSearchTool.js'
@@ -90,8 +83,7 @@ import { isTodoV2Enabled } from './utils/tasks.js'
90
83
  /* eslint-disable custom-rules/no-process-env-top-level, @typescript-eslint/no-require-imports */
91
84
  const VerifyPlanExecutionTool =
92
85
  process.env.CLAUDE_CODE_VERIFY_PLAN === 'true'
93
- ? require('./tools/VerifyPlanExecutionTool/VerifyPlanExecutionTool.js')
94
- .VerifyPlanExecutionTool
86
+ ? VerifyPlanExecutionToolImpl
95
87
  : null
96
88
  /* eslint-enable custom-rules/no-process-env-top-level, @typescript-eslint/no-require-imports */
97
89
  import { SYNTHETIC_OUTPUT_TOOL_NAME } from './tools/SyntheticOutputTool/SyntheticOutputTool.js'
@@ -104,32 +96,29 @@ export {
104
96
  import { feature } from 'bun:bundle'
105
97
  // Dead code elimination: conditional import for OVERFLOW_TEST_TOOL
106
98
  /* eslint-disable custom-rules/no-process-env-top-level, @typescript-eslint/no-require-imports */
107
- const OverflowTestTool = feature('OVERFLOW_TEST_TOOL')
108
- ? require('./tools/OverflowTestTool/OverflowTestTool.js').OverflowTestTool
109
- : null
110
- const CtxInspectTool = feature('CONTEXT_COLLAPSE')
111
- ? require('./tools/CtxInspectTool/CtxInspectTool.js').CtxInspectTool
112
- : null
99
+ const OverflowTestTool = feature('OVERFLOW_TEST_TOOL') ? OverflowTestToolImpl : null
100
+ const CtxInspectTool = feature('CONTEXT_COLLAPSE') ? CtxInspectToolImpl : null
113
101
  const TerminalCaptureTool = feature('TERMINAL_PANEL')
114
- ? require('./tools/TerminalCaptureTool/TerminalCaptureTool.js')
115
- .TerminalCaptureTool
102
+ ? TerminalCaptureToolImpl
116
103
  : null
117
104
  const WebBrowserTool = feature('WEB_BROWSER_TOOL')
118
- ? require('./tools/WebBrowserTool/WebBrowserTool.js').WebBrowserTool
105
+ ? WebBrowserToolImpl
119
106
  : null
120
107
  const coordinatorModeModule = feature('COORDINATOR_MODE')
121
108
  ? (require('./coordinator/coordinatorMode.js') as typeof import('./coordinator/coordinatorMode.js'))
122
109
  : null
123
110
  const SnipTool = feature('HISTORY_SNIP')
124
- ? require('./tools/SnipTool/SnipTool.js').SnipTool
111
+ ? SnipToolImpl
125
112
  : null
126
113
  const ListPeersTool = feature('UDS_INBOX')
127
- ? require('./tools/ListPeersTool/ListPeersTool.js').ListPeersTool
114
+ ? ListPeersToolImpl
128
115
  : null
116
+ // Static import (see MonitorTool): the workflow shares the task registry and
117
+ // notification queue with the app
129
118
  const WorkflowTool = feature('WORKFLOW_SCRIPTS')
130
119
  ? (() => {
131
- require('./tools/WorkflowTool/bundled/index.js').initBundledWorkflows()
132
- return require('./tools/WorkflowTool/WorkflowTool.js').WorkflowTool
120
+ initBundledWorkflows()
121
+ return WorkflowToolImpl
133
122
  })()
134
123
  : null
135
124
  /* eslint-enable custom-rules/no-process-env-top-level, @typescript-eslint/no-require-imports */
@@ -140,6 +129,21 @@ import { isEnvTruthy } from './utils/envUtils.js'
140
129
  import { isPowerShellToolEnabled } from './utils/shell/shellToolUtils.js'
141
130
  import { isAgentSwarmsEnabled } from './utils/agentSwarmsEnabled.js'
142
131
  import { isWorktreeModeEnabled } from './utils/worktreeModeEnabled.js'
132
+ import { MonitorTool as MonitorToolImpl } from './tools/MonitorTool/MonitorTool.js'
133
+ import { SendUserFileTool as SendUserFileToolImpl } from './tools/SendUserFileTool/SendUserFileTool.js'
134
+ import { PushNotificationTool as PushNotificationToolImpl } from './tools/PushNotificationTool/PushNotificationTool.js'
135
+ import { SubscribePRTool as SubscribePRToolImpl } from './tools/SubscribePRTool/SubscribePRTool.js'
136
+ import { OverflowTestTool as OverflowTestToolImpl } from './tools/OverflowTestTool/OverflowTestTool.js'
137
+ import { SleepTool as SleepToolImpl } from './tools/SleepTool/SleepTool.js'
138
+ import { TerminalCaptureTool as TerminalCaptureToolImpl } from './tools/TerminalCaptureTool/TerminalCaptureTool.js'
139
+ import { WebBrowserTool as WebBrowserToolImpl } from './tools/WebBrowserTool/WebBrowserTool.js'
140
+ import { SnipTool as SnipToolImpl } from './tools/SnipTool/SnipTool.js'
141
+ import { ListPeersTool as ListPeersToolImpl } from './tools/ListPeersTool/ListPeersTool.js'
142
+ import { VerifyPlanExecutionTool as VerifyPlanExecutionToolImpl } from './tools/VerifyPlanExecutionTool/VerifyPlanExecutionTool.js'
143
+ import { CtxInspectTool as CtxInspectToolImpl } from './tools/CtxInspectTool/CtxInspectTool.js'
144
+ import { WorkflowTool as WorkflowToolImpl } from './tools/WorkflowTool/WorkflowTool.js'
145
+ import { initBundledWorkflows } from './tools/WorkflowTool/bundled/index.js'
146
+ import { DiscoverSkillsTool as DiscoverSkillsToolImpl } from './tools/DiscoverSkillsTool/DiscoverSkillsTool.js'
143
147
  import {
144
148
  REPL_TOOL_NAME,
145
149
  REPL_ONLY_TOOLS,
@@ -231,6 +235,7 @@ export function getAllBaseTools(): Tools {
231
235
  ...(VerifyPlanExecutionTool ? [VerifyPlanExecutionTool] : []),
232
236
  ...(process.env.USER_TYPE === 'ant' && REPLTool ? [REPLTool] : []),
233
237
  ...(WorkflowTool ? [WorkflowTool] : []),
238
+ ...(feature('EXPERIMENTAL_SKILL_SEARCH') ? [DiscoverSkillsToolImpl] : []),
234
239
  ...(SleepTool ? [SleepTool] : []),
235
240
  ...cronTools,
236
241
  ...(RemoteTriggerTool ? [RemoteTriggerTool] : []),
@@ -163,6 +163,11 @@ export function modelSupportsAutoMode(model: string): boolean {
163
163
  // External: firstParty-only at launch (PI probes not wired for
164
164
  // Bedrock/Vertex/Foundry yet). Checked before allowModels so the GB
165
165
  // override can't enable auto mode on unsupported providers.
166
+ // OpenAI-compatible backend: the classifier runs on the local model itself
167
+ // (getClassifierModel falls back to the main loop model), so no provider gate
168
+ if (process.env.OPENAI_API_KEY) {
169
+ return true
170
+ }
166
171
  if (process.env.USER_TYPE !== 'ant' && getAPIProvider() !== 'firstParty') {
167
172
  return false
168
173
  }
@@ -52,6 +52,15 @@ export function getContextWindowForModel(
52
52
  model: string,
53
53
  betas?: string[],
54
54
  ): number {
55
+ // OpenAI-compatible backends: the real window comes from the server (a local
56
+ // llama.cpp may run 32k-128k), so auto-compact and the context meter follow it
57
+ // instead of the Claude default
58
+ if (process.env.OPENAI_API_KEY && process.env.OPENAI_CONTEXT_WINDOW) {
59
+ const override = parseInt(process.env.OPENAI_CONTEXT_WINDOW, 10)
60
+ if (!isNaN(override) && override > 0) {
61
+ return override
62
+ }
63
+ }
55
64
  // Allow override via environment variable (ant-only)
56
65
  // This takes precedence over all other context window resolution, including 1M detection,
57
66
  // so users can cap the effective context window for local decisions (auto-compact, etc.)
@@ -64,7 +64,7 @@ const LEGACY_BRIEF_TOOL_NAME: string | null =
64
64
  require('../tools/BriefTool/prompt.js') as typeof import('../tools/BriefTool/prompt.js')
65
65
  ).LEGACY_BRIEF_TOOL_NAME
66
66
  : null
67
- const SEND_USER_FILE_TOOL_NAME: string | null = feature('KAIROS')
67
+ const SEND_USER_FILE_TOOL_NAME: string | null = feature('KAIROS') || feature('KAIROS_PUSH_NOTIFICATION')
68
68
  ? (
69
69
  require('../tools/SendUserFileTool/prompt.js') as typeof import('../tools/SendUserFileTool/prompt.js')
70
70
  ).SEND_USER_FILE_TOOL_NAME
@@ -105,6 +105,8 @@ export type ForkedAgentParams = {
105
105
  maxTurns?: number
106
106
  /** Optional callback invoked for each message as it arrives (for streaming UI) */
107
107
  onMessage?: (message: Message) => void
108
+ /** Called for every raw stream event (text deltas etc.) before they are dropped. */
109
+ onStreamEvent?: (event: unknown) => void
108
110
  /** Skip sidechain transcript recording (e.g., for ephemeral work like speculation) */
109
111
  skipTranscript?: boolean
110
112
  /** Skip writing new prompt cache entries on the last message. For
@@ -496,6 +498,7 @@ export async function runForkedAgent({
496
498
  maxOutputTokens,
497
499
  maxTurns,
498
500
  onMessage,
501
+ onStreamEvent,
499
502
  skipTranscript,
500
503
  skipCacheWrite,
501
504
  }: ForkedAgentParams): Promise<ForkedAgentResult> {
@@ -556,6 +559,7 @@ export async function runForkedAgent({
556
559
  })) {
557
560
  // Extract real usage from message_delta stream events (final usage per API call)
558
561
  if (message.type === 'stream_event') {
562
+ onStreamEvent?.(message)
559
563
  if (
560
564
  'event' in message &&
561
565
  message.event?.type === 'message_delta' &&
@@ -253,9 +253,13 @@ export function getLogoDisplayData(): {
253
253
  const cwd = serverUrl
254
254
  ? `${displayPath} in ${serverUrl.replace(/^https?:\/\//, '')}`
255
255
  : displayPath
256
- const billingType = isClaudeAISubscriber()
257
- ? getSubscriptionName()
258
- : 'API Usage Billing'
256
+ const billingType = process.env.OPENAI_API_KEY
257
+ ? process.env.OPENAI_BASE_URL && /localhost|127\.0\.0\.1/.test(process.env.OPENAI_BASE_URL)
258
+ ? 'Local model'
259
+ : 'OpenAI-compatible API'
260
+ : isClaudeAISubscriber()
261
+ ? getSubscriptionName()
262
+ : 'API Usage Billing'
259
263
  const agentName = getInitialSettings().agent
260
264
 
261
265
  return {
@@ -50,13 +50,27 @@ function logOperation(operation: QueueOperation, content?: string): void {
50
50
  // Within the same priority, commands are processed FIFO.
51
51
  // ============================================================================
52
52
 
53
- const commandQueue: QueuedCommand[] = []
54
- /** Frozen snapshot — recreated on every mutation for useSyncExternalStore. */
55
- let snapshot: readonly QueuedCommand[] = Object.freeze([])
56
- const queueChanged = createSignal()
53
+ // Process-wide state. Feature-gated modules are loaded through the CJS
54
+ // `require` shim and get their own copy of this module; without a shared
55
+ // store they would enqueue into a queue nobody drains.
56
+ type QueueGlobals = {
57
+ commandQueue: QueuedCommand[]
58
+ /** Frozen snapshot — recreated on every mutation for useSyncExternalStore. */
59
+ snapshot: readonly QueuedCommand[]
60
+ queueChanged: ReturnType<typeof createSignal>
61
+ }
62
+ const queueGlobals: QueueGlobals = ((
63
+ globalThis as unknown as { __xelaMessageQueue?: QueueGlobals }
64
+ ).__xelaMessageQueue ??= {
65
+ commandQueue: [],
66
+ snapshot: Object.freeze([]),
67
+ queueChanged: createSignal(),
68
+ })
69
+ const commandQueue: QueuedCommand[] = queueGlobals.commandQueue
70
+ const queueChanged = queueGlobals.queueChanged
57
71
 
58
72
  function notifySubscribers(): void {
59
- snapshot = Object.freeze([...commandQueue])
73
+ queueGlobals.snapshot = Object.freeze([...commandQueue])
60
74
  queueChanged.emit()
61
75
  }
62
76
 
@@ -76,7 +90,7 @@ export const subscribeToCommandQueue = queueChanged.subscribe
76
90
  * Returns a frozen array that only changes reference on mutation.
77
91
  */
78
92
  export function getCommandQueueSnapshot(): readonly QueuedCommand[] {
79
- return snapshot
93
+ return queueGlobals.snapshot
80
94
  }
81
95
 
82
96
  // ============================================================================
@@ -328,12 +342,12 @@ export function clearCommandQueue(): void {
328
342
  }
329
343
 
330
344
  /**
331
- * Clear all commands and reset snapshot.
345
+ * Clear all commands and reset queueGlobals.snapshot.
332
346
  * Used for test cleanup.
333
347
  */
334
348
  export function resetCommandQueue(): void {
335
349
  commandQueue.length = 0
336
- snapshot = Object.freeze([])
350
+ queueGlobals.snapshot = Object.freeze([])
337
351
  }
338
352
 
339
353
  // ============================================================================
@@ -492,7 +506,7 @@ export const subscribeToPendingNotifications = subscribeToCommandQueue
492
506
 
493
507
  /** @deprecated Use getCommandQueueSnapshot */
494
508
  export function getPendingNotificationsSnapshot(): readonly QueuedCommand[] {
495
- return snapshot
509
+ return queueGlobals.snapshot
496
510
  }
497
511
 
498
512
  /** @deprecated Use hasCommandsInQueue */
@@ -63,6 +63,12 @@ export function isNonCustomOpusModel(model: ModelName): boolean {
63
63
  * 4. Settings (from user's saved settings)
64
64
  */
65
65
  export function getUserSpecifiedModelSetting(): ModelSetting | undefined {
66
+ // OpenAI-compatible backend: the configured model wins over any Claude model
67
+ // left in settings, so the UI shows what actually answers
68
+ if (process.env.OPENAI_API_KEY) {
69
+ return process.env.OPENAI_MODEL || 'gpt-4o'
70
+ }
71
+
66
72
  let specifiedModel: ModelSetting | undefined
67
73
 
68
74
  const modelOverride = getMainLoopModelOverride()
@@ -74,8 +80,7 @@ export function getUserSpecifiedModelSetting(): ModelSetting | undefined {
74
80
  }
75
81
 
76
82
  // Ignore the user-specified model if it's not in the availableModels allowlist.
77
- // Skip this check on OpenAI backend — the allowlist is for Claude models only.
78
- if (specifiedModel && !process.env.OPENAI_API_KEY && !isModelAllowed(specifiedModel)) {
83
+ if (specifiedModel && !isModelAllowed(specifiedModel)) {
79
84
  return undefined
80
85
  }
81
86
 
@@ -95,12 +100,8 @@ export function getUserSpecifiedModelSetting(): ModelSetting | undefined {
95
100
  * @returns The resolved model name to use
96
101
  */
97
102
  export function getMainLoopModel(): ModelName {
98
- // When using OpenAI backend, respect user-selected model, fall back to OPENAI_MODEL
103
+ // When using OpenAI backend, return the OpenAI model name directly
99
104
  if (process.env.OPENAI_API_KEY) {
100
- const model = getUserSpecifiedModelSetting()
101
- if (model !== undefined && model !== null) {
102
- return model
103
- }
104
105
  return process.env.OPENAI_MODEL || 'gpt-4o'
105
106
  }
106
107
  const model = getUserSpecifiedModelSetting()
@@ -165,9 +166,9 @@ export function getRuntimeMainLoopModel(params: {
165
166
  }): ModelName {
166
167
  const { permissionMode, mainLoopModel, exceeds200kTokens = false } = params
167
168
 
168
- // When using OpenAI backend, use the passed-in model (already resolved by caller)
169
+ // When using OpenAI backend, always use the configured model
169
170
  if (process.env.OPENAI_API_KEY) {
170
- return mainLoopModel || process.env.OPENAI_MODEL || 'gpt-4o'
171
+ return process.env.OPENAI_MODEL || 'gpt-4o'
171
172
  }
172
173
 
173
174
  // opusplan uses Opus in plan mode without [1m] suffix.