coding-tool-x 3.7.5 → 3.7.7

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 (47) hide show
  1. package/dist/web/assets/{Analytics-DnnufrQi.js → Analytics-B6FzPvdJ.js} +1 -1
  2. package/dist/web/assets/{ConfigTemplates-BTVhMSfe.js → ConfigTemplates-DXVnqR5c.js} +1 -1
  3. package/dist/web/assets/{Home-D03PEREx.js → Home-FHJB7hYI.js} +1 -1
  4. package/dist/web/assets/{PluginManager-BiMqEiyQ.js → PluginManager-D8RToURX.js} +1 -1
  5. package/dist/web/assets/ProjectList-Cdz4jnOg.js +1 -0
  6. package/dist/web/assets/ProjectList-VeMNXfgO.css +1 -0
  7. package/dist/web/assets/SessionList-C4chCPxK.css +1 -0
  8. package/dist/web/assets/SessionList-XYydFlQg.js +1 -0
  9. package/dist/web/assets/{SkillManager-DAy8k5CL.js → SkillManager-Bu9p7mMb.js} +1 -1
  10. package/dist/web/assets/{WorkspaceManager-Bs2D6R9j.js → WorkspaceManager-Bx0Gj3Mk.js} +1 -1
  11. package/dist/web/assets/index-DxkAvn4g.js +2 -0
  12. package/dist/web/assets/index-S8wrU2Nf.css +1 -0
  13. package/dist/web/index.html +2 -2
  14. package/package.json +1 -1
  15. package/src/commands/daemon.js +3 -1
  16. package/src/commands/port-config.js +0 -2
  17. package/src/commands/switch.js +1 -5
  18. package/src/commands/ui.js +4 -3
  19. package/src/commands/workspace.js +35 -3
  20. package/src/config/loader.js +76 -5
  21. package/src/index.js +11 -0
  22. package/src/server/api/config.js +0 -3
  23. package/src/server/api/omp-channels.js +50 -7
  24. package/src/server/api/settings.js +0 -2
  25. package/src/server/api/workspaces.js +39 -1
  26. package/src/server/services/dashboard-snapshot-worker.js +2 -1
  27. package/src/server/services/https-cert.js +3 -2
  28. package/src/server/services/native-oauth-adapters.js +2 -1
  29. package/src/server/services/omp-auth-provider-worker.js +29 -0
  30. package/src/server/services/omp-auth-providers.js +138 -11
  31. package/src/server/services/omp-config.js +7 -4
  32. package/src/server/services/omp-sessions.js +18 -14
  33. package/src/server/services/plugins-service.js +2 -1
  34. package/src/server/services/project-snapshots.js +8 -1
  35. package/src/server/services/session-snapshots.js +10 -1
  36. package/src/server/services/sessions.js +35 -19
  37. package/src/server/services/snapshot-cache.js +47 -3
  38. package/src/server/services/web-build.js +2 -1
  39. package/src/server/services/workspace-service.js +204 -98
  40. package/src/utils/cli-flags.js +7 -0
  41. package/src/utils/session.js +10 -4
  42. package/dist/web/assets/ProjectList-C6BA3V49.js +0 -1
  43. package/dist/web/assets/ProjectList-DQ5ZCJO2.css +0 -1
  44. package/dist/web/assets/SessionList--9Bl6EhV.css +0 -1
  45. package/dist/web/assets/SessionList-DChG04jz.js +0 -1
  46. package/dist/web/assets/index-DnDBHGCC.js +0 -2
  47. package/dist/web/assets/index-U3TOywtB.css +0 -1
@@ -61,7 +61,10 @@ function runOmpCommand(command, args = [], env = process.env, options = {}, stdi
61
61
  encoding: 'utf8',
62
62
  env: buildCommandEnv(env),
63
63
  stdio,
64
- timeout: options.timeout || 3000
64
+ timeout: options.timeout || 3000,
65
+ // On Windows, an npm/PowerShell command shim can otherwise create a visible
66
+ // console every time we probe OMP or resolve its configuration directory.
67
+ windowsHide: true
65
68
  });
66
69
  }
67
70
 
