ccgx-workflow 1.0.8 → 1.0.10

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/dist/cli.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import cac from 'cac';
3
3
  import ansis from 'ansis';
4
- import { d as diagnoseMcpConfig, i as isWindows, r as readClaudeCodeConfig, f as fixWindowsMcpConfig, w as writeClaudeCodeConfig, a as readCcgConfig, b as initI18n, c as i18n, s as showMainMenu, e as init, g as configMcp, v as version } from './shared/ccgx-workflow.Do8kaPtk.mjs';
4
+ import { d as diagnoseMcpConfig, i as isWindows, r as readClaudeCodeConfig, f as fixWindowsMcpConfig, w as writeClaudeCodeConfig, a as readCcgConfig, b as initI18n, c as i18n, s as showMainMenu, e as init, g as configMcp, v as version } from './shared/ccgx-workflow.9nxM0FK2.mjs';
5
5
  import { execSync } from 'node:child_process';
6
6
  import 'inquirer';
7
7
  import 'ora';
@@ -75,6 +75,17 @@ async function fixMcp() {
75
75
  }
76
76
  }
77
77
 
78
+ const STUCK_MIN_AGE_SECONDS = 300;
79
+ const STUCK_CPU_RATIO_THRESHOLD = 0.01;
80
+ function isBrokerProcess(p) {
81
+ return /acp-broker|app-server-broker|broker-lifecycle/.test(p.cmdLine);
82
+ }
83
+ function isStuck(p) {
84
+ if (!isBrokerProcess(p)) return false;
85
+ const wallSeconds = p.ageHours * 3600;
86
+ if (wallSeconds < STUCK_MIN_AGE_SECONDS) return false;
87
+ return p.cpuSeconds / wallSeconds < STUCK_CPU_RATIO_THRESHOLD;
88
+ }
78
89
  function categorize(cmdLine) {
79
90
  const cmd = cmdLine.toLowerCase();
80
91
  if (/quasar|vite|webpack|next\b|nuxt|astro|svelte-kit|pnpm.*run.*dev|npm.*run.*dev|yarn.*run.*dev|yarn dev|pnpm dev|npm run dev/.test(cmd)) {
@@ -87,17 +98,23 @@ function categorize(cmdLine) {
87
98
  return "other";
88
99
  }
89
100
  function listNodeProcessesWindows() {
90
- const ps = `Get-CimInstance Win32_Process -Filter "Name='node.exe'" | ForEach-Object {
101
+ const ps = `$ProgressPreference = 'SilentlyContinue'
102
+ Get-CimInstance Win32_Process -Filter "Name='node.exe'" | ForEach-Object {
91
103
  $cd = $_.CreationDate
92
104
  $h = if ($cd) { ((Get-Date) - $cd).TotalHours } else { 0 }
93
105
  $cmd = if ($_.CommandLine) { $_.CommandLine } else { '' }
94
- "$($_.ProcessId)|$h|$cmd"
106
+ $kt = if ($_.KernelModeTime) { $_.KernelModeTime } else { 0 }
107
+ $ut = if ($_.UserModeTime) { $_.UserModeTime } else { 0 }
108
+ $cpuS = ($kt + $ut) / 10000000.0
109
+ "$($_.ProcessId)|$h|$cpuS|$cmd"
95
110
  }`;
96
111
  let out;
97
112
  try {
98
- out = execSync(`powershell -NoProfile -Command "${ps.replace(/\n\s+/g, " ")}"`, {
113
+ const encoded = Buffer.from(ps, "utf16le").toString("base64");
114
+ out = execSync(`powershell -NoProfile -EncodedCommand ${encoded}`, {
99
115
  encoding: "utf-8",
100
- maxBuffer: 16 * 1024 * 1024
116
+ maxBuffer: 16 * 1024 * 1024,
117
+ stdio: ["ignore", "pipe", "ignore"]
101
118
  });
102
119
  } catch {
103
120
  return [];
@@ -108,43 +125,56 @@ function listNodeProcessesWindows() {
108
125
  if (!trimmed) continue;
109
126
  const sep1 = trimmed.indexOf("|");
110
127
  const sep2 = trimmed.indexOf("|", sep1 + 1);
111
- if (sep1 < 0 || sep2 < 0) continue;
128
+ const sep3 = trimmed.indexOf("|", sep2 + 1);
129
+ if (sep1 < 0 || sep2 < 0 || sep3 < 0) continue;
112
130
  const pid = Number.parseInt(trimmed.slice(0, sep1), 10);
113
131
  const ageHours = Number.parseFloat(trimmed.slice(sep1 + 1, sep2));
114
- const cmdLine = trimmed.slice(sep2 + 1);
132
+ const cpuSeconds = Number.parseFloat(trimmed.slice(sep2 + 1, sep3));
133
+ const cmdLine = trimmed.slice(sep3 + 1);
115
134
  if (Number.isNaN(pid)) continue;
116
135
  procs.push({
117
136
  pid,
118
137
  ageHours: Number.isFinite(ageHours) ? ageHours : 0,
138
+ cpuSeconds: Number.isFinite(cpuSeconds) ? cpuSeconds : 0,
119
139
  cmdLine,
120
140
  category: categorize(cmdLine)
121
141
  });
122
142
  }
123
143
  return procs;
124
144
  }
145
+ function parseHmsToSeconds(spec) {
146
+ const parts = spec.split(/[-:]/).map((s) => Number.parseInt(s, 10));
147
+ if (parts.length === 4) return parts[0] * 86400 + parts[1] * 3600 + parts[2] * 60 + parts[3];
148
+ if (parts.length === 3) return parts[0] * 3600 + parts[1] * 60 + parts[2];
149
+ if (parts.length === 2) return parts[0] * 60 + parts[1];
150
+ return 0;
151
+ }
125
152
  function listNodeProcessesPosix() {
126
153
  let out;
127
154
  try {
128
- out = execSync("ps -eo pid,etime,command", { encoding: "utf-8", maxBuffer: 16 * 1024 * 1024 });
155
+ out = execSync("ps -eo pid,etime,time,command", { encoding: "utf-8", maxBuffer: 16 * 1024 * 1024 });
129
156
  } catch {
130
157
  return [];
131
158
  }
132
159
  const procs = [];
133
160
  for (const line of out.split("\n").slice(1)) {
134
- const m = line.match(/^\s*(\d+)\s+(\S+)\s+(.+)$/);
161
+ const m = line.match(/^\s*(\d+)\s+(\S+)\s+(\S+)\s+(.+)$/);
135
162
  if (!m) continue;
136
- const cmdLine = m[3];
163
+ const cmdLine = m[4];
137
164
  if (!/(^|\/)node(\s|$)/.test(cmdLine)) continue;
138
165
  const pid = Number.parseInt(m[1], 10);
139
166
  const etime = m[2];
167
+ const cpuTime = m[3];
140
168
  const parts = etime.split(/[-:]/).map((s) => Number.parseInt(s, 10));
141
169
  let ageHours = 0;
142
170
  if (parts.length === 4) ageHours = parts[0] * 24 + parts[1] + parts[2] / 60;
143
171
  else if (parts.length === 3) ageHours = parts[0] + parts[1] / 60;
144
172
  else if (parts.length === 2) ageHours = parts[0] / 60;
173
+ const cpuSeconds = parseHmsToSeconds(cpuTime);
145
174
  procs.push({
146
175
  pid,
147
176
  ageHours,
177
+ cpuSeconds,
148
178
  cmdLine,
149
179
  category: categorize(cmdLine)
150
180
  });
@@ -156,13 +186,13 @@ function listNodeProcesses() {
156
186
  }
157
187
  function killProcessWindows(pid) {
158
188
  try {
159
- execSync(`powershell -NoProfile -Command "Stop-Process -Id ${pid} -Force -ErrorAction Stop"`, { stdio: "pipe" });
160
- return { ok: true, method: "Stop-Process" };
189
+ execSync(`taskkill /F /T /PID ${pid}`, { stdio: "pipe" });
190
+ return { ok: true, method: "taskkill /F /T" };
161
191
  } catch {
162
192
  }
163
193
  try {
164
- execSync(`taskkill /F /PID ${pid}`, { stdio: "pipe" });
165
- return { ok: true, method: "taskkill /F" };
194
+ execSync(`powershell -NoProfile -Command "Stop-Process -Id ${pid} -Force -ErrorAction Stop"`, { stdio: "pipe" });
195
+ return { ok: true, method: "Stop-Process" };
166
196
  } catch {
167
197
  }
168
198
  try {
@@ -173,6 +203,11 @@ function killProcessWindows(pid) {
173
203
  }
174
204
  }
175
205
  function killProcessPosix(pid) {
206
+ try {
207
+ execSync(`kill -TERM -- -${pid}`, { stdio: "pipe" });
208
+ return { ok: true, method: "SIGTERM (group)" };
209
+ } catch {
210
+ }
176
211
  try {
177
212
  execSync(`kill -TERM ${pid}`, { stdio: "pipe" });
178
213
  return { ok: true, method: "SIGTERM" };
@@ -188,8 +223,10 @@ function killProcessPosix(pid) {
188
223
  async function killOrphans(options = {}) {
189
224
  const dryRun = options.dryRun ?? true;
190
225
  const minAgeHours = options.minAgeHours ?? 1;
226
+ const stuckOnly = options.stuckOnly ?? false;
227
+ const targetDesc = stuckOnly ? "stuck broker daemons only (CPU/wall < 1%, age > 5min, ignores companions/MCP)" : `orphan node processes >${minAgeHours}h`;
191
228
  console.log(ansis.cyan.bold("\n ccgx kill-orphans"));
192
- console.log(ansis.gray(` ${dryRun ? "[DRY-RUN]" : "[KILL MODE]"} target: orphan node processes >${minAgeHours}h
229
+ console.log(ansis.gray(` ${dryRun ? "[DRY-RUN]" : "[KILL MODE]"} target: ${targetDesc}
193
230
  `));
194
231
  const all = listNodeProcesses();
195
232
  if (all.length === 0) {
@@ -206,24 +243,37 @@ async function killOrphans(options = {}) {
206
243
  const list = groups.get(cat) ?? [];
207
244
  if (list.length === 0) continue;
208
245
  const oldestH = Math.max(...list.map((p) => p.ageHours));
246
+ const stuckCount = list.filter(isStuck).length;
209
247
  const tag = cat === "dev-server" ? ansis.green("SAFE") : cat === "other" ? ansis.gray("UNKN") : ansis.yellow("ORPH");
210
- console.log(` ${tag} ${cat.padEnd(14)} ${String(list.length).padStart(3)} processes (oldest ${oldestH.toFixed(1)}h)`);
248
+ const stuckTag = stuckCount > 0 ? ansis.red(` ${stuckCount} stuck`) : "";
249
+ console.log(` ${tag} ${cat.padEnd(14)} ${String(list.length).padStart(3)} processes (oldest ${oldestH.toFixed(1)}h)${stuckTag}`);
211
250
  }
212
251
  console.log();
213
252
  const ORPHAN_CATS = ["mcp-server", "codex-cli", "gemini-cli", "phase-runner"];
214
- const targets = all.filter((p) => ORPHAN_CATS.includes(p.category) && p.ageHours >= minAgeHours);
253
+ const targets = all.filter((p) => {
254
+ if (!ORPHAN_CATS.includes(p.category)) return false;
255
+ if (stuckOnly) return isStuck(p);
256
+ return p.ageHours >= minAgeHours;
257
+ });
215
258
  if (targets.length === 0) {
216
- console.log(ansis.green(` \u2713 No orphans >${minAgeHours}h to clean.`));
259
+ const reason = stuckOnly ? "stuck" : `>${minAgeHours}h`;
260
+ console.log(ansis.green(` \u2713 No ${reason} orphans to clean.`));
217
261
  return;
218
262
  }
219
263
  console.log(ansis.bold(` Targets (${targets.length}):`));
220
264
  for (const p of targets) {
221
- const cmdShort = p.cmdLine.length > 70 ? `${p.cmdLine.slice(0, 70)}...` : p.cmdLine;
222
- console.log(` PID ${String(p.pid).padStart(6)} ${p.ageHours.toFixed(1).padStart(5)}h ${ansis.gray(p.category.padEnd(14))} ${cmdShort}`);
265
+ const cmdShort = p.cmdLine.length > 60 ? `${p.cmdLine.slice(0, 60)}...` : p.cmdLine;
266
+ const wallS = p.ageHours * 3600;
267
+ const ratio = wallS > 0 ? p.cpuSeconds / wallS * 100 : 0;
268
+ const stuckMark = isStuck(p) ? ansis.red(" STUCK") : "";
269
+ console.log(
270
+ ` PID ${String(p.pid).padStart(6)} ${p.ageHours.toFixed(1).padStart(5)}h CPU ${p.cpuSeconds.toFixed(1).padStart(6)}s (${ratio.toFixed(2).padStart(5)}%) ${ansis.gray(p.category.padEnd(14))} ${cmdShort}${stuckMark}`
271
+ );
223
272
  }
224
273
  console.log();
225
274
  if (dryRun) {
226
- console.log(ansis.gray(` Run with --kill to actually terminate (skips dev-server / other categories).`));
275
+ const flag = stuckOnly ? "--stuck --kill" : "--kill";
276
+ console.log(ansis.gray(` Run with ${flag} to actually terminate (skips dev-server / other categories).`));
227
277
  return;
228
278
  }
229
279
  const killer = isWindows() ? killProcessWindows : killProcessPosix;
@@ -324,10 +374,11 @@ async function setupCommands(cli) {
324
374
  cli.command("diagnose-mcp", i18n.t("cli:help.commandDescriptions.diagnoseMcp")).action(async () => {
325
375
  await diagnoseMcp();
326
376
  });
327
- cli.command("kill-orphans", "\u6E05\u7406 Claude Code session \u9000\u51FA\u540E\u6B8B\u7559\u7684\u5B64\u513F node \u8FDB\u7A0B\uFF08MCP server / codex / gemini\uFF09").option("--kill", "\u5B9E\u9645\u6267\u884C kill (\u9ED8\u8BA4 dry-run)").option("--min-age-hours <N>", "\u53EA\u6E05\u7406\u8D85\u8FC7 N \u5C0F\u65F6\u7684\u8FDB\u7A0B (\u9ED8\u8BA4 1)").action(async (options) => {
377
+ cli.command("kill-orphans", "\u6E05\u7406 Claude Code session \u9000\u51FA\u540E\u6B8B\u7559\u7684\u5B64\u513F node \u8FDB\u7A0B\uFF08MCP server / codex / gemini\uFF09").option("--kill", "\u5B9E\u9645\u6267\u884C kill (\u9ED8\u8BA4 dry-run)").option("--min-age-hours <N>", "\u53EA\u6E05\u7406\u8D85\u8FC7 N \u5C0F\u65F6\u7684\u8FDB\u7A0B (\u9ED8\u8BA4 1)").option("--stuck", "\u53EA\u6E05\u7406\u505C\u6EDE\u8FDB\u7A0B\uFF1ACPU \u5360\u6BD4 < 1% \u4E14 wall age > 5min\uFF08\u8986\u76D6 --min-age-hours\uFF09").action(async (options) => {
328
378
  await killOrphans({
329
379
  dryRun: !options.kill,
330
- minAgeHours: options.minAgeHours ? Number.parseFloat(options.minAgeHours) : 1
380
+ minAgeHours: options.minAgeHours ? Number.parseFloat(options.minAgeHours) : 1,
381
+ stuckOnly: options.stuck
331
382
  });
332
383
  });
333
384
  cli.command("fix-mcp", i18n.t("cli:help.commandDescriptions.fixMcp")).action(async () => {
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
- import { h as collectSkills, j as resolvePluginBashCommand } from './shared/ccgx-workflow.Do8kaPtk.mjs';
2
- export { k as changeLanguage, G as checkForUpdates, I as collectInvocableSkills, H as compareVersions, m as createDefaultConfig, n as createDefaultRouting, J as generateCommandContent, o as getCcgDir, p as getConfigPath, E as getCurrentVersion, F as getLatestVersion, t as getWorkflowById, q as getWorkflowConfigs, c as i18n, e as init, b as initI18n, y as installAceTool, z as installAceToolRs, K as installSkillCommands, x as installWorkflows, C as migrateToV1_4_0, D as needsMigration, L as parseFrontmatter, a as readCcgConfig, s as showMainMenu, B as uninstallAceTool, A as uninstallWorkflows, u as update, l as writeCcgConfig } from './shared/ccgx-workflow.Do8kaPtk.mjs';
1
+ import { h as collectSkills, j as resolvePluginBashCommand } from './shared/ccgx-workflow.9nxM0FK2.mjs';
2
+ export { k as changeLanguage, G as checkForUpdates, I as collectInvocableSkills, H as compareVersions, m as createDefaultConfig, n as createDefaultRouting, J as generateCommandContent, o as getCcgDir, p as getConfigPath, E as getCurrentVersion, F as getLatestVersion, t as getWorkflowById, q as getWorkflowConfigs, c as i18n, e as init, b as initI18n, y as installAceTool, z as installAceToolRs, K as installSkillCommands, x as installWorkflows, C as migrateToV1_4_0, D as needsMigration, L as parseFrontmatter, a as readCcgConfig, s as showMainMenu, B as uninstallAceTool, A as uninstallWorkflows, u as update, l as writeCcgConfig } from './shared/ccgx-workflow.9nxM0FK2.mjs';
3
3
  import { existsSync, readFileSync, mkdirSync, writeFileSync, statSync, readdirSync } from 'node:fs';
4
4
  import { join, dirname } from 'node:path';
5
5
  import { homedir } from 'node:os';
@@ -13,7 +13,7 @@ import { join as join$2 } from 'node:path/posix';
13
13
  import { join as join$1 } from 'node:path';
14
14
  import i18next from 'i18next';
15
15
 
16
- const version = "1.0.8";
16
+ const version = "1.0.10";
17
17
 
18
18
  function cmd(id, order, category, name, nameEn, description, descriptionEn, cmdOverride) {
19
19
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccgx-workflow",
3
- "version": "1.0.8",
3
+ "version": "1.0.10",
4
4
  "description": "Multi-model orchestration for Claude Code. Codex + Gemini parallel collaboration with fresh-context subagent protocols, OS-level process isolation, and Plan-Critic-Verify quality tiers. Successor to ccg-workflow.",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@10.17.1",
@@ -142,6 +142,18 @@ const PATCHES = [
142
142
  match: /(if \(message\.error\) \{\s*\r?\n\s*)pending\.reject\(message\.error\);(\s*\r?\n\s*\} else \{)/,
143
143
  replace: `$1// CCG P-9 patch: wrap JSON-RPC error object in Error instance.\n // Without this, callers doing \`e instanceof Error ? e.message : String(e)\`\n // get "[object Object]" — losing real error info (auth-expired, broker-dead,\n // parse-error, etc). See .ccg-migration/PLUGIN-PATCHES.md P-9.\n const _err = message.error;\n const _wrapped = Object.assign(\n new Error(typeof _err === "object" && _err !== null && _err.message\n ? String(_err.message)\n : String(_err)),\n {\n jsonrpcCode: typeof _err === "object" && _err !== null ? _err.code : undefined,\n jsonrpcData: typeof _err === "object" && _err !== null ? _err.data : undefined,\n },\n );\n pending.reject(_wrapped);$2`,
144
144
  },
145
+ {
146
+ id: "P-12",
147
+ file: "gemini-companion.mjs",
148
+ description: "CLAUDE_PLUGIN_DATA env cross-contamination — recompute from script path",
149
+ // Guard: any patched marker present
150
+ guard: /CCG P-12 patch/,
151
+ // Anchor on the last import line (createStreamHandler). Newer plugin
152
+ // versions may add imports after this; if so, the regex misses (reported
153
+ // by the script's [MISS] line) and the patch is re-evaluated upstream.
154
+ match: /(import\s+\{\s*createStreamHandler\s*\}\s+from\s+"\.\/lib\/stream-output\.mjs";?)/,
155
+ replace: `$1\n\n// CCG P-12 patch: prevent CLAUDE_PLUGIN_DATA cross-contamination from previous\n// plugin invocations (e.g., codex). Recompute from this script's physical path\n// so the broker session file lands in our own data dir, enabling broker reuse.\n// See .ccg-migration/PLUGIN-PATCHES.md P-12.\n{\n const _scriptDir = path.dirname(fileURLToPath(import.meta.url));\n const _versionDir = path.dirname(_scriptDir);\n const _pluginDir = path.dirname(_versionDir);\n const _marketplaceDir = path.dirname(_pluginDir);\n const _cacheDir = path.dirname(_marketplaceDir);\n const _pluginsDir = path.dirname(_cacheDir);\n const _pluginName = path.basename(_pluginDir);\n const _marketplaceName = path.basename(_marketplaceDir);\n process.env.CLAUDE_PLUGIN_DATA = path.join(_pluginsDir, "data", _pluginName + "-" + _marketplaceName);\n}`,
156
+ },
145
157
  ];
146
158
 
147
159
  let applied = 0;