@massa-ai/opencode-plugin 1.60.0 → 1.61.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.
@@ -80,9 +80,9 @@ next_step: <string>
80
80
  - References (paths relative to the `massa-ai` skill directory): `references/agent-orchestration.md`, `references/audit-report-io.md` (Judge With Debate Report Contracts).
81
81
 
82
82
  ## Model Hint
83
- See `references/agent-orchestration.md` (Model Diversity Fallback): `metadata.model_tier`
84
- (`deep`) is the per-slot fallback; `workflows/judge-with-debate.md` owns the live slot
85
- assignments.
83
+ See `references/agent-orchestration.md` (Model Diversity Fallback): `judge` carries no
84
+ per-agent override in `skills/model-profiles.json`, so the active profile's host default is
85
+ the per-slot fallback; `workflows/judge-with-debate.md` owns the live slot assignments.
86
86
 
87
87
  ## Validation Sensors
88
88
  - Every criterion score carries an exact quotation from the artifact.
@@ -71,8 +71,9 @@ overall: weighted-mean
71
71
  - References (paths relative to the `massa-ai` skill directory): `references/agent-orchestration.md`, `references/audit-report-io.md` (Judge With Debate Report Contracts).
72
72
 
73
73
  ## Model Hint
74
- See `references/agent-orchestration.md` (Model Diversity Fallback): `metadata.model_tier`
75
- (`deep`) is the fallback; `workflows/judge-with-debate.md` owns the live model assignment.
74
+ See `references/agent-orchestration.md` (Model Diversity Fallback): `meta-judge` carries no
75
+ per-agent override in `skills/model-profiles.json`, so the active profile's host default is
76
+ the fallback; `workflows/judge-with-debate.md` owns the live model assignment.
76
77
 
77
78
  ## Validation Sensors
78
79
  - Output parses as YAML; weights sum to 1.0 (±0.001); every criterion carries id, name, weight, scale (min 1, max 5), rubric anchors for 1/3/5, and a non-empty checklist.
@@ -80,9 +80,9 @@ next_step: <string>
80
80
  - References (paths relative to the `massa-ai` skill directory): `references/agent-orchestration.md`, `references/audit-report-io.md` (Judge With Debate Report Contracts).
81
81
 
82
82
  ## Model Hint
83
- See `references/agent-orchestration.md` (Model Diversity Fallback): `metadata.model_tier`
84
- (`deep`) is the per-slot fallback; `workflows/judge-with-debate.md` owns the live slot
85
- assignments.
83
+ See `references/agent-orchestration.md` (Model Diversity Fallback): `judge` carries no
84
+ per-agent override in `skills/model-profiles.json`, so the active profile's host default is
85
+ the per-slot fallback; `workflows/judge-with-debate.md` owns the live slot assignments.
86
86
 
87
87
  ## Validation Sensors
88
88
  - Every criterion score carries an exact quotation from the artifact.
@@ -71,8 +71,9 @@ overall: weighted-mean
71
71
  - References (paths relative to the `massa-ai` skill directory): `references/agent-orchestration.md`, `references/audit-report-io.md` (Judge With Debate Report Contracts).
72
72
 
73
73
  ## Model Hint
74
- See `references/agent-orchestration.md` (Model Diversity Fallback): `metadata.model_tier`
75
- (`deep`) is the fallback; `workflows/judge-with-debate.md` owns the live model assignment.
74
+ See `references/agent-orchestration.md` (Model Diversity Fallback): `meta-judge` carries no
75
+ per-agent override in `skills/model-profiles.json`, so the active profile's host default is
76
+ the fallback; `workflows/judge-with-debate.md` owns the live model assignment.
76
77
 
77
78
  ## Validation Sensors
78
79
  - Output parses as YAML; weights sum to 1.0 (±0.001); every criterion carries id, name, weight, scale (min 1, max 5), rubric anchors for 1/3/5, and a non-empty checklist.
@@ -80,9 +80,9 @@ next_step: <string>
80
80
  - References (paths relative to the `massa-ai` skill directory): `references/agent-orchestration.md`, `references/audit-report-io.md` (Judge With Debate Report Contracts).
81
81
 
82
82
  ## Model Hint
83
- See `references/agent-orchestration.md` (Model Diversity Fallback): `metadata.model_tier`
84
- (`deep`) is the per-slot fallback; `workflows/judge-with-debate.md` owns the live slot
85
- assignments.
83
+ See `references/agent-orchestration.md` (Model Diversity Fallback): `judge` carries no
84
+ per-agent override in `skills/model-profiles.json`, so the active profile's host default is
85
+ the per-slot fallback; `workflows/judge-with-debate.md` owns the live slot assignments.
86
86
 
87
87
  ## Validation Sensors
88
88
  - Every criterion score carries an exact quotation from the artifact.
@@ -71,8 +71,9 @@ overall: weighted-mean
71
71
  - References (paths relative to the `massa-ai` skill directory): `references/agent-orchestration.md`, `references/audit-report-io.md` (Judge With Debate Report Contracts).
72
72
 
73
73
  ## Model Hint
74
- See `references/agent-orchestration.md` (Model Diversity Fallback): `metadata.model_tier`
75
- (`deep`) is the fallback; `workflows/judge-with-debate.md` owns the live model assignment.
74
+ See `references/agent-orchestration.md` (Model Diversity Fallback): `meta-judge` carries no
75
+ per-agent override in `skills/model-profiles.json`, so the active profile's host default is
76
+ the fallback; `workflows/judge-with-debate.md` owns the live model assignment.
76
77
 
77
78
  ## Validation Sensors
78
79
  - Output parses as YAML; weights sum to 1.0 (±0.001); every criterion carries id, name, weight, scale (min 1, max 5), rubric anchors for 1/3/5, and a non-empty checklist.
@@ -80,9 +80,9 @@ next_step: <string>
80
80
  - References (paths relative to the `massa-ai` skill directory): `references/agent-orchestration.md`, `references/audit-report-io.md` (Judge With Debate Report Contracts).
81
81
 
82
82
  ## Model Hint
83
- See `references/agent-orchestration.md` (Model Diversity Fallback): `metadata.model_tier`
84
- (`deep`) is the per-slot fallback; `workflows/judge-with-debate.md` owns the live slot
85
- assignments.
83
+ See `references/agent-orchestration.md` (Model Diversity Fallback): `judge` carries no
84
+ per-agent override in `skills/model-profiles.json`, so the active profile's host default is
85
+ the per-slot fallback; `workflows/judge-with-debate.md` owns the live slot assignments.
86
86
 
87
87
  ## Validation Sensors
88
88
  - Every criterion score carries an exact quotation from the artifact.
@@ -71,8 +71,9 @@ overall: weighted-mean
71
71
  - References (paths relative to the `massa-ai` skill directory): `references/agent-orchestration.md`, `references/audit-report-io.md` (Judge With Debate Report Contracts).
72
72
 
73
73
  ## Model Hint
74
- See `references/agent-orchestration.md` (Model Diversity Fallback): `metadata.model_tier`
75
- (`deep`) is the fallback; `workflows/judge-with-debate.md` owns the live model assignment.
74
+ See `references/agent-orchestration.md` (Model Diversity Fallback): `meta-judge` carries no
75
+ per-agent override in `skills/model-profiles.json`, so the active profile's host default is
76
+ the fallback; `workflows/judge-with-debate.md` owns the live model assignment.
76
77
 
77
78
  ## Validation Sensors
78
79
  - Output parses as YAML; weights sum to 1.0 (±0.001); every criterion carries id, name, weight, scale (min 1, max 5), rubric anchors for 1/3/5, and a non-empty checklist.
@@ -80,9 +80,9 @@ next_step: <string>
80
80
  - References (paths relative to the `massa-ai` skill directory): `references/agent-orchestration.md`, `references/audit-report-io.md` (Judge With Debate Report Contracts).
81
81
 
82
82
  ## Model Hint
83
- See `references/agent-orchestration.md` (Model Diversity Fallback): `metadata.model_tier`
84
- (`deep`) is the per-slot fallback; `workflows/judge-with-debate.md` owns the live slot
85
- assignments.
83
+ See `references/agent-orchestration.md` (Model Diversity Fallback): `judge` carries no
84
+ per-agent override in `skills/model-profiles.json`, so the active profile's host default is
85
+ the per-slot fallback; `workflows/judge-with-debate.md` owns the live slot assignments.
86
86
 
87
87
  ## Validation Sensors
88
88
  - Every criterion score carries an exact quotation from the artifact.
@@ -71,8 +71,9 @@ overall: weighted-mean
71
71
  - References (paths relative to the `massa-ai` skill directory): `references/agent-orchestration.md`, `references/audit-report-io.md` (Judge With Debate Report Contracts).
72
72
 
73
73
  ## Model Hint
74
- See `references/agent-orchestration.md` (Model Diversity Fallback): `metadata.model_tier`
75
- (`deep`) is the fallback; `workflows/judge-with-debate.md` owns the live model assignment.
74
+ See `references/agent-orchestration.md` (Model Diversity Fallback): `meta-judge` carries no
75
+ per-agent override in `skills/model-profiles.json`, so the active profile's host default is
76
+ the fallback; `workflows/judge-with-debate.md` owns the live model assignment.
76
77
 
77
78
  ## Validation Sensors
78
79
  - Output parses as YAML; weights sum to 1.0 (±0.001); every criterion carries id, name, weight, scale (min 1, max 5), rubric anchors for 1/3/5, and a non-empty checklist.