@@ -76,8 +79,8 @@ function readOmpAgentDirFromCommand(command, env = process.env, options = {}) {
76
79
  }
77
80
 
78
81
  function getOmpAgentDir(env = process.env, options = {}) {
79
- const runtime = options.runtime || resolveOmpRuntime(env, options);
80
- if (runtime.runtime === 'omp' && runtime.installed) {
82
+ const runtime = options.runtime || (options.resolveRuntime === false ? null : resolveOmpRuntime(env, options));
83
+ if (runtime?.runtime === 'omp' && runtime.installed) {
81
84
  const commandAgentDir = readOmpAgentDirFromCommand(runtime.command, env, options);
82
85
  if (commandAgentDir) {
83
86
  return commandAgentDir;
@@ -87,7 +90,7 @@ function getOmpAgentDir(env = process.env, options = {}) {
87
90
  }
88
91
 
89
92
  function getOmpPaths(env = process.env, options = {}) {
90
- const runtime = options.runtime || resolveOmpRuntime(env, options);
93
+ const runtime = options.runtime || (options.resolveRuntime === false ? null : resolveOmpRuntime(env, options));
91
94
  const agentDir = getOmpAgentDir(env, { ...options, runtime });
92
95
  return {
93
96
  agentDir,
@@ -1,8 +1,7 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
- const { execFileSync } = require('child_process');
4
3
  const { PATHS, HOME_DIR } = require('../../config/paths');
5
- const { getOmpCommand, getOmpPaths, isOmpInstalled, resolveOmpRuntime } = require('./omp-config');
4
+ const { getOmpCommand, getOmpPaths, resolveOmpRuntime } = require('./omp-config');
6
5
 
7
6
  const PROJECT_ORDER_FILE = PATHS.ompProjectOrder;
8
7
  const SESSION_ORDER_FILE = PATHS.ompSessionOrder;
@@ -213,7 +212,20 @@ function parseSessionFile(filePath) {
213
212
  };
214
213
  }
215
214
 
216
- function scanSessionFiles(rootDir = getOmpPaths().sessions) {
215
+ function getOmpSessionPaths() {
216
+ // Reading historical sessions normally only needs OMP's native directory
217
+ // convention. Avoid starting the CLI here: project/count snapshot workers
218
+ // invoke this path independently, which made Windows repeatedly open
219
+ // PowerShell shims. Keep the CLI lookup as a fallback for non-standard OMP
220
+ // locations that cannot be derived from the environment.
221
+ const nativePaths = getOmpPaths(process.env, { resolveRuntime: false });
222
+ if (fs.existsSync(nativePaths.agentDir) || fs.existsSync(nativePaths.sessions)) {
223
+ return nativePaths;
224
+ }
225
+ return getOmpPaths();
226
+ }
227
+
228
+ function scanSessionFiles(rootDir = getOmpSessionPaths().sessions) {
217
229
  if (!fs.existsSync(rootDir)) {
218
230
  return [];
219
231
  }
@@ -462,20 +474,12 @@ function isOmpCliInstalled() {
462
474
  }
463
475
 
464
476
  let installed = false;
465
- if (isOmpInstalled()) {
477
+ const nativePaths = getOmpSessionPaths();
478
+ if (fs.existsSync(nativePaths.agentDir)) {
466
479
  installed = true;
467
480
  } else {
468
481
  const runtime = resolveOmpRuntime();
469
- if (runtime.installed) {
470
- installed = true;
471
- } else {
472
- try {
473
- execFileSync(getOmpCommand(), ['--version'], { stdio: 'ignore', timeout: 3000 });
474
- installed = true;
475
- } catch {
476
- installed = false;
477
- }
478
- }
482
+ installed = Boolean(runtime && runtime.installed);
479
483
  }
480
484
 
481
485
  ompInstallCache = {
@@ -833,7 +833,8 @@ class PluginsService {
833
833
  cwd: process.cwd(),
834
834
  encoding: 'utf8',
835
835
  stdio: 'pipe',
836
- timeout: 120000
836
+ timeout: 120000,
837
+ windowsHide: true
837
838
  });
838
839
  }
839
840
 
@@ -6,6 +6,8 @@ const {
6
6
  } = require('./snapshot-cache');
7
7
 
8
8
  const PROJECT_SNAPSHOT_TTL_MS = 60 * 1000;
9
+ const PROJECT_SNAPSHOT_WAIT_ON_MISS_MS = process.env.NODE_ENV === 'test' ? 0 : 1500;
10
+ const PROJECT_SNAPSHOT_WAIT_ON_FORCE_MS = process.env.NODE_ENV === 'test' ? 0 : 1500;
9
11
 
10
12
  function projectListKey(source) {
11
13
  return `projects:list:${source}`;
@@ -33,7 +35,10 @@ async function getProjectListSnapshot(source, {
33
35
  fallbackValue,
34
36
  refresh,
35
37
  force,
36
- backgroundOnMiss: !force
38
+ backgroundOnMiss: !force,
39
+ staleWhileForce: true,
40
+ waitOnMissMs: PROJECT_SNAPSHOT_WAIT_ON_MISS_MS,
41
+ waitOnForceMs: PROJECT_SNAPSHOT_WAIT_ON_FORCE_MS
37
42
  });
38
43
  }
39
44
 
@@ -44,6 +49,8 @@ function invalidateProjectSnapshots(source) {
44
49
 
45
50
  module.exports = {
46
51
  PROJECT_SNAPSHOT_TTL_MS,
52
+ PROJECT_SNAPSHOT_WAIT_ON_MISS_MS,
53
+ PROJECT_SNAPSHOT_WAIT_ON_FORCE_MS,
47
54
  projectListKey,
48
55
  projectCountKey,
49
56
  emptyProjectList,
@@ -9,7 +9,9 @@ const {
9
9
 
10
10
  const SESSION_SNAPSHOT_TTL_MS = 60 * 1000;
11
11
  const SESSION_SNAPSHOT_DEFER_MS = process.env.NODE_ENV === 'test' ? 0 : 750;
12
- const SESSION_SNAPSHOT_WORKER_TIMEOUT_MS = 120 * 1000;
12
+ const SESSION_SNAPSHOT_WORKER_TIMEOUT_MS = 180 * 1000;
13
+ const SESSION_SNAPSHOT_WAIT_ON_MISS_MS = process.env.NODE_ENV === 'test' ? 0 : 2500;
14
+ const SESSION_SNAPSHOT_WAIT_ON_FORCE_MS = process.env.NODE_ENV === 'test' ? 0 : 2500;
13
15
 
14
16
  function sessionListKey(source, projectName) {
15
17
  return `sessions:list:${source}:${projectName}`;
@@ -44,6 +46,7 @@ function runSessionSnapshotWorker(source, projectName, config = {}, options = {}
44
46
  const workerPath = path.join(__dirname, 'session-snapshot-worker.js');
45
47
  const child = fork(workerPath, [], {
46
48
  stdio: ['ignore', 'ignore', 'pipe', 'ipc'],
49
+ windowsHide: true,
47
50
  env: {
48
51
  ...process.env,
49
52
  CC_TOOL_SESSION_SNAPSHOT_WORKER: '1'
@@ -113,6 +116,9 @@ async function getSessionListSnapshot(source, projectName, {
113
116
  refresh,
114
117
  force: process.env.NODE_ENV === 'test' ? true : force,
115
118
  backgroundOnMiss: process.env.NODE_ENV === 'test' ? false : !force,
119
+ staleWhileForce: true,
120
+ waitOnMissMs: SESSION_SNAPSHOT_WAIT_ON_MISS_MS,
121
+ waitOnForceMs: SESSION_SNAPSHOT_WAIT_ON_FORCE_MS,
116
122
  deferMs: SESSION_SNAPSHOT_DEFER_MS
117
123
  });
118
124
  }
@@ -128,6 +134,9 @@ function invalidateSessionSnapshots(source, projectName = null) {
128
134
 
129
135
  module.exports = {
130
136
  SESSION_SNAPSHOT_TTL_MS,
137
+ SESSION_SNAPSHOT_WAIT_ON_MISS_MS,
138
+ SESSION_SNAPSHOT_WAIT_ON_FORCE_MS,
139
+ SESSION_SNAPSHOT_WORKER_TIMEOUT_MS,
131
140
  defaultProjectInfo,
132
141
  emptySessionList,
133
142
  getSessionListSnapshot,
@@ -17,6 +17,17 @@ const CLAUDE_PROJECTS_DIR = NATIVE_PATHS.claude.projects;
17
17
  const CODEX_PROJECTS_DIR = path.join(path.dirname(NATIVE_PATHS.codex.config), 'projects');
18
18
  const GEMINI_PROJECTS_DIR = path.join(path.dirname(NATIVE_PATHS.gemini.env), 'projects');
19
19
 
20
+ function resolveProjectsDir(config = {}) {
21
+ return config.projectsDir || CLAUDE_PROJECTS_DIR;
22
+ }
23
+
24
+ function withResolvedProjectsDir(config = {}) {
25
+ return {
26
+ ...config,
27
+ projectsDir: resolveProjectsDir(config)
28
+ };
29
+ }
30
+
20
31
  // Base directory for cc-tool data
21
32
  function getCcToolDir() {
22
33
  return PATHS.base;
@@ -182,7 +193,7 @@ function sliceClaudeSessionContentByUserMessage(content, afterUserMessageNumber)
182
193
 
183
194
  // Get all projects with stats (async version)
184
195
  async function getProjects(config) {
185
- const projectsDir = config.projectsDir;
196
+ const projectsDir = resolveProjectsDir(config);
186
197
 
187
198
  if (!fs.existsSync(projectsDir)) {
188
199
  return [];
@@ -383,16 +394,17 @@ function extractCwdFromSessionHeader(sessionFile) {
383
394
 
384
395
  // Get projects with detailed stats (with caching) - async version
385
396
  async function getProjectsWithStats(config, options = {}) {
397
+ const resolvedConfig = withResolvedProjectsDir(config);
386
398
  if (!options.force) {
387
399
  // Check enhanced cache first
388
- const cacheKey = `${CacheKeys.PROJECTS}${config.projectsDir}`;
400
+ const cacheKey = `${CacheKeys.PROJECTS}${resolvedConfig.projectsDir}`;
389
401
  const enhancedCached = globalCache.get(cacheKey);
390
402
  if (enhancedCached) {
391
403
  return enhancedCached;
392
404
  }
393
405
 
394
406
  // Check old cache
395
- const cached = getCachedProjects(config);
407
+ const cached = getCachedProjects(resolvedConfig);
396
408
  if (cached) {
397
409
  globalCache.set(cacheKey, cached, 300000); // 5分钟
398
410
  return cached;
@@ -400,22 +412,22 @@ async function getProjectsWithStats(config, options = {}) {
400
412
  }
401
413
 
402
414
  try {
403
- const data = await buildProjectsWithStats(config);
415
+ const data = await buildProjectsWithStats(resolvedConfig);
404
416
  if (!Array.isArray(data)) {
405
- console.warn(`[getProjectsWithStats] Unexpected non-array result for ${config.projectsDir}, returning empty array.`);
417
+ console.warn(`[getProjectsWithStats] Unexpected non-array result for ${resolvedConfig.projectsDir}, returning empty array.`);
406
418
  return [];
407
419
  }
408
- setCachedProjects(config, data);
409
- globalCache.set(`${CacheKeys.PROJECTS}${config.projectsDir}`, data, 300000);
420
+ setCachedProjects(resolvedConfig, data);
421
+ globalCache.set(`${CacheKeys.PROJECTS}${resolvedConfig.projectsDir}`, data, 300000);
410
422
  return data;
411
423
  } catch (err) {
412
- console.error(`[getProjectsWithStats] Failed to build projects for ${config.projectsDir}:`, err);
424
+ console.error(`[getProjectsWithStats] Failed to build projects for ${resolvedConfig.projectsDir}:`, err);
413
425
  return [];
414
426
  }
415
427
  }
416
428
 
417
429
  async function buildProjectsWithStats(config) {
418
- const projectsDir = config.projectsDir;
430
+ const projectsDir = resolveProjectsDir(config);
419
431
 
420
432
  if (!fs.existsSync(projectsDir)) {
421
433
  return [];
@@ -482,7 +494,7 @@ async function buildProjectsWithStats(config) {
482
494
 
483
495
  // 获取 Claude 项目/会话数量(轻量统计)
484
496
  function getProjectAndSessionCounts(config) {
485
- const projectsDir = config.projectsDir;
497
+ const projectsDir = resolveProjectsDir(config);
486
498
  if (!fs.existsSync(projectsDir)) {
487
499
  return { projectCount: 0, sessionCount: 0 };
488
500
  }
@@ -672,7 +684,8 @@ async function getSessionsForProject(config, projectName, options = {}) {
672
684
 
673
685
  // Delete a session
674
686
  function deleteSession(config, projectName, sessionId) {
675
- const projectDir = path.join(config.projectsDir, projectName);
687
+ const resolvedConfig = withResolvedProjectsDir(config);
688
+ const projectDir = path.join(resolvedConfig.projectsDir, projectName);
676
689
  const sessionFile = path.join(projectDir, sessionId + '.jsonl');
677
690
 
678
691
  if (!fs.existsSync(sessionFile)) {
@@ -680,14 +693,15 @@ function deleteSession(config, projectName, sessionId) {
680
693
  }
681
694
 
682
695
  fs.unlinkSync(sessionFile);
683
- invalidateProjectsCache(config);
696
+ invalidateProjectsCache(resolvedConfig);
684
697
  invalidateSessionResultCache(projectName);
685
698
  return { success: true };
686
699
  }
687
700
 
688
701
  // Fork a session
689
702
  function forkSession(config, projectName, sessionId, options = {}) {
690
- const projectDir = path.join(config.projectsDir, projectName);
703
+ const resolvedConfig = withResolvedProjectsDir(config);
704
+ const projectDir = path.join(resolvedConfig.projectsDir, projectName);
691
705
  const sessionFile = path.join(projectDir, sessionId + '.jsonl');
692
706
 
693
707
  if (!fs.existsSync(sessionFile)) {
@@ -716,7 +730,7 @@ function forkSession(config, projectName, sessionId, options = {}) {
716
730
  const { setAlias } = require('./alias');
717
731
  setAlias(newSessionId, options.alias);
718
732
  }
719
- invalidateProjectsCache(config);
733
+ invalidateProjectsCache(resolvedConfig);
720
734
  invalidateSessionResultCache(projectName);
721
735
 
722
736
  return {
@@ -768,7 +782,8 @@ function saveSessionOrder(projectName, order) {
768
782
 
769
783
  // Delete a project (remove the entire project directory)
770
784
  function deleteProject(config, projectName) {
771
- const projectDir = path.join(config.projectsDir, projectName);
785
+ const resolvedConfig = withResolvedProjectsDir(config);
786
+ const projectDir = path.join(resolvedConfig.projectsDir, projectName);
772
787
 
773
788
  if (!fs.existsSync(projectDir)) {
774
789
  throw new Error('Project not found');
@@ -778,19 +793,20 @@ function deleteProject(config, projectName) {
778
793
  fs.rmSync(projectDir, { recursive: true, force: true });
779
794
 
780
795
  // Remove from order file if exists
781
- const order = getProjectOrder(config);
796
+ const order = getProjectOrder(resolvedConfig);
782
797
  const newOrder = order.filter(name => name !== projectName);
783
798
  if (newOrder.length !== order.length) {
784
- saveProjectOrder(config, newOrder);
799
+ saveProjectOrder(resolvedConfig, newOrder);
785
800
  }
786
801
 
787
- invalidateProjectsCache(config);
802
+ invalidateProjectsCache(resolvedConfig);
788
803
  return { success: true };
789
804
  }
790
805
 
791
806
  // Search sessions for keyword
792
807
  function searchSessions(config, projectName, keyword, contextLength = 15) {
793
- const projectDir = path.join(config.projectsDir, projectName);
808
+ const resolvedConfig = withResolvedProjectsDir(config);
809
+ const projectDir = path.join(resolvedConfig.projectsDir, projectName);
794
810
 
795
811
  if (!fs.existsSync(projectDir)) {
796
812
  return [];
@@ -9,6 +9,7 @@ const DEFAULT_TTL_MS = 60 * 1000;
9
9
  const SNAPSHOT_VERSION = 1;
10
10
  const DEFAULT_BACKGROUND_DELAY_MS = process.env.NODE_ENV === 'test' ? 0 : 750;
11
11
  const DEFAULT_BACKGROUND_GAP_MS = process.env.NODE_ENV === 'test' ? 0 : 100;
12
+ const WAIT_TIMEOUT = Symbol('snapshot-wait-timeout');
12
13
 
13
14
  const snapshots = new Map();
14
15
  const inflight = new Map();
@@ -196,6 +197,28 @@ function isExpired(entry, ttlMs, nowMs) {
196
197
  return nowMs - Number(entry.updatedAtMs || 0) >= ttlMs;
197
198
  }
198
199
 
200
+ async function waitForRefresh(promise, waitMs) {
201
+ const timeoutMs = Number(waitMs) || 0;
202
+ if (timeoutMs <= 0) return { skipped: true };
203
+
204
+ let timer = null;
205
+ const result = await Promise.race([
206
+ promise
207
+ .then(entry => ({ entry }))
208
+ .catch(error => ({ error })),
209
+ new Promise(resolve => {
210
+ timer = setTimeout(() => resolve(WAIT_TIMEOUT), timeoutMs);
211
+ })
212
+ ]);
213
+ if (timer) {
214
+ clearTimeout(timer);
215
+ }
216
+ if (result === WAIT_TIMEOUT) {
217
+ return { timedOut: true };
218
+ }
219
+ return result;
220
+ }
221
+
199
222
  function makeMeta(entry, {
200
223
  key,
201
224
  stale,
@@ -220,7 +243,9 @@ async function getSnapshot(key, {
220
243
  force = false,
221
244
  staleWhileForce = false,
222
245
  nowMs = Date.now(),
223
- deferMs = DEFAULT_BACKGROUND_DELAY_MS
246
+ deferMs = DEFAULT_BACKGROUND_DELAY_MS,
247
+ waitOnMissMs = 0,
248
+ waitOnForceMs = 0
224
249
  } = {}) {
225
250
  if (typeof refresh !== 'function') {
226
251
  const entry = getEntry(key);
@@ -241,7 +266,26 @@ async function getSnapshot(key, {
241
266
  let refreshError = null;
242
267
 
243
268
  if (expired) {
244
- if (!entry && backgroundOnMiss === false) {
269
+ const coldWaitMs = !entry
270
+ ? (force ? (waitOnForceMs || waitOnMissMs) : waitOnMissMs)
271
+ : 0;
272
+
273
+ if (!entry && Number(coldWaitMs) > 0) {
274
+ refreshPromise = refreshSnapshot(key, refresh, { bypassInflight: force });
275
+ const waitResult = await waitForRefresh(refreshPromise, coldWaitMs);
276
+ if (waitResult.entry) {
277
+ entry = waitResult.entry;
278
+ expired = false;
279
+ refreshPromise = null;
280
+ } else if (waitResult.error) {
281
+ refreshError = waitResult.error?.message || String(waitResult.error);
282
+ entry = getEntry(key);
283
+ if (entry) {
284
+ expired = isExpired(entry, ttlMs, nowMs);
285
+ }
286
+ refreshPromise = null;
287
+ }
288
+ } else if (!entry && backgroundOnMiss === false) {
245
289
  try {
246
290
  entry = await refreshSnapshot(key, refresh, { bypassInflight: force });
247
291
  expired = false;
@@ -251,7 +295,7 @@ async function getSnapshot(key, {
251
295
  if (!entry) throw error;
252
296
  }
253
297
  } else {
254
- refreshPromise = refreshSnapshot(key, refresh, { defer: true, deferMs, bypassInflight: force });
298
+ refreshPromise = refreshSnapshot(key, refresh, { defer: !force, deferMs, bypassInflight: force });
255
299
  }
256
300
  }
257
301
 
@@ -101,7 +101,8 @@ function runWebBuild(options = {}) {
101
101
  const child = spawn(npmCommand, ['run', 'build'], {
102
102
  cwd: sourceRoot,
103
103
  env,
104
- stdio
104
+ stdio,
105
+ windowsHide: true
105
106
  });
106
107
 
107
108
  child.once('error', (error) => {