@yeaft/webchat-agent 1.0.27 → 1.0.29

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/cli.js CHANGED
@@ -41,7 +41,7 @@ const subArgs = args.slice(1);
41
41
  const SERVICE_COMMANDS = ['install', 'uninstall', 'start', 'stop', 'restart', 'status', 'logs'];
42
42
 
43
43
  if (command === 'doctor') {
44
- handleDoctorCommand();
44
+ await handleDoctorCommand();
45
45
  } else if (command === 'llm') {
46
46
  await handleLlmCommand(subArgs);
47
47
  } else if (command === 'upgrade') {
@@ -51,7 +51,7 @@ if (command === 'doctor') {
51
51
  } else if (command === '--help' || command === '-h') {
52
52
  printHelp();
53
53
  } else if (SERVICE_COMMANDS.includes(command)) {
54
- handleServiceCommand(command, subArgs);
54
+ await handleServiceCommand(command, subArgs);
55
55
  } else {
56
56
  // Normal agent startup — parse flags and set env vars
57
57
  parseAndStart(args);
@@ -385,13 +385,13 @@ function parseLlmArgs(args) {
385
385
  async function handleServiceCommand(command, args) {
386
386
  const service = await import('./service.js');
387
387
  switch (command) {
388
- case 'install': service.install(args); break;
389
- case 'uninstall': service.uninstall(args); break;
390
- case 'start': service.start(args); break;
391
- case 'stop': service.stop(args); break;
392
- case 'restart': service.restart(args); break;
393
- case 'status': service.status(args); break;
394
- case 'logs': service.logs(args); break;
388
+ case 'install': await service.install(args); break;
389
+ case 'uninstall': await service.uninstall(args); break;
390
+ case 'start': await service.start(args); break;
391
+ case 'stop': await service.stop(args); break;
392
+ case 'restart': await service.restart(args); break;
393
+ case 'status': await service.status(args); break;
394
+ case 'logs': await service.logs(args); break;
395
395
  }
396
396
  }
397
397
 
@@ -676,10 +676,9 @@ export async function handleMessage(msg) {
676
676
  await handleYeaftFetchToolStats(msg);
677
677
  break;
678
678
 
679
- // fix-vp-multi-thread (bug 4): hydrate the Yeaft debug panel from
680
- // the persistent SQLite trace. Without this, the panel only shows
681
- // turns that happened after the panel was opened — every previous
682
- // turn is invisible.
679
+ // Hydrate the Yeaft debug panel from the persistent file-backed trace.
680
+ // Without this, the panel only shows turns that happened after it was
681
+ // opened — every previous turn is invisible.
683
682
  case 'yeaft_fetch_debug_history':
684
683
  case 'unify_fetch_debug_history':
685
684
  await handleYeaftFetchDebugHistory(msg);
package/llm-config-cli.js CHANGED
@@ -8,6 +8,8 @@ import {
8
8
  modelIdsFromProviderModels,
9
9
  } from './llm-model-discovery.js';
10
10
 
11
+ export const DEFAULT_GITHUB_COPILOT_MODEL = 'gpt-5.5';
12
+
11
13
  const VALID_PROTOCOLS = new Set(['anthropic', 'openai-responses']);
12
14
  const VALID_CREDENTIAL_PROVIDERS = new Set(['github-copilot']);
13
15
 
@@ -230,6 +232,52 @@ export async function useGitHubCopilot(config, options = {}) {
230
232
  return { config: next, provider, discovery };
231
233
  }
232
234
 
235
+ export function hasLocalLlmConfig(config = {}) {
236
+ const providers = Array.isArray(config.providers) ? config.providers.filter(Boolean) : [];
237
+ return providers.length > 0 || Boolean(config.primaryModel) || Boolean(config.fastModel);
238
+ }
239
+
240
+ export function isDefaultSeedLlmConfig(config = {}) {
241
+ const providers = Array.isArray(config.providers) ? config.providers.filter(Boolean) : [];
242
+ if (providers.length !== 1) return false;
243
+ const provider = providers[0];
244
+ return provider?.name === 'my-proxy'
245
+ && provider?.baseUrl === 'http://localhost:6628/v1'
246
+ && provider?.apiKey === 'proxy'
247
+ && typeof config.primaryModel === 'string'
248
+ && config.primaryModel.startsWith('my-proxy/');
249
+ }
250
+
251
+ export async function tryAutoConfigureGitHubCopilot(configPath = getDefaultYeaftConfigPath(), options = {}) {
252
+ let current;
253
+ try {
254
+ current = readLocalLlmConfig(configPath);
255
+ } catch (error) {
256
+ return { configured: false, reason: 'invalid-config', error, config: null };
257
+ }
258
+
259
+ const allowConfigured = Boolean(options.allowConfigured) || isDefaultSeedLlmConfig(current);
260
+ if (!allowConfigured && hasLocalLlmConfig(current)) {
261
+ return { configured: false, reason: 'already-configured', config: current };
262
+ }
263
+
264
+ try {
265
+ const result = await useGitHubCopilot(current, {
266
+ ...options,
267
+ model: options.model || DEFAULT_GITHUB_COPILOT_MODEL,
268
+ });
269
+ writeLocalLlmConfig(result.config, configPath);
270
+ return { configured: true, reason: 'configured', ...result };
271
+ } catch (error) {
272
+ return {
273
+ configured: false,
274
+ reason: error?.code === 'COPILOT_CREDENTIAL_MISSING' ? 'credential-missing' : 'unavailable',
275
+ error,
276
+ config: current,
277
+ };
278
+ }
279
+ }
280
+
233
281
 
234
282
  export async function useOpenAICompatible(config, options = {}, env = process.env) {
235
283
  const name = options.name ? String(options.name).trim() : 'openai';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "1.0.27",
3
+ "version": "1.0.29",
4
4
  "description": "Remote worker agent for Yeaft Web Code Agent — connects the native Yeaft engine, CLI providers, and workbench tools",
5
5
  "main": "index.js",
6
6
  "type": "module",
package/service/index.js CHANGED
@@ -3,6 +3,7 @@
3
3
  * Routes install/uninstall/start/stop/restart/status/logs to the correct platform module.
4
4
  */
5
5
  import { existsSync } from 'fs';
6
+ import { join } from 'path';
6
7
  import { platform } from 'os';
7
8
  import {
8
9
  getConfigDir, getLogDir, getConfigPath,
@@ -10,6 +11,7 @@ import {
10
11
  parseServiceArgs, validateConfig, getInstanceIdFromArgs, getDefaultYeaftDir
11
12
  } from './config.js';
12
13
  import { initYeaftDir } from '../yeaft/init.js';
14
+ import { DEFAULT_GITHUB_COPILOT_MODEL, tryAutoConfigureGitHubCopilot } from '../llm-config-cli.js';
13
15
  import { getSystemdServicePath, linuxInstall, linuxUninstall, linuxStart, linuxStop, linuxRestart, linuxStatus, linuxLogs } from './linux.js';
14
16
  import { getLaunchdPlistPath, macInstall, macUninstall, macStart, macStop, macRestart, macStatus, macLogs } from './macos.js';
15
17
  import { winInstall, winUninstall, winStart, winStop, winRestart, winStatus, winLogs } from './windows.js';
@@ -35,6 +37,19 @@ export {
35
37
 
36
38
  const os = platform();
37
39
 
40
+ export async function autoConfigureGitHubCopilotIfAvailable(yeaftDir, options = {}) {
41
+ const result = await tryAutoConfigureGitHubCopilot(join(yeaftDir, 'config.json'), options);
42
+ if (result.configured) {
43
+ console.log(`Configured GitHub Copilot provider automatically with ${DEFAULT_GITHUB_COPILOT_MODEL}.`);
44
+ if (result.discovery?.warning) console.log(`Warning: ${result.discovery.warning}`);
45
+ } else if (result.reason === 'already-configured') {
46
+ console.log('LLM config already exists; skipped automatic GitHub Copilot setup.');
47
+ } else if (result.reason === 'invalid-config') {
48
+ console.log('Existing LLM config is invalid; skipped automatic GitHub Copilot setup.');
49
+ }
50
+ return result;
51
+ }
52
+
38
53
  function ensureInstalled(instanceId) {
39
54
  if (os === 'linux') {
40
55
  if (!existsSync(getSystemdServicePath(instanceId))) {
@@ -50,7 +65,7 @@ function ensureInstalled(instanceId) {
50
65
  // Windows check is done inside individual functions
51
66
  }
52
67
 
53
- export function install(args) {
68
+ export async function install(args) {
54
69
  const config = parseServiceArgs(args);
55
70
  validateConfig(config);
56
71
  saveServiceConfig(config);
@@ -59,6 +74,9 @@ export function install(args) {
59
74
  // so `yeaft` CLI is ready to use immediately after install
60
75
  const effectiveYeaftDir = config.yeaftDir || getDefaultYeaftDir(config.instanceId);
61
76
  const { dir, created } = initYeaftDir(effectiveYeaftDir);
77
+ await autoConfigureGitHubCopilotIfAvailable(dir, {
78
+ allowConfigured: created.includes(join(dir, 'config.json')),
79
+ });
62
80
  if (created.length > 0) {
63
81
  console.log(`Initialized ${dir}`);
64
82
  console.log(` Edit ${dir}/config.json to configure LLM providers.`);
@@ -34,6 +34,7 @@ const MAX_INLINE_VALUE_BYTES = 1024 * 1024;
34
34
  const MAX_RAW_REQUEST_BYTES = 2 * 1024 * 1024;
35
35
  const TRACE_FLUSH_INTERVAL_MS = 5_000;
36
36
  const TRACE_FLUSH_DIRTY_LOOPS = 10;
37
+ const MAX_SEARCH_PATTERN_CHARS = 300;
37
38
 
38
39
  function isPlainObject(value) {
39
40
  return value && typeof value === 'object' && !Array.isArray(value);
@@ -74,6 +75,70 @@ function readJson(filePath) {
74
75
  }
75
76
  }
76
77
 
78
+ function compileTraceSearchRegex(search) {
79
+ const raw = typeof search === 'string' ? search.trim() : '';
80
+ if (!raw) return null;
81
+ if (raw.length > MAX_SEARCH_PATTERN_CHARS) {
82
+ throw new Error(`Debug search regex is too long; max ${MAX_SEARCH_PATTERN_CHARS} characters`);
83
+ }
84
+ let pattern = raw;
85
+ let flags = 'i';
86
+ const slashForm = raw.match(/^\/(.*)\/([a-z]*)$/);
87
+ if (slashForm) {
88
+ pattern = slashForm[1];
89
+ flags = slashForm[2] || '';
90
+ }
91
+ if (regexHasUnsafeQuantifiedGroup(pattern)) {
92
+ throw new Error('Debug search regex contains an unsafe quantified group; refine the pattern');
93
+ }
94
+ const allowed = new Set(['d', 'g', 'i', 'm', 's', 'u', 'v', 'y']);
95
+ const uniqueFlags = [];
96
+ for (const ch of flags) {
97
+ if (!allowed.has(ch)) throw new Error(`Invalid debug search regex flag: ${ch}`);
98
+ if (!uniqueFlags.includes(ch)) uniqueFlags.push(ch);
99
+ }
100
+ if (!slashForm && !uniqueFlags.includes('i')) uniqueFlags.push('i');
101
+ const stableFlags = uniqueFlags.filter(ch => ch !== 'g' && ch !== 'y').join('');
102
+ return new RegExp(pattern, stableFlags);
103
+ }
104
+
105
+ function regexHasUnsafeQuantifiedGroup(pattern) {
106
+ const groupBody = String.raw`(?:[^()\\]|\\.|\[[^\]]*\]|\([^()]*\))*`;
107
+ const nestedQuantifier = new RegExp(String.raw`\(${groupBody}[+*{]${groupBody}\)\s*[+*{]`);
108
+ const quantifiedAlternation = new RegExp(String.raw`\(${groupBody}\|${groupBody}\)\s*[+*{]`);
109
+ return nestedQuantifier.test(pattern) || quantifiedAlternation.test(pattern);
110
+ }
111
+
112
+ function buildTraceSearchDocument(trace) {
113
+ const loops = Array.isArray(trace?.loops) ? trace.loops : [];
114
+ const tools = Array.isArray(trace?.tools) ? trace.tools : [];
115
+ const toolNames = tools.map(t => t?.toolName || t?.name || '').filter(Boolean).join(' ');
116
+ const loopModels = loops.map(l => l?.model || '').filter(Boolean).join(' ');
117
+ const stopReasons = loops.map(l => l?.stopReason || '').filter(Boolean).join(' ');
118
+ return [
119
+ trace?.requestId,
120
+ trace?.traceId,
121
+ trace?.messageId,
122
+ trace?.sessionId,
123
+ trace?.vpId,
124
+ trace?.threadId,
125
+ trace?.mode,
126
+ trace?.userPrompt,
127
+ loopModels,
128
+ stopReasons,
129
+ toolNames,
130
+ ].filter(v => v != null && v !== '').map(String).join('\n').slice(0, 20_000);
131
+ }
132
+
133
+ function traceMatchesRegex(trace, regex) {
134
+ if (!regex) return true;
135
+ try {
136
+ return regex.test(buildTraceSearchDocument(trace));
137
+ } catch {
138
+ return false;
139
+ }
140
+ }
141
+
77
142
  function truncateText(value, maxBytes = MAX_TEXT_BYTES) {
78
143
  if (value == null) return value ?? null;
79
144
  const str = String(value);
@@ -696,12 +761,14 @@ export class DebugTrace {
696
761
  .flatMap(({ trace }) => traceToLegacyRows(trace));
697
762
  }
698
763
 
699
- fetchRecentDebugHistory({ limit = MAX_HISTORY_LIMIT, dreamLimit = 5, sessionId = null, threadId = null, indexOnly = false, detailTurnId = null } = {}) {
764
+ fetchRecentDebugHistory({ limit = MAX_HISTORY_LIMIT, dreamLimit = 5, sessionId = null, threadId = null, indexOnly = false, detailTurnId = null, search = '' } = {}) {
700
765
  this.#flushPendingSync();
701
766
  const lim = Math.max(1, Math.min(MAX_HISTORY_LIMIT, Number(limit) || MAX_HISTORY_LIMIT));
702
767
  const requestedDetailTurnId = typeof detailTurnId === 'string' && detailTurnId ? detailTurnId : null;
768
+ const searchRegex = requestedDetailTurnId ? null : compileTraceSearchRegex(search);
703
769
  const traces = this.#traceSummaries(sessionId)
704
770
  .filter(({ trace }) => !threadId || trace.threadId === threadId)
771
+ .filter(({ trace }) => requestedDetailTurnId || traceMatchesRegex(trace, searchRegex))
705
772
  .map(({ trace }) => trace);
706
773
  const dreamEvents = this.#readDreamEvents({ sessionId, dreamLimit });
707
774
  if (requestedDetailTurnId) {
@@ -4491,6 +4491,7 @@ export async function handleYeaftFetchToolStats(_msg = {}) {
4491
4491
  * - `detailTurnId` — fetch full loops/tools for one request
4492
4492
  * - `sessionId` — narrow by Session
4493
4493
  * - `threadId` — narrow by thread
4494
+ * - `search` — regex matched against bounded request summaries
4494
4495
  *
4495
4496
  * Sends:
4496
4497
  * { type: 'yeaft_debug_history', loops: [...], turns: [...], indexOnly, detailTurnId }
@@ -4503,6 +4504,9 @@ export async function handleYeaftFetchDebugHistory(msg = {}) {
4503
4504
  const dreamLimit = Number.isFinite(msg?.dreamLimit) ? Number(msg.dreamLimit) : 5;
4504
4505
  const sessionId = typeof msg?.sessionId === 'string' && msg.sessionId ? msg.sessionId : null;
4505
4506
  const threadId = typeof msg?.threadId === 'string' && msg.threadId ? msg.threadId : null;
4507
+ const search = typeof msg?.search === 'string' ? msg.search.trim() : '';
4508
+ const requestId = typeof msg?.requestId === 'string' && msg.requestId ? msg.requestId : null;
4509
+ const requestKind = typeof msg?.requestKind === 'string' && msg.requestKind ? msg.requestKind : null;
4506
4510
  const indexOnly = !!msg?.indexOnly;
4507
4511
  const detailTurnId = typeof msg?.detailTurnId === 'string' && msg.detailTurnId ? msg.detailTurnId : null;
4508
4512
  let loops = [];
@@ -4511,7 +4515,7 @@ export async function handleYeaftFetchDebugHistory(msg = {}) {
4511
4515
  let hasMore = false;
4512
4516
  try {
4513
4517
  if (session?.trace && typeof session.trace.fetchRecentDebugHistory === 'function') {
4514
- const out = session.trace.fetchRecentDebugHistory({ limit, dreamLimit, sessionId, threadId, indexOnly, detailTurnId });
4518
+ const out = session.trace.fetchRecentDebugHistory({ limit, dreamLimit, sessionId, threadId, indexOnly, detailTurnId, search });
4515
4519
  loops = Array.isArray(out?.loops) ? out.loops : [];
4516
4520
  turns = Array.isArray(out?.turns) ? out.turns : [];
4517
4521
  dreamEvents = Array.isArray(out?.dreamEvents) ? out.dreamEvents : [];
@@ -4523,6 +4527,14 @@ export async function handleYeaftFetchDebugHistory(msg = {}) {
4523
4527
  loops: [],
4524
4528
  turns: [],
4525
4529
  dreamEvents: [],
4530
+ requestId,
4531
+ requestKind,
4532
+ sessionId,
4533
+ threadId,
4534
+ search,
4535
+ limit,
4536
+ indexOnly,
4537
+ detailTurnId,
4526
4538
  error: err && err.message ? err.message : String(err),
4527
4539
  });
4528
4540
  return;
@@ -4532,8 +4544,11 @@ export async function handleYeaftFetchDebugHistory(msg = {}) {
4532
4544
  loops,
4533
4545
  turns,
4534
4546
  dreamEvents,
4547
+ requestId,
4548
+ requestKind,
4535
4549
  sessionId,
4536
4550
  threadId,
4551
+ search,
4537
4552
  hasMore,
4538
4553
  limit,
4539
4554
  indexOnly,