@@ -80,9 +80,9 @@ next_step: <string>
80
80
  - References (paths relative to the `massa-ai` skill directory): `references/agent-orchestration.md`, `references/audit-report-io.md` (Judge With Debate Report Contracts).
81
81
 
82
82
  ## Model Hint
83
- See `references/agent-orchestration.md` (Model Diversity Fallback): `metadata.model_tier`
84
- (`deep`) is the per-slot fallback; `workflows/judge-with-debate.md` owns the live slot
85
- assignments.
83
+ See `references/agent-orchestration.md` (Model Diversity Fallback): `judge` carries no
84
+ per-agent override in `skills/model-profiles.json`, so the active profile's host default is
85
+ the per-slot fallback; `workflows/judge-with-debate.md` owns the live slot assignments.
86
86
 
87
87
  ## Validation Sensors
88
88
  - Every criterion score carries an exact quotation from the artifact.
@@ -71,8 +71,9 @@ overall: weighted-mean
71
71
  - References (paths relative to the `massa-ai` skill directory): `references/agent-orchestration.md`, `references/audit-report-io.md` (Judge With Debate Report Contracts).
72
72
 
73
73
  ## Model Hint
74
- See `references/agent-orchestration.md` (Model Diversity Fallback): `metadata.model_tier`
75
- (`deep`) is the fallback; `workflows/judge-with-debate.md` owns the live model assignment.
74
+ See `references/agent-orchestration.md` (Model Diversity Fallback): `meta-judge` carries no
75
+ per-agent override in `skills/model-profiles.json`, so the active profile's host default is
76
+ the fallback; `workflows/judge-with-debate.md` owns the live model assignment.
76
77
 
77
78
  ## Validation Sensors
78
79
  - Output parses as YAML; weights sum to 1.0 (±0.001); every criterion carries id, name, weight, scale (min 1, max 5), rubric anchors for 1/3/5, and a non-empty checklist.
@@ -80,9 +80,9 @@ next_step: <string>
80
80
  - References (paths relative to the `massa-ai` skill directory): `references/agent-orchestration.md`, `references/audit-report-io.md` (Judge With Debate Report Contracts).
81
81
 
82
82
  ## Model Hint
83
- See `references/agent-orchestration.md` (Model Diversity Fallback): `metadata.model_tier`
84
- (`deep`) is the per-slot fallback; `workflows/judge-with-debate.md` owns the live slot
85
- assignments.
83
+ See `references/agent-orchestration.md` (Model Diversity Fallback): `judge` carries no
84
+ per-agent override in `skills/model-profiles.json`, so the active profile's host default is
85
+ the per-slot fallback; `workflows/judge-with-debate.md` owns the live slot assignments.
86
86
 
87
87
  ## Validation Sensors
88
88
  - Every criterion score carries an exact quotation from the artifact.
@@ -71,8 +71,9 @@ overall: weighted-mean
71
71
  - References (paths relative to the `massa-ai` skill directory): `references/agent-orchestration.md`, `references/audit-report-io.md` (Judge With Debate Report Contracts).
72
72
 
73
73
  ## Model Hint
74
- See `references/agent-orchestration.md` (Model Diversity Fallback): `metadata.model_tier`
75
- (`deep`) is the fallback; `workflows/judge-with-debate.md` owns the live model assignment.
74
+ See `references/agent-orchestration.md` (Model Diversity Fallback): `meta-judge` carries no
75
+ per-agent override in `skills/model-profiles.json`, so the active profile's host default is
76
+ the fallback; `workflows/judge-with-debate.md` owns the live model assignment.
76
77
 
77
78
  ## Validation Sensors
78
79
  - Output parses as YAML; weights sum to 1.0 (±0.001); every criterion carries id, name, weight, scale (min 1, max 5), rubric anchors for 1/3/5, and a non-empty checklist.
@@ -80,9 +80,9 @@ next_step: <string>
80
80
  - References (paths relative to the `massa-ai` skill directory): `references/agent-orchestration.md`, `references/audit-report-io.md` (Judge With Debate Report Contracts).
81
81
 
82
82
  ## Model Hint
83
- See `references/agent-orchestration.md` (Model Diversity Fallback): `metadata.model_tier`
84
- (`deep`) is the per-slot fallback; `workflows/judge-with-debate.md` owns the live slot
85
- assignments.
83
+ See `references/agent-orchestration.md` (Model Diversity Fallback): `judge` carries no
84
+ per-agent override in `skills/model-profiles.json`, so the active profile's host default is
85
+ the per-slot fallback; `workflows/judge-with-debate.md` owns the live slot assignments.
86
86
 
87
87
  ## Validation Sensors
88
88
  - Every criterion score carries an exact quotation from the artifact.
@@ -71,8 +71,9 @@ overall: weighted-mean
71
71
  - References (paths relative to the `massa-ai` skill directory): `references/agent-orchestration.md`, `references/audit-report-io.md` (Judge With Debate Report Contracts).
72
72
 
73
73
  ## Model Hint
74
- See `references/agent-orchestration.md` (Model Diversity Fallback): `metadata.model_tier`
75
- (`deep`) is the fallback; `workflows/judge-with-debate.md` owns the live model assignment.
74
+ See `references/agent-orchestration.md` (Model Diversity Fallback): `meta-judge` carries no
75
+ per-agent override in `skills/model-profiles.json`, so the active profile's host default is
76
+ the fallback; `workflows/judge-with-debate.md` owns the live model assignment.
76
77
 
77
78
  ## Validation Sensors
78
79
  - Output parses as YAML; weights sum to 1.0 (±0.001); every criterion carries id, name, weight, scale (min 1, max 5), rubric anchors for 1/3/5, and a non-empty checklist.
@@ -985,7 +985,7 @@ function getConfigForEnv() {
985
985
  } else {
986
986
  console.error(`[getConfigForEnv] embedding.provider "${provider}" has no env-projection branch \u2014 no embedding env vars were set`);
987
987
  }
988
- env.LOG_LEVEL = config.logging.level;
988
+ env.MASSA_AI_LOG_LEVEL = config.logging.level;
989
989
  env.ENABLE_METRICS = String(config.logging.enableMetrics);
990
990
  return env;
991
991
  }
@@ -1383,7 +1383,7 @@ var defaultConfig = {
1383
1383
  corsOrigins: envList("MASSA_AI_API_CORS_ORIGINS", fileConfig.security?.corsOrigins ?? [])
1384
1384
  },
1385
1385
  logging: {
1386
- level: process.env.LOG_LEVEL || fileConfig.logging?.level || "info",
1386
+ level: process.env.MASSA_AI_LOG_LEVEL || fileConfig.logging?.level || "info",
1387
1387
  enableMetrics: process.env.ENABLE_METRICS === "true" || process.env.ENABLE_METRICS === undefined && !!fileConfig.logging?.enableMetrics,
1388
1388
  file: process.env.MASSA_AI_LOG_FILE || fileConfig.logging?.file || path4.join(resolvedDataDir, "logs", "massa-ai.log"),
1389
1389
  enableFileSink: envBool("MASSA_AI_LOG_ENABLE_FILE_SINK", fileConfig.logging?.enableFileSink ?? true),
@@ -1864,6 +1864,34 @@ var LOG_LEVEL_BUFFER_TAGS = {
1864
1864
  [LogLevel.WARN]: "warn",
1865
1865
  [LogLevel.ERROR]: "error"
1866
1866
  };
1867
+ var REPEAT_WINDOW_MS = 15 * 60 * 1000;
1868
+ var MAX_REPEAT_KEYS = 500;
1869
+ function formatAgo(ms) {
1870
+ const totalSeconds = Math.floor(ms / 1000);
1871
+ if (totalSeconds < 60)
1872
+ return `${totalSeconds}s`;
1873
+ return `${Math.floor(totalSeconds / 60)}m`;
1874
+ }
1875
+ var MAX_ERROR_TEXT_CHARS = 300;
1876
+ function capErrorText(value) {
1877
+ if (value.length <= MAX_ERROR_TEXT_CHARS)
1878
+ return value;
1879
+ const truncatedChars = value.length - MAX_ERROR_TEXT_CHARS;
1880
+ return `${value.slice(0, MAX_ERROR_TEXT_CHARS)}\u2026(truncated ${truncatedChars} chars)`;
1881
+ }
1882
+ function pickErrorFields(err, includeStack) {
1883
+ const out = { name: err.name, message: capErrorText(err.message) };
1884
+ if (includeStack)
1885
+ out.stack = err.stack;
1886
+ const code = err.code;
1887
+ if (code !== undefined)
1888
+ out.code = code;
1889
+ const cause = err.cause;
1890
+ if (cause !== undefined) {
1891
+ out.cause = capErrorText(cause instanceof Error ? cause.message : String(cause));
1892
+ }
1893
+ return out;
1894
+ }
1867
1895
 
1868
1896
  class Logger {
1869
1897
  _level;
@@ -1873,6 +1901,7 @@ class Logger {
1873
1901
  _maxFileSizeBytes;
1874
1902
  _maxFiles;
1875
1903
  _initialized = false;
1904
+ repeats = new Map;
1876
1905
  constructor() {}
1877
1906
  ensureInitialized() {
1878
1907
  if (!this._initialized) {
@@ -1933,13 +1962,70 @@ class Logger {
1933
1962
  shouldLog(level) {
1934
1963
  return level >= this.level;
1935
1964
  }
1965
+ serializeMetaErrors(meta) {
1966
+ if (!meta)
1967
+ return meta;
1968
+ let out;
1969
+ for (const [key, value] of Object.entries(meta)) {
1970
+ if (value instanceof Error) {
1971
+ if (!out)
1972
+ out = { ...meta };
1973
+ out[key] = pickErrorFields(value, false);
1974
+ }
1975
+ }
1976
+ return out ?? meta;
1977
+ }
1978
+ applyRepeatAccounting(level, message, meta) {
1979
+ if (level !== LogLevel.WARN && level !== LogLevel.ERROR)
1980
+ return meta;
1981
+ const label = typeof meta?.label === "string" ? meta.label : "";
1982
+ const key = `${level}|${message}|${label}`;
1983
+ const now = Date.now();
1984
+ const existing = this.repeats.get(key);
1985
+ if (!existing || now - existing.firstSeenAt > REPEAT_WINDOW_MS) {
1986
+ if (this.repeats.size >= MAX_REPEAT_KEYS)
1987
+ this.repeats.clear();
1988
+ this.repeats.set(key, { firstSeenAt: now, count: 1 });
1989
+ return meta;
1990
+ }
1991
+ existing.count += 1;
1992
+ return {
1993
+ ...meta,
1994
+ occurrences: existing.count,
1995
+ firstSeenAgo: formatAgo(now - existing.firstSeenAt)
1996
+ };
1997
+ }
1998
+ _resetRepeatsForTesting() {
1999
+ this.repeats.clear();
2000
+ }
2001
+ safeStringifyMeta(meta) {
2002
+ const seen = new WeakSet;
2003
+ try {
2004
+ return JSON.stringify(meta, (_key, value) => {
2005
+ if (typeof value === "bigint")
2006
+ return value.toString();
2007
+ if (typeof value === "object" && value !== null) {
2008
+ if (seen.has(value))
2009
+ return "[Circular]";
2010
+ seen.add(value);
2011
+ }
2012
+ return value;
2013
+ });
2014
+ } catch (err) {
2015
+ return JSON.stringify({
2016
+ metaUnserializable: err instanceof Error ? err.message : String(err)
2017
+ });
2018
+ }
2019
+ }
1936
2020
  formatMessage(level, message, meta, timestamp = new Date().toISOString()) {
1937
- const metaStr = meta ? ` ${JSON.stringify(meta)}` : "";
2021
+ const metaStr = meta ? ` ${this.safeStringifyMeta(meta)}` : "";
1938
2022
  return `[${timestamp}] [${level}] ${message}${metaStr}`;
1939
2023
  }
1940
2024
  emit(level, message, meta) {
2025
+ const serializedMeta = this.serializeMetaErrors(meta);
2026
+ const finalMeta = this.applyRepeatAccounting(level, message, serializedMeta);
1941
2027
  const ts = new Date().toISOString();
1942
- const line = this.formatMessage(LOG_LEVEL_LABELS[level], message, meta, ts);
2028
+ const line = this.formatMessage(LOG_LEVEL_LABELS[level], message, finalMeta, ts);
1943
2029
  console.error(line);
1944
2030
  if (this.enableFileSink) {
1945
2031
  const filePath = this.logFilePath;
@@ -1951,7 +2037,7 @@ class Logger {
1951
2037
  ts,
1952
2038
  level: LOG_LEVEL_BUFFER_TAGS[level],
1953
2039
  message,
1954
- ...meta ? { meta } : {}
2040
+ ...finalMeta ? { meta: finalMeta } : {}
1955
2041
  });
1956
2042
  }
1957
2043
  debug(message, meta) {
@@ -1973,11 +2059,7 @@ class Logger {
1973
2059
  if (this.shouldLog(LogLevel.ERROR)) {
1974
2060
  const errorMeta = error ? {
1975
2061
  ...meta,
1976
- error: {
1977
- name: error.name,
1978
- message: error.message,
1979
- stack: error.stack
1980
- }
2062
+ error: error instanceof Error ? pickErrorFields(error, true) : { message: String(error) }
1981
2063
  } : meta;
1982
2064
  this.emit(LogLevel.ERROR, message, errorMeta);
1983
2065
  }
@@ -2071,7 +2153,9 @@ class SmartRateLimiter {
2071
2153
  const hasRequestCapacity = this.requestLimiter.tryConsume(1);
2072
2154
  const hasTokenCapacity = this.tokenLimiter.tryConsume(estimatedTokens);
2073
2155
  if (!hasRequestCapacity) {
2074
- logger.warn("Request rate limit exceeded");
2156
+ logger.warn("Request rate limit exceeded", {
2157
+ availableTokens: this.requestLimiter.getAvailableTokens()
2158
+ });
2075
2159
  return false;
2076
2160
  }
2077
2161
  if (!hasTokenCapacity) {
@@ -2343,9 +2427,9 @@ function acquireLock(stateFilePath, options = {}) {
2343
2427
  }
2344
2428
  }
2345
2429
  // ../../packages/shared/dist/profile-switch/engine.js
2346
- import fs6 from "fs";
2347
- import path10 from "path";
2348
- import os6 from "os";
2430
+ import fs7 from "fs";
2431
+ import path11 from "path";
2432
+ import os7 from "os";
2349
2433
  import crypto3 from "crypto";
2350
2434
  import { execFileSync as execFileSync2 } from "child_process";
2351
2435
 
@@ -2425,12 +2509,13 @@ function selectRecord(records) {
2425
2509
  }
2426
2510
  return best ?? pool[pool.length - 1];
2427
2511
  }
2428
- function resolveClaudeMarketplaceRoot(opts = {}) {
2512
+ function resolveClaudeMarketplaceInstall(opts = {}) {
2429
2513
  const targetHome = opts.targetHome ?? os5.homedir();
2430
2514
  const pluginKey = opts.pluginKey ?? DEFAULT_PLUGIN_KEY;
2431
2515
  const directoryResult = resolveDirectorySourceRoot(targetHome, pluginKey);
2432
- if (directoryResult !== undefined)
2433
- return directoryResult;
2516
+ if (directoryResult !== undefined) {
2517
+ return directoryResult === null ? null : { root: directoryResult, route: "directory-source" };
2518
+ }
2434
2519
  const registryPath = path9.join(targetHome, ".claude", "plugins", "installed_plugins.json");
2435
2520
  let records;
2436
2521
  try {
@@ -2452,7 +2537,202 @@ function resolveClaudeMarketplaceRoot(opts = {}) {
2452
2537
  } catch {
2453
2538
  return null;
2454
2539
  }
2455
- return installPath;
2540
+ return { root: installPath, route: "registry-cache" };
2541
+ }
2542
+ function resolveClaudeMarketplaceRoot(opts = {}) {
2543
+ return resolveClaudeMarketplaceInstall(opts)?.root ?? null;
2544
+ }
2545
+ function readInstalledPluginVersion(opts = {}) {
2546
+ const targetHome = opts.targetHome ?? os5.homedir();
2547
+ const pluginKey = opts.pluginKey ?? DEFAULT_PLUGIN_KEY;
2548
+ const registryPath = path9.join(targetHome, ".claude", "plugins", "installed_plugins.json");
2549
+ let records;
2550
+ try {
2551
+ const parsed = JSON.parse(fs5.readFileSync(registryPath, "utf8"));
2552
+ records = parsed?.plugins?.[pluginKey];
2553
+ } catch {
2554
+ return null;
2555
+ }
2556
+ if (!Array.isArray(records) || records.length === 0)
2557
+ return null;
2558
+ return selectRecord(records)?.version ?? null;
2559
+ }
2560
+
2561
+ // ../../packages/shared/dist/profile-switch/doctor.js
2562
+ import fs6 from "fs";
2563
+ import os6 from "os";
2564
+ import path10 from "path";
2565
+
2566
+ // ../../packages/shared/dist/profile-switch/frontmatter.js
2567
+ function parseFrontmatter(raw) {
2568
+ const match = /^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/.exec(raw);
2569
+ if (!match) {
2570
+ throw new Error("charter missing YAML frontmatter (--- ... ---) block");
2571
+ }
2572
+ const yamlText = match[1] ?? "";
2573
+ const body = (match[2] ?? "").replace(/^\r?\n/, "");
2574
+ const frontmatter = parseSimpleYaml(yamlText);
2575
+ return { frontmatter, body };
2576
+ }
2577
+ function parseSimpleYaml(text) {
2578
+ const result = {};
2579
+ const lines = text.split(/\r?\n/);
2580
+ let i = 0;
2581
+ while (i < lines.length) {
2582
+ const line = lines[i] ?? "";
2583
+ if (line.trim() === "" || line.trim().startsWith("#")) {
2584
+ i++;
2585
+ continue;
2586
+ }
2587
+ const m = /^([A-Za-z_][A-Za-z0-9_]*):\s*(.*)$/.exec(line);
2588
+ if (!m) {
2589
+ i++;
2590
+ continue;
2591
+ }
2592
+ const key = m[1];
2593
+ const rest = (m[2] ?? "").trim();
2594
+ if (rest !== "") {
2595
+ result[key] = unquoteScalar(rest);
2596
+ i++;
2597
+ continue;
2598
+ }
2599
+ const nested = {};
2600
+ i++;
2601
+ while (i < lines.length) {
2602
+ const nestedLine = lines[i] ?? "";
2603
+ if (/^\s{2,}\S/.test(nestedLine) === false)
2604
+ break;
2605
+ const nm = /^\s{2,}([A-Za-z_][A-Za-z0-9_]*):\s*(.*)$/.exec(nestedLine);
2606
+ if (!nm)
2607
+ break;
2608
+ nested[nm[1]] = unquoteScalar((nm[2] ?? "").trim());
2609
+ i++;
2610
+ }
2611
+ result[key] = nested;
2612
+ }
2613
+ return result;
2614
+ }
2615
+ function unquoteScalar(s) {
2616
+ if (s.startsWith('"') && s.endsWith('"') || s.startsWith("'") && s.endsWith("'")) {
2617
+ return s.slice(1, -1);
2618
+ }
2619
+ return s;
2620
+ }
2621
+
2622
+ // ../../packages/shared/dist/profile-switch/doctor.js
2623
+ var ENV_OVERRIDE_VARS = ["CLAUDE_CODE_SUBAGENT_MODEL"];
2624
+ function readTextFile(filePath) {
2625
+ try {
2626
+ return fs6.readFileSync(filePath, "utf8");
2627
+ } catch {
2628
+ return null;
2629
+ }
2630
+ }
2631
+ function readJsonFile(filePath) {
2632
+ const raw = readTextFile(filePath);
2633
+ if (raw === null)
2634
+ return null;
2635
+ try {
2636
+ return JSON.parse(raw);
2637
+ } catch {
2638
+ return null;
2639
+ }
2640
+ }
2641
+ function readPluginVersion(pluginRoot) {
2642
+ const manifest = readJsonFile(path10.join(pluginRoot, ".claude-plugin", "plugin.json"));
2643
+ return typeof manifest?.version === "string" ? manifest.version : null;
2644
+ }
2645
+ function detectEnvOverride(env) {
2646
+ for (const name of ENV_OVERRIDE_VARS) {
2647
+ const value = env[name];
2648
+ if (typeof value === "string" && value.trim()) {
2649
+ return { name, value: value.trim() };
2650
+ }
2651
+ }
2652
+ return null;
2653
+ }
2654
+ function readRoles(liveRoot, activeProfile) {
2655
+ const agentsDir = path10.join(liveRoot, "agents");
2656
+ let entries;
2657
+ try {
2658
+ entries = fs6.readdirSync(agentsDir, { withFileTypes: true });
2659
+ } catch {
2660
+ return [];
2661
+ }
2662
+ const roles = [];
2663
+ for (const entry of entries) {
2664
+ if (!entry.isFile() || !entry.name.startsWith("massa-ai-") || !entry.name.endsWith(".md")) {
2665
+ continue;
2666
+ }
2667
+ const activeRaw = readTextFile(path10.join(agentsDir, entry.name));
2668
+ let model = null;
2669
+ let effort = null;
2670
+ if (activeRaw !== null) {
2671
+ try {
2672
+ const { frontmatter } = parseFrontmatter(activeRaw);
2673
+ model = typeof frontmatter.model === "string" ? frontmatter.model : null;
2674
+ effort = typeof frontmatter.effort === "string" ? frontmatter.effort : null;
2675
+ } catch {}
2676
+ }
2677
+ let staleVariant = false;
2678
+ if (activeProfile && activeRaw !== null) {
2679
+ const variantRaw = readTextFile(path10.join(liveRoot, "agent-profiles", activeProfile, entry.name));
2680
+ if (variantRaw !== null) {
2681
+ staleVariant = variantRaw !== activeRaw;
2682
+ }
2683
+ }
2684
+ roles.push({ name: entry.name, model, effort, staleVariant });
2685
+ }
2686
+ return roles.sort((a, b) => a.name.localeCompare(b.name));
2687
+ }
2688
+ function runtimeDriftReport(opts = {}) {
2689
+ const targetHome = opts.targetHome ?? os6.homedir();
2690
+ const host = opts.host ?? "claude";
2691
+ const stateFilePath = opts.stateFilePath ?? path10.join(targetHome, ".config", "massa-ai", "install-state.json");
2692
+ let state = opts.state ?? null;
2693
+ if (state === null) {
2694
+ try {
2695
+ state = readInstallState(stateFilePath);
2696
+ } catch {
2697
+ state = null;
2698
+ }
2699
+ }
2700
+ const platform = state?.platforms?.[host];
2701
+ const stateVersion = typeof platform?.plugin?.version === "string" ? platform.plugin.version : null;
2702
+ const activeProfile = platform?.modelProfile?.profile ?? null;
2703
+ if (host !== "claude") {
2704
+ return {
2705
+ host,
2706
+ route: "unresolved",
2707
+ liveRoot: null,
2708
+ sourceVersion: null,
2709
+ stateVersion,
2710
+ pinnedVersion: null,
2711
+ activeProfile,
2712
+ roles: [],
2713
+ envOverride: detectEnvOverride(opts.env ?? process.env),
2714
+ versionDrift: false,
2715
+ profileMaterialized: false
2716
+ };
2717
+ }
2718
+ const install = resolveClaudeMarketplaceInstall({ targetHome, pluginKey: opts.pluginKey });
2719
+ const liveRoot = install?.root ?? null;
2720
+ const sourceVersion = liveRoot === null ? null : readPluginVersion(liveRoot);
2721
+ const pinnedVersion = readInstalledPluginVersion({ targetHome, pluginKey: opts.pluginKey });
2722
+ const roles = liveRoot === null ? [] : readRoles(liveRoot, activeProfile);
2723
+ return {
2724
+ host: "claude",
2725
+ route: install?.route ?? "unresolved",
2726
+ liveRoot,
2727
+ sourceVersion,
2728
+ stateVersion,
2729
+ pinnedVersion,
2730
+ activeProfile,
2731
+ roles,
2732
+ envOverride: detectEnvOverride(opts.env ?? process.env),
2733
+ versionDrift: sourceVersion !== null && stateVersion !== null && sourceVersion !== stateVersion,
2734
+ profileMaterialized: roles.some((role) => role.staleVariant)
2735
+ };
2456
2736
  }
2457
2737
 
2458
2738
  // ../../packages/shared/dist/profile-switch/engine.js
@@ -2470,10 +2750,10 @@ function namedError3(name, message) {
2470
2750
  var UnknownProfileError = (profile, known) => namedError3("UnknownProfileError", `unknown profile "${profile}" \u2014 installed: ${known.length > 0 ? known.join(", ") : "none"}`);
2471
2751
  var NoHostsDetectedError = () => namedError3("NoHostsDetectedError", "no installed hosts found");
2472
2752
  function defaultStatePath(targetHome) {
2473
- return path10.join(targetHome, ".config", "massa-ai", "install-state.json");
2753
+ return path11.join(targetHome, ".config", "massa-ai", "install-state.json");
2474
2754
  }
2475
2755
  function resolveCommon(opts) {
2476
- const targetHome = opts.targetHome ?? os6.homedir();
2756
+ const targetHome = opts.targetHome ?? os7.homedir();
2477
2757
  const stateFilePath = opts.stateFilePath ?? defaultStatePath(targetHome);
2478
2758
  return { targetHome, stateFilePath };
2479
2759
  }
@@ -2481,7 +2761,7 @@ function marketplaceRoots(targetHome, state) {
2481
2761
  return state.platforms.claude?.installRoute === "marketplace" ? { claude: resolveClaudeMarketplaceRoot({ targetHome }) ?? undefined } : {};
2482
2762
  }
2483
2763
  function claudeMarketplaceUnresolvedReason(targetHome) {
2484
- const registryPath = path10.join(targetHome, ".claude", "plugins", "installed_plugins.json");
2764
+ const registryPath = path11.join(targetHome, ".claude", "plugins", "installed_plugins.json");
2485
2765
  return `claude installRoute is "marketplace" but no install root could be resolved from ${registryPath} ` + "\u2014 re-run the Claude plugin installer, or verify the plugin registry file";
2486
2766
  }
2487
2767
  function listProfiles(opts = {}) {
@@ -2489,6 +2769,12 @@ function listProfiles(opts = {}) {
2489
2769
  const state = readInstallState(stateFilePath);
2490
2770
  const roots = marketplaceRoots(targetHome, state);
2491
2771
  const universe = opts.hosts ?? HOSTS;
2772
+ const claudeDrift = universe.includes("claude") ? runtimeDriftReport({ targetHome, stateFilePath, state, env: opts.env }) : null;
2773
+ const claudeDriftFields = (host) => host === "claude" && claudeDrift !== null ? {
2774
+ liveRoot: claudeDrift.liveRoot,
2775
+ sourceVersion: claudeDrift.sourceVersion,
2776
+ envOverride: claudeDrift.envOverride ? `${claudeDrift.envOverride.name}=${claudeDrift.envOverride.value}` : null
2777
+ } : { liveRoot: null, sourceVersion: null, envOverride: null };
2492
2778
  const hosts = universe.map((host) => {
2493
2779
  if (host === "claude" && state.platforms.claude?.installRoute === "marketplace" && roots.claude === undefined) {
2494
2780
  const platform2 = state.platforms.claude;
@@ -2497,9 +2783,10 @@ function listProfiles(opts = {}) {
2497
2783
  installed: false,
2498
2784
  skipped: false,
2499
2785
  skipReason: null,
2500
- activeProfile: platform2.modelProfile?.profile ?? opts.hostDefaults?.[host] ?? "balanced",
2786
+ activeProfile: platform2.modelProfile?.profile ?? "balanced",
2501
2787
  bundleVersion: platform2.plugin?.version ?? null,
2502
- availableProfiles: []
2788
+ availableProfiles: [],
2789
+ ...claudeDriftFields(host)
2503
2790
  };
2504
2791
  }
2505
2792
  const layout = resolveHostLayout(host, { targetHome, projectRoot: opts.projectRoot, marketplaceRoot: roots });
@@ -2511,10 +2798,11 @@ function listProfiles(opts = {}) {
2511
2798
  skipReason: layout.reason,
2512
2799
  activeProfile: null,
2513
2800
  bundleVersion: null,
2514
- availableProfiles: []
2801
+ availableProfiles: [],
2802
+ ...claudeDriftFields(host)
2515
2803
  };
2516
2804
  }
2517
- const installed = fs6.existsSync(layout.activeDir);
2805
+ const installed = fs7.existsSync(layout.activeDir);
2518
2806
  const availableProfiles = listVariantProfiles(layout);
2519
2807
  const platform = state.platforms[host];
2520
2808
  return {
@@ -2522,17 +2810,18 @@ function listProfiles(opts = {}) {
2522
2810
  installed,
2523
2811
  skipped: false,
2524
2812
  skipReason: null,
2525
- activeProfile: platform?.modelProfile?.profile ?? opts.hostDefaults?.[host] ?? "balanced",
2813
+ activeProfile: platform?.modelProfile?.profile ?? "balanced",
2526
2814
  bundleVersion: platform?.plugin?.version ?? null,
2527
- availableProfiles
2815
+ availableProfiles,
2816
+ ...claudeDriftFields(host)
2528
2817
  };
2529
2818
  });
2530
2819
  return { hosts };
2531
2820
  }
2532
2821
  function listVariantProfiles(layout) {
2533
- if (!fs6.existsSync(layout.variantsRoot))
2822
+ if (!fs7.existsSync(layout.variantsRoot))
2534
2823
  return [];
2535
- return fs6.readdirSync(layout.variantsRoot, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name).sort();
2824
+ return fs7.readdirSync(layout.variantsRoot, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name).sort();
2536
2825
  }
2537
2826
  function matchesGlob(filename, glob) {
2538
2827
  const starIdx = glob.indexOf("*");
@@ -2543,7 +2832,7 @@ function matchesGlob(filename, glob) {
2543
2832
  return filename.length >= prefix.length + suffix.length && filename.startsWith(prefix) && filename.endsWith(suffix);
2544
2833
  }
2545
2834
  function matchingFileNames(dir, glob) {
2546
- return fs6.readdirSync(dir, { withFileTypes: true }).filter((e) => e.isFile() && matchesGlob(e.name, glob)).map((e) => e.name);
2835
+ return fs7.readdirSync(dir, { withFileTypes: true }).filter((e) => e.isFile() && matchesGlob(e.name, glob)).map((e) => e.name);
2547
2836
  }
2548
2837
  function detectGitAvailability(dir) {
2549
2838
  try {
@@ -2571,7 +2860,7 @@ function gitTrackedFileNames(dir, filenames) {
2571
2860
  var GUARD_PASS = { blocked: false, unchecked: false };
2572
2861
  var GUARD_UNCHECKED = { blocked: false, unchecked: true };
2573
2862
  function checkTrackedPathGuard(activeDir, filenames) {
2574
- if (filenames.length === 0 || !fs6.existsSync(activeDir))
2863
+ if (filenames.length === 0 || !fs7.existsSync(activeDir))
2575
2864
  return GUARD_PASS;
2576
2865
  const availability = detectGitAvailability(activeDir);
2577
2866
  if (availability === "no-git")
@@ -2582,53 +2871,53 @@ function checkTrackedPathGuard(activeDir, filenames) {
2582
2871
  if (tracked.size === 0)
2583
2872
  return GUARD_PASS;
2584
2873
  const offending = filenames.find((name) => tracked.has(name));
2585
- return { blocked: true, path: path10.join(activeDir, offending), unchecked: false };
2874
+ return { blocked: true, path: path11.join(activeDir, offending), unchecked: false };
2586
2875
  }
2587
2876
  function assertStateWritable(stateFilePath) {
2588
- const dir = path10.dirname(stateFilePath);
2877
+ const dir = path11.dirname(stateFilePath);
2589
2878
  try {
2590
- fs6.mkdirSync(dir, { recursive: true });
2879
+ fs7.mkdirSync(dir, { recursive: true });
2591
2880
  } catch (err) {
2592
2881
  throw UnwritableInstallStateError(stateFilePath, err.message);
2593
2882
  }
2594
- const checkPath = fs6.existsSync(stateFilePath) ? stateFilePath : dir;
2883
+ const checkPath = fs7.existsSync(stateFilePath) ? stateFilePath : dir;
2595
2884
  try {
2596
- fs6.accessSync(checkPath, fs6.constants.W_OK);
2885
+ fs7.accessSync(checkPath, fs7.constants.W_OK);
2597
2886
  } catch (err) {
2598
2887
  throw UnwritableInstallStateError(stateFilePath, err.message);
2599
2888
  }
2600
2889
  }
2601
2890
  function copyFileRouteVariant(layout, variantDir) {
2602
- fs6.mkdirSync(layout.activeDir, { recursive: true });
2891
+ fs7.mkdirSync(layout.activeDir, { recursive: true });
2603
2892
  let changed = 0;
2604
- for (const entry of fs6.readdirSync(variantDir, { withFileTypes: true })) {
2893
+ for (const entry of fs7.readdirSync(variantDir, { withFileTypes: true })) {
2605
2894
  if (!entry.isFile() || !matchesGlob(entry.name, layout.activeGlob))
2606
2895
  continue;
2607
- fs6.copyFileSync(path10.join(variantDir, entry.name), path10.join(layout.activeDir, entry.name));
2896
+ fs7.copyFileSync(path11.join(variantDir, entry.name), path11.join(layout.activeDir, entry.name));
2608
2897
  changed++;
2609
2898
  }
2610
2899
  return changed;
2611
2900
  }
2612
2901
  function repointOpencodeVariant(layout, variantDir) {
2613
- fs6.mkdirSync(layout.activeDir, { recursive: true });
2902
+ fs7.mkdirSync(layout.activeDir, { recursive: true });
2614
2903
  let changed = 0;
2615
- for (const entry of fs6.readdirSync(variantDir, { withFileTypes: true })) {
2904
+ for (const entry of fs7.readdirSync(variantDir, { withFileTypes: true })) {
2616
2905
  if (!entry.isFile() || !matchesGlob(entry.name, layout.activeGlob))
2617
2906
  continue;
2618
- const dest = path10.join(layout.activeDir, entry.name);
2619
- const target = path10.resolve(path10.join(variantDir, entry.name));
2907
+ const dest = path11.join(layout.activeDir, entry.name);
2908
+ const target = path11.resolve(path11.join(variantDir, entry.name));
2620
2909
  let destExists = true;
2621
2910
  let destIsSymlink = false;
2622
2911
  try {
2623
- destIsSymlink = fs6.lstatSync(dest).isSymbolicLink();
2912
+ destIsSymlink = fs7.lstatSync(dest).isSymbolicLink();
2624
2913
  } catch {
2625
2914
  destExists = false;
2626
2915
  }
2627
2916
  if (destExists && !destIsSymlink)
2628
2917
  continue;
2629
2918
  const tmp = `${dest}.massa-ai-switch.${crypto3.randomUUID()}`;
2630
- fs6.symlinkSync(target, tmp);
2631
- fs6.renameSync(tmp, dest);
2919
+ fs7.symlinkSync(target, tmp);
2920
+ fs7.renameSync(tmp, dest);
2632
2921
  changed++;
2633
2922
  }
2634
2923
  return changed;
@@ -2668,13 +2957,13 @@ function switchProfile(opts) {
2668
2957
  if (fileHosts.length === 0) {
2669
2958
  return { profile: opts.profile, dryRun, hosts: orderRows(universe, skipRows), restartRequired: false };
2670
2959
  }
2671
- const installedFileHosts = fileHosts.filter((h) => fs6.existsSync(h.layout.activeDir));
2960
+ const installedFileHosts = fileHosts.filter((h) => fs7.existsSync(h.layout.activeDir));
2672
2961
  if (installedFileHosts.length === 0)
2673
2962
  throw NoHostsDetectedError();
2674
2963
  const withAvailability = fileHosts.map((h) => {
2675
- const variantsRootExists = fs6.existsSync(h.layout.variantsRoot);
2964
+ const variantsRootExists = fs7.existsSync(h.layout.variantsRoot);
2676
2965
  const variantDir = h.layout.variantDir(opts.profile);
2677
- const available = variantsRootExists && fs6.existsSync(variantDir) && fs6.statSync(variantDir).isDirectory();
2966
+ const available = variantsRootExists && fs7.existsSync(variantDir) && fs7.statSync(variantDir).isDirectory();
2678
2967
  return { ...h, variantsRootExists, variantDir, available };
2679
2968
  });
2680
2969
  if (!withAvailability.some((h) => h.available)) {
@@ -2710,7 +2999,7 @@ function switchProfile(opts) {
2710
2999
  continue;
2711
3000
  }
2712
3001
  if (dryRun) {
2713
- rows.push({ host: h.host, status: "switched" });
3002
+ rows.push({ host: h.host, status: "would-switch" });
2714
3003
  continue;
2715
3004
  }
2716
3005
  const candidateNames = matchingFileNames(h.variantDir, h.layout.activeGlob);
@@ -2751,15 +3040,15 @@ function orderRows(universe, rows) {
2751
3040
  }
2752
3041
  // ../../packages/shared/dist/profile-switch/report.js
2753
3042
  function reportSucceeded(report) {
2754
- return report.hosts.every((h) => h.status === "switched" || h.status === "skipped");
3043
+ return report.hosts.every((h) => h.status === "switched" || h.status === "would-switch" || h.status === "skipped");
2755
3044
  }
2756
3045
  // ../../packages/shared/dist/profile-switch/variant-sync.js
2757
- import fs7 from "fs";
2758
- import path11 from "path";
2759
- import os7 from "os";
3046
+ import fs8 from "fs";
3047
+ import path12 from "path";
3048
+ import os8 from "os";
2760
3049
  import crypto4 from "crypto";
2761
3050
  function defaultStatePath2(targetHome) {
2762
- return path11.join(targetHome, ".config", "massa-ai", "install-state.json");
3051
+ return path12.join(targetHome, ".config", "massa-ai", "install-state.json");
2763
3052
  }
2764
3053
  function marketplaceRoots2(targetHome, state) {
2765
3054
  return state.platforms.claude?.installRoute === "marketplace" ? { claude: resolveClaudeMarketplaceRoot({ targetHome }) ?? undefined } : {};
@@ -2767,13 +3056,13 @@ function marketplaceRoots2(targetHome, state) {
2767
3056
  var tempFileCounter2 = 0;
2768
3057
  function writeFileIntoDirAtomically(destDir, destName, content) {
2769
3058
  const unique = `${process.pid}.${++tempFileCounter2}.${crypto4.randomBytes(6).toString("hex")}`;
2770
- const tempFile = path11.join(destDir, `.${destName}.${unique}.tmp`);
3059
+ const tempFile = path12.join(destDir, `.${destName}.${unique}.tmp`);
2771
3060
  try {
2772
- fs7.writeFileSync(tempFile, content);
2773
- fs7.renameSync(tempFile, path11.join(destDir, destName));
3061
+ fs8.writeFileSync(tempFile, content);
3062
+ fs8.renameSync(tempFile, path12.join(destDir, destName));
2774
3063
  } catch (error) {
2775
3064
  try {
2776
- fs7.unlinkSync(tempFile);
3065
+ fs8.unlinkSync(tempFile);
2777
3066
  } catch {}
2778
3067
  throw error;
2779
3068
  }
@@ -2781,20 +3070,20 @@ function writeFileIntoDirAtomically(destDir, destName, content) {
2781
3070
  function isSafeDirName(name) {
2782
3071
  if (name === "." || name === "..")
2783
3072
  return false;
2784
- if (name.includes("/") || name.includes("\\") || name.includes(path11.sep))
3073
+ if (name.includes("/") || name.includes("\\") || name.includes(path12.sep))
2785
3074
  return false;
2786
- return path11.basename(name) === name;
3075
+ return path12.basename(name) === name;
2787
3076
  }
2788
3077
  function syncHost(host, sourceRoot, targetHome, marketplaceRoot) {
2789
3078
  const layout = resolveHostLayout(host, { targetHome, marketplaceRoot });
2790
3079
  if (layout.route === "skip") {
2791
3080
  return { host, status: "skipped", profiles: [], retained: [], files: 0, reason: layout.reason };
2792
3081
  }
2793
- const srcDir = path11.join(sourceRoot, "apps", `${host}-plugin`, "agent-profiles");
2794
- if (!fs7.existsSync(srcDir) || !fs7.statSync(srcDir).isDirectory()) {
3082
+ const srcDir = path12.join(sourceRoot, "apps", `${host}-plugin`, "agent-profiles");
3083
+ if (!fs8.existsSync(srcDir) || !fs8.statSync(srcDir).isDirectory()) {
2795
3084
  return { host, status: "skipped", profiles: [], retained: [], files: 0, reason: "no generated variants for this host" };
2796
3085
  }
2797
- if (!fs7.existsSync(layout.variantsRoot)) {
3086
+ if (!fs8.existsSync(layout.variantsRoot)) {
2798
3087
  return {
2799
3088
  host,
2800
3089
  status: "skipped",
@@ -2806,24 +3095,24 @@ function syncHost(host, sourceRoot, targetHome, marketplaceRoot) {
2806
3095
  }
2807
3096
  const profiles = [];
2808
3097
  let files = 0;
2809
- for (const entry of fs7.readdirSync(srcDir, { withFileTypes: true })) {
3098
+ for (const entry of fs8.readdirSync(srcDir, { withFileTypes: true })) {
2810
3099
  if (!entry.isDirectory())
2811
3100
  continue;
2812
3101
  if (!isSafeDirName(entry.name))
2813
3102
  continue;
2814
- const srcProfileDir = path11.join(srcDir, entry.name);
2815
- const destProfileDir = path11.join(layout.variantsRoot, entry.name);
2816
- fs7.mkdirSync(destProfileDir, { recursive: true });
2817
- for (const fileEntry of fs7.readdirSync(srcProfileDir, { withFileTypes: true })) {
3103
+ const srcProfileDir = path12.join(srcDir, entry.name);
3104
+ const destProfileDir = path12.join(layout.variantsRoot, entry.name);
3105
+ fs8.mkdirSync(destProfileDir, { recursive: true });
3106
+ for (const fileEntry of fs8.readdirSync(srcProfileDir, { withFileTypes: true })) {
2818
3107
  if (!fileEntry.isFile())
2819
3108
  continue;
2820
- const content = fs7.readFileSync(path11.join(srcProfileDir, fileEntry.name));
3109
+ const content = fs8.readFileSync(path12.join(srcProfileDir, fileEntry.name));
2821
3110
  writeFileIntoDirAtomically(destProfileDir, fileEntry.name, content);
2822
3111
  files++;
2823
3112
  }
2824
3113
  profiles.push(entry.name);
2825
3114
  }
2826
- const retained = fs7.readdirSync(layout.variantsRoot, { withFileTypes: true }).filter((e) => e.isDirectory() && !profiles.includes(e.name)).map((e) => e.name).sort();
3115
+ const retained = fs8.readdirSync(layout.variantsRoot, { withFileTypes: true }).filter((e) => e.isDirectory() && !profiles.includes(e.name)).map((e) => e.name).sort();
2827
3116
  return { host, status: "synced", profiles: profiles.sort(), retained, files };
2828
3117
  }
2829
3118
  function syncGeneratedVariants(opts) {
@@ -2839,7 +3128,7 @@ function syncGeneratedVariants(opts) {
2839
3128
  }));
2840
3129
  }
2841
3130
  const sourceRoot = opts.sourceRoot;
2842
- const targetHome = opts.targetHome ?? os7.homedir();
3131
+ const targetHome = opts.targetHome ?? os8.homedir();
2843
3132
  const state = readInstallState(defaultStatePath2(targetHome));
2844
3133
  const roots = marketplaceRoots2(targetHome, state);
2845
3134
  return hosts.map((host) => {
@@ -2851,14 +3140,14 @@ function syncGeneratedVariants(opts) {
2851
3140
  });
2852
3141
  }
2853
3142
  // ../../packages/shared/dist/profile-switch/repo-root.js
2854
- import fs8 from "fs";
2855
- import path12 from "path";
3143
+ import fs9 from "fs";
3144
+ import path13 from "path";
2856
3145
  function findRepoRootWithMarker(startDir, marker, maxLevels) {
2857
3146
  let dir = startDir;
2858
3147
  for (let i = 0;i <= maxLevels; i++) {
2859
- if (fs8.existsSync(path12.join(dir, marker)))
3148
+ if (fs9.existsSync(path13.join(dir, marker)))
2860
3149
  return dir;
2861
- const parent = path12.dirname(dir);
3150
+ const parent = path13.dirname(dir);
2862
3151
  if (parent === dir)
2863
3152
  break;
2864
3153
  dir = parent;
@@ -2953,7 +3242,7 @@ function assertKnownRuleId(id) {
2953
3242
  }
2954
3243
  // ../../packages/shared/dist/bootstrap/state.js
2955
3244
  init_config_loader();
2956
- import fs9 from "fs";
3245
+ import fs10 from "fs";
2957
3246
  var BOOTSTRAP_STATE_KEY = "bootstrap";
2958
3247
  var BOOTSTRAP_RULES_KEY = "rules";
2959
3248
  var BOOTSTRAP_STATE_PATH = `${BOOTSTRAP_STATE_KEY}.${BOOTSTRAP_RULES_KEY}`;
@@ -2987,7 +3276,7 @@ function resolveBootstrapState(doc) {
2987
3276
  }
2988
3277
  function readConfigBytes() {
2989
3278
  try {
2990
- return fs9.readFileSync(getConfigPath(), "utf-8");
3279
+ return fs10.readFileSync(getConfigPath(), "utf-8");
2991
3280
  } catch (error) {
2992
3281
  if (error?.code === "ENOENT")
2993
3282
  return "";
@@ -3035,7 +3324,7 @@ function setBootstrapRuleEnabled(id, enabled) {
3035
3324
  };
3036
3325
  }
3037
3326
  // ../../packages/shared/dist/bootstrap/render.js
3038
- import path13 from "path";
3327
+ import path14 from "path";
3039
3328
  var BOOTSTRAP_BLOCK_START = "<!-- massa-ai:bootstrap:start -->";
3040
3329
  var BOOTSTRAP_BLOCK_END = "<!-- massa-ai:bootstrap:end -->";
3041
3330
  var CONTRACT_FILENAME = "MASSA-AI.md";
@@ -3067,19 +3356,19 @@ var HOST_CONFIG_DIR = {
3067
3356
  function resolveHostRoot(host, targetHome, hostRoot) {
3068
3357
  requireAbsoluteTargetHome(targetHome);
3069
3358
  if (hostRoot === undefined)
3070
- return path13.join(targetHome, ...HOST_CONFIG_DIR[host]);
3071
- const relative = path13.relative(targetHome, hostRoot);
3072
- if (!path13.isAbsolute(hostRoot) || relative === "" || relative.startsWith("..") || path13.isAbsolute(relative)) {
3359
+ return path14.join(targetHome, ...HOST_CONFIG_DIR[host]);
3360
+ const relative = path14.relative(targetHome, hostRoot);
3361
+ if (!path14.isAbsolute(hostRoot) || relative === "" || relative.startsWith("..") || path14.isAbsolute(relative)) {
3073
3362
  throw new BootstrapRenderError("HostRootOutsideTargetHomeError", `hostRoot must be an absolute directory inside targetHome, got "${hostRoot}" for targetHome "${targetHome}"`, [hostRoot, targetHome]);
3074
3363
  }
3075
3364
  return hostRoot;
3076
3365
  }
3077
3366
  function bootstrapContractPath(host, targetHome, hostRoot) {
3078
- return path13.join(resolveHostRoot(host, targetHome, hostRoot), CONTRACT_FILENAME);
3367
+ return path14.join(resolveHostRoot(host, targetHome, hostRoot), CONTRACT_FILENAME);
3079
3368
  }
3080
3369
  function bootstrapStateFilePath(targetHome) {
3081
3370
  requireAbsoluteTargetHome(targetHome);
3082
- return path13.join(targetHome, ".config", "massa-ai", "config.json");
3371
+ return path14.join(targetHome, ".config", "massa-ai", "config.json");
3083
3372
  }
3084
3373
  function renderBootstrap(options) {
3085
3374
  const { source, state, host, targetHome, hostRoot } = options;
@@ -3102,7 +3391,7 @@ ${body}`;
3102
3391
  return { contract, pointer };
3103
3392
  }
3104
3393
  function requireAbsoluteTargetHome(targetHome) {
3105
- if (!path13.isAbsolute(targetHome)) {
3394
+ if (!path14.isAbsolute(targetHome)) {
3106
3395
  throw new BootstrapRenderError("TargetHomeNotAbsoluteError", `targetHome must be an absolute path, got "${targetHome}"`, [targetHome]);
3107
3396
  }
3108
3397
  }
@@ -3255,8 +3544,8 @@ function buildBootstrapReport(input) {
3255
3544
  }
3256
3545
  // ../../packages/shared/dist/bootstrap/engine.js
3257
3546
  init_config_loader();
3258
- import fs10 from "fs";
3259
- import path14 from "path";
3547
+ import fs11 from "fs";
3548
+ import path15 from "path";
3260
3549
  var INSTALL_STATE_FILENAME = "install-state.json";
3261
3550
  var WIRING_REMEDY = "scripts/install-skills.sh --apply";
3262
3551
 
@@ -3273,7 +3562,7 @@ function applyBootstrapState(options) {
3273
3562
  const dryRun = options.dryRun ?? false;
3274
3563
  const warn = options.onWarning ?? ((message) => console.warn(message));
3275
3564
  const configPath = bootstrapStateFilePath(targetHome);
3276
- const installStatePath = path14.join(path14.dirname(configPath), INSTALL_STATE_FILENAME);
3565
+ const installStatePath = path15.join(path15.dirname(configPath), INSTALL_STATE_FILENAME);
3277
3566
  const { platforms } = readInstallState(installStatePath);
3278
3567
  const installed = HOSTS.filter((host) => platforms[host] !== undefined);
3279
3568
  if (installed.length === 0) {
@@ -3366,22 +3655,22 @@ function applyHost(input) {
3366
3655
  }
3367
3656
  function wiringArtifact(host, targetHome, hostRoot) {
3368
3657
  const root = resolveHostRoot(host, targetHome, hostRoot);
3369
- const contractPath = path14.join(root, CONTRACT_FILENAME);
3658
+ const contractPath = path15.join(root, CONTRACT_FILENAME);
3370
3659
  switch (host) {
3371
3660
  case "claude":
3372
- return { file: path14.join(root, "CLAUDE.md"), token: `@${CONTRACT_FILENAME}` };
3661
+ return { file: path15.join(root, "CLAUDE.md"), token: `@${CONTRACT_FILENAME}` };
3373
3662
  case "codex":
3374
3663
  case "cursor":
3375
- return { file: path14.join(root, "AGENTS.md"), token: contractPath };
3664
+ return { file: path15.join(root, "AGENTS.md"), token: contractPath };
3376
3665
  case "opencode":
3377
3666
  return { file: openCodeConfigPath(root), token: `"${contractPath}"` };
3378
3667
  }
3379
3668
  }
3380
3669
  function openCodeConfigPath(root) {
3381
- const json = path14.join(root, "opencode.json");
3382
- if (fs10.existsSync(json))
3670
+ const json = path15.join(root, "opencode.json");
3671
+ if (fs11.existsSync(json))
3383
3672
  return json;
3384
- return path14.join(root, "opencode.jsonc");
3673
+ return path15.join(root, "opencode.jsonc");
3385
3674
  }
3386
3675
  function isWired(host, targetHome, hostRoot) {
3387
3676
  const artifact = wiringArtifact(host, targetHome, hostRoot);
@@ -3394,7 +3683,7 @@ function notWiredReason(host, targetHome, hostRoot) {
3394
3683
  }
3395
3684
  function readFileOrNull(filePath) {
3396
3685
  try {
3397
- return fs10.readFileSync(filePath, "utf-8");
3686
+ return fs11.readFileSync(filePath, "utf-8");
3398
3687
  } catch {
3399
3688
  return null;
3400
3689
  }
@@ -3440,11 +3729,11 @@ function formatBootstrapReport(report) {
3440
3729
  init_config_loader();
3441
3730
  // src/config-cli.ts
3442
3731
  init_inference_providers();
3443
- import { promises as fs11 } from "fs";
3444
- import path15 from "path";
3445
- import os8 from "os";
3732
+ import { promises as fs12 } from "fs";
3733
+ import path16 from "path";
3734
+ import os9 from "os";
3446
3735
  import { fileURLToPath } from "url";
3447
- var __dirname2 = path15.dirname(fileURLToPath(import.meta.url));
3736
+ var __dirname2 = path16.dirname(fileURLToPath(import.meta.url));
3448
3737
  var WRITABLE_PROVIDERS = ["ollama", "lmstudio", "mistral", "openai", "google", "cohere"];
3449
3738
  var GENERATOR_MARKER = "scripts/generate-subagent-artifacts.ts";
3450
3739
  var GENERATOR_MARKER_MAX_LEVELS = 6;
@@ -3487,6 +3776,14 @@ Commands:
3487
3776
  profile set <name> [--host <h>] [--dry-run]
3488
3777
  Switch installed agents to a profile (restart required after)
3489
3778
 
3779
+ doctor [--fix] [--host <h>] [--target <dir>]
3780
+ Report agent model/profile drift: live-tree vs recorded
3781
+ versions, per-role models, variant staleness, env
3782
+ overrides. --fix re-runs the profile switch for the
3783
+ recorded active profile (restart required after).
3784
+ --target redirects the home the state/registries are
3785
+ read from (test seam, same convention as bootstrap)
3786
+
3490
3787
  bootstrap list List every startup-contract rule: state, default, description
3491
3788
  bootstrap show Same as 'bootstrap list'
3492
3789
  bootstrap enable <rule-id> [--target <dir> --yes] [--dry-run]
@@ -3504,6 +3801,8 @@ Examples:
3504
3801
  massa-ai-config set embedding.dimensions 1024
3505
3802
  massa-ai-config agents install --user
3506
3803
  massa-ai-config profile set work --dry-run
3804
+ massa-ai-config doctor
3805
+ massa-ai-config doctor --fix
3507
3806
  massa-ai-config bootstrap list
3508
3807
  massa-ai-config bootstrap disable caveman
3509
3808
  `);
@@ -3547,6 +3846,30 @@ function formatSwitchReport(report) {
3547
3846
  A host session restart is required for the change to take effect.`);
3548
3847
  }
3549
3848
  }
3849
+ function formatDriftReport(report) {
3850
+ console.log(`doctor (${report.host}, route: ${report.route})`);
3851
+ console.log(` live root: ${report.liveRoot ?? "n/a"}`);
3852
+ console.log(` source version: ${report.sourceVersion ?? "n/a"} (live tree)`);
3853
+ console.log(` state version: ${report.stateVersion ?? "n/a"} (install-state)`);
3854
+ console.log(` pinned version: ${report.pinnedVersion ?? "n/a"} (installed_plugins)`);
3855
+ console.log(` active profile: ${report.activeProfile ?? "n/a"}`);
3856
+ for (const role of report.roles) {
3857
+ const stale = role.staleVariant ? " \u2014 STALE vs the recorded profile's variant" : "";
3858
+ console.log(` ${role.name}: model=${role.model ?? "unknown"} effort=${role.effort ?? "unknown"}${stale}`);
3859
+ }
3860
+ if (report.versionDrift) {
3861
+ console.log(` drift: live tree ${report.sourceVersion} != recorded ${report.stateVersion} \u2014 update the plugin (or re-run the installer)`);
3862
+ }
3863
+ if (report.profileMaterialized) {
3864
+ console.log(" drift: active agent files differ from the recorded profile's variants \u2014 run `massa-ai-config doctor --fix` (re-runs the profile switch)");
3865
+ }
3866
+ if (report.envOverride) {
3867
+ console.log(` override: ${report.envOverride.name}=${report.envOverride.value} wins over every per-agent model at runtime \u2014 remove it from the host env to let profiles govern`);
3868
+ }
3869
+ if (report.route !== "unresolved" && !report.versionDrift && !report.profileMaterialized && !report.envOverride) {
3870
+ console.log(" healthy: every recording agrees.");
3871
+ }
3872
+ }
3550
3873
  async function runCli(argv) {
3551
3874
  const args = argv;
3552
3875
  const command = args[0];
@@ -3722,18 +4045,18 @@ Using defaults:`);
3722
4045
  return 1;
3723
4046
  }
3724
4047
  const scope = typeof options.project === "boolean" ? "project" : "user";
3725
- const agentsDir = scope === "project" ? path15.join(process.cwd(), ".opencode/agents") : path15.join(process.env.XDG_CONFIG_HOME && process.env.XDG_CONFIG_HOME.trim() || path15.join(os8.homedir(), ".config"), "opencode", "agents");
3726
- const sourceAgentsDir = path15.resolve(__dirname2, "..", "agents");
4048
+ const agentsDir = scope === "project" ? path16.join(process.cwd(), ".opencode/agents") : path16.join(process.env.XDG_CONFIG_HOME && process.env.XDG_CONFIG_HOME.trim() || path16.join(os9.homedir(), ".config"), "opencode", "agents");
4049
+ const sourceAgentsDir = path16.resolve(__dirname2, "..", "agents");
3727
4050
  if (subcommand === "install") {
3728
- await fs11.mkdir(agentsDir, { recursive: true });
4051
+ await fs12.mkdir(agentsDir, { recursive: true });
3729
4052
  let count = 0;
3730
- const entries = await fs11.readdir(sourceAgentsDir);
4053
+ const entries = await fs12.readdir(sourceAgentsDir);
3731
4054
  for (const entry of entries) {
3732
4055
  if (!entry.startsWith("massa-ai-") || !entry.endsWith(".md"))
3733
4056
  continue;
3734
- const src = path15.join(sourceAgentsDir, entry);
3735
- const dest = path15.join(agentsDir, entry);
3736
- await fs11.copyFile(src, dest);
4057
+ const src = path16.join(sourceAgentsDir, entry);
4058
+ const dest = path16.join(agentsDir, entry);
4059
+ await fs12.copyFile(src, dest);
3737
4060
  count++;
3738
4061
  }
3739
4062
  console.log(`+ ${count} subagent specialists (generated from skills/agents/*/SKILL.md)`);
@@ -3741,14 +4064,14 @@ Using defaults:`);
3741
4064
  } else {
3742
4065
  let removed = 0;
3743
4066
  try {
3744
- const entries = await fs11.readdir(agentsDir);
4067
+ const entries = await fs12.readdir(agentsDir);
3745
4068
  for (const entry of entries) {
3746
4069
  if (!entry.startsWith("massa-ai-") || !entry.endsWith(".md"))
3747
4070
  continue;
3748
- const filePath = path15.join(agentsDir, entry);
3749
- const content = await fs11.readFile(filePath, "utf8");
4071
+ const filePath = path16.join(agentsDir, entry);
4072
+ const content = await fs12.readFile(filePath, "utf8");
3750
4073
  if (content.includes("massa-ai-owned: true")) {
3751
- await fs11.unlink(filePath);
4074
+ await fs12.unlink(filePath);
3752
4075
  removed++;
3753
4076
  }
3754
4077
  }
@@ -3801,6 +4124,39 @@ Using defaults:`);
3801
4124
  console.error("Usage: massa-ai-config profile <list|show|set> ...");
3802
4125
  return 1;
3803
4126
  }
4127
+ case "doctor": {
4128
+ const fix = options["fix"] === true;
4129
+ const hostOpt = typeof options.host === "string" ? options.host : undefined;
4130
+ if (hostOpt !== undefined && !isHost(hostOpt)) {
4131
+ console.error(`Error: unknown host "${hostOpt}"`);
4132
+ return 1;
4133
+ }
4134
+ const host = hostOpt ?? "claude";
4135
+ const targetHome = typeof options.target === "string" ? options.target : os9.homedir();
4136
+ try {
4137
+ let report = runtimeDriftReport({ targetHome, host });
4138
+ if (fix) {
4139
+ const profile = report.activeProfile;
4140
+ if (!profile) {
4141
+ console.error("Error: no recorded active profile in install-state.json \u2014 run " + "`massa-ai-config profile set <name>` first; there is nothing to fix from.");
4142
+ return 1;
4143
+ }
4144
+ const sourceRoot = findRepoRootWithMarker(__dirname2, GENERATOR_MARKER, GENERATOR_MARKER_MAX_LEVELS);
4145
+ formatVariantSync(syncGeneratedVariants({ sourceRoot, targetHome }));
4146
+ const switchReport = switchProfile({ profile, host, targetHome });
4147
+ formatSwitchReport(switchReport);
4148
+ if (!reportSucceeded(switchReport)) {
4149
+ return 1;
4150
+ }
4151
+ report = runtimeDriftReport({ targetHome, host });
4152
+ }
4153
+ formatDriftReport(report);
4154
+ return 0;
4155
+ } catch (e) {
4156
+ console.error(`Error: ${e.message}`);
4157
+ return 1;
4158
+ }
4159
+ }
3804
4160
  case "bootstrap": {
3805
4161
  const subcommand = args[1];
3806
4162
  if (subcommand === "list" || subcommand === "show") {
@@ -3825,9 +4181,9 @@ Using defaults:`);
3825
4181
  return 1;
3826
4182
  }
3827
4183
  const targetOpt = typeof options.target === "string" ? options.target : undefined;
3828
- const targetHome = targetOpt === undefined ? os8.homedir() : path15.resolve(targetOpt);
3829
- if (targetHome !== os8.homedir() && options.yes !== true) {
3830
- console.error(`Error: --target ${targetHome} is not your home (${os8.homedir()}) \u2014 pass --yes to confirm writing there`);
4184
+ const targetHome = targetOpt === undefined ? os9.homedir() : path16.resolve(targetOpt);
4185
+ if (targetHome !== os9.homedir() && options.yes !== true) {
4186
+ console.error(`Error: --target ${targetHome} is not your home (${os9.homedir()}) \u2014 pass --yes to confirm writing there`);
3831
4187
  return 1;
3832
4188
  }
3833
4189
  const dryRun = options["dry-run"] === true;
@@ -3845,7 +4201,7 @@ Using defaults:`);
3845
4201
  const report = applyBootstrapState({
3846
4202
  targetHome,
3847
4203
  dryRun,
3848
- sourcePath: repoRoot === null ? undefined : path15.join(repoRoot, "skills", "AGENTS.md")
4204
+ sourcePath: repoRoot === null ? undefined : path16.join(repoRoot, "skills", "AGENTS.md")
3849
4205
  });
3850
4206
  console.log(formatBootstrapReport(report));
3851
4207
  return bootstrapReportSucceeded(report) ? 0 : 1;
package/dist/index.js CHANGED
@@ -984,7 +984,7 @@ function getConfigForEnv() {
984
984
  } else {
985
985
  console.error(`[getConfigForEnv] embedding.provider "${provider}" has no env-projection branch \u2014 no embedding env vars were set`);
986
986
  }
987
- env.LOG_LEVEL = config.logging.level;
987
+ env.MASSA_AI_LOG_LEVEL = config.logging.level;
988
988
  env.ENABLE_METRICS = String(config.logging.enableMetrics);
989
989
  return env;
990
990
  }
@@ -1382,7 +1382,7 @@ var defaultConfig = {
1382
1382
  corsOrigins: envList("MASSA_AI_API_CORS_ORIGINS", fileConfig.security?.corsOrigins ?? [])
1383
1383
  },
1384
1384
  logging: {
1385
- level: process.env.LOG_LEVEL || fileConfig.logging?.level || "info",
1385
+ level: process.env.MASSA_AI_LOG_LEVEL || fileConfig.logging?.level || "info",
1386
1386
  enableMetrics: process.env.ENABLE_METRICS === "true" || process.env.ENABLE_METRICS === undefined && !!fileConfig.logging?.enableMetrics,
1387
1387
  file: process.env.MASSA_AI_LOG_FILE || fileConfig.logging?.file || path4.join(resolvedDataDir, "logs", "massa-ai.log"),
1388
1388
  enableFileSink: envBool("MASSA_AI_LOG_ENABLE_FILE_SINK", fileConfig.logging?.enableFileSink ?? true),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@massa-ai/opencode-plugin",
3
- "version": "1.60.0",
3
+ "version": "1.61.0",
4
4
  "description": "massa-ai plugin for OpenCode - Semantic code search, memory, and context compression",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -24,8 +24,8 @@
24
24
  "dependencies": {
25
25
  "@opencode-ai/plugin": "^1.2.15",
26
26
  "@opencode-ai/sdk": "^1.2.15",
27
- "@massa-ai/core": "^1.60.0",
28
- "@massa-ai/shared": "^1.60.0"
27
+ "@massa-ai/core": "^1.61.0",
28
+ "@massa-ai/shared": "^1.61.0"
29
29
  },
30
30
  "devDependencies": {
31
31
  "@types/node": "^22.10.5",