aienvmp 0.1.10 → 0.1.12

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/BUGFIXES.md CHANGED
@@ -28,6 +28,12 @@ Short record of bugs, fixes, and follow-up checks.
28
28
  - Fix: test command used `/bin/zsh -lc` to load login shell PATH.
29
29
  - Verification: remote test detected Node `v25.3.0`, npm `11.11.0`, and completed `aienvmp sync`.
30
30
 
31
+ ### Security summaries lacked remediation hints
32
+
33
+ - Issue: AI agents could see vulnerable package names but not enough bounded detail to plan the next dependency update.
34
+ - Fix: npm and Python security summaries now include fix versions and advisory references when scanners provide them.
35
+ - Verification: parser tests cover npm remediation objects and pip-audit advisory ids; Windows and macOS tarball tests completed `sync --security`.
36
+
31
37
  ## Template
32
38
 
33
39
  ### Title
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.12
4
+
5
+ - Added AI-readable `recommendedActions` to `context --json` and `doctor --json`.
6
+ - Added concise recommended actions to text context and doctor output.
7
+ - Kept recommendations advisory-only; strict failure still requires `doctor --ci`.
8
+
9
+ ## 0.1.11
10
+
11
+ - Added bounded remediation details to security summaries, including fix versions and advisory references.
12
+ - Surfaced remediation hints in `AIENV.md` and the dashboard so AI agents can plan safer dependency updates.
13
+ - Kept security scanning read-only and opt-in.
14
+
3
15
  ## 0.1.10
4
16
 
5
17
  - Added optional `pip-audit` JSON parsing for Python security summaries.
package/README.md CHANGED
@@ -55,7 +55,7 @@ npx aienvmp snippet agents
55
55
  ```bash
56
56
  aienvmp sync # update env map, light SBOM, ledger, dashboard
57
57
  aienvmp context # AI preflight brief
58
- aienvmp context --json # machine-readable AI decision context
58
+ aienvmp context --json # machine-readable AI decision context + recommended actions
59
59
  aienvmp handoff # next-agent handoff summary
60
60
  aienvmp intent # record a planned env change
61
61
  aienvmp record # record what changed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aienvmp",
3
- "version": "0.1.10",
3
+ "version": "0.1.12",
4
4
  "description": "AI-first environment maps and lightweight runtime SBOMs for shared development machines.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/actions.js ADDED
@@ -0,0 +1,87 @@
1
+ export function recommendedActions(manifest = {}, context = {}) {
2
+ const warnings = context.warnings || [];
3
+ const intents = context.intents || [];
4
+ const actions = [];
5
+
6
+ if (isStaleWarning(warnings)) {
7
+ actions.push(action("sync-snapshot", "high", "sync", "Refresh the environment snapshot before changing runtimes or package managers.", "aienvmp sync"));
8
+ }
9
+
10
+ if (intents.length) {
11
+ actions.push(action("review-open-intents", "high", "coordination", "Review or resolve open environment intents before another agent changes the environment.", "aienvmp context --json"));
12
+ }
13
+
14
+ if (warnings.some((warning) => warning.code === "conflicting-open-intents")) {
15
+ actions.push(action("coordinate-agents", "high", "coordination", "Multiple agents are planning changes to the same environment target. Coordinate with the user before proceeding."));
16
+ }
17
+
18
+ actions.push(...securityActions(manifest.security));
19
+
20
+ if (hasRuntimePolicyWarning(warnings)) {
21
+ actions.push(action("review-version-policy", "medium", "runtime", "Detected runtime or package manager policy drift. Prefer project-local version files and ask before global changes."));
22
+ }
23
+
24
+ if (warnings.some((warning) => warning.code === "handoff-stale")) {
25
+ actions.push(action("record-handoff", "medium", "handoff", "Record an AI handoff after environment changes so the next agent starts with current context.", "aienvmp handoff --record --actor agent:id"));
26
+ }
27
+
28
+ if (!actions.length) {
29
+ actions.push(action("continue-project-local", "low", "workflow", "Continue with project-local work. Record intent before environment changes.", "aienvmp intent --actor agent:id --action planned-change"));
30
+ }
31
+
32
+ return dedupeActions(actions).slice(0, 8);
33
+ }
34
+
35
+ function securityActions(security = {}) {
36
+ if (!security.enabled) return [];
37
+
38
+ const summary = security.summary || {};
39
+ const highRisk = Number(summary.critical || 0) > 0 || Number(summary.high || 0) > 0;
40
+ const packages = (security.topPackages || []).slice(0, 5);
41
+ if (!highRisk && !packages.length) return [];
42
+
43
+ const packageHints = packages.map((pkg) => {
44
+ const fix = pkg.fixVersions?.length ? `fix ${pkg.fixVersions.slice(0, 2).join(", ")}` : pkg.fixAvailable ? "fix available" : "review required";
45
+ return `${pkg.name} (${pkg.severity}, ${fix})`;
46
+ });
47
+
48
+ return [action(
49
+ "review-security-remediation",
50
+ highRisk ? "high" : "medium",
51
+ "security",
52
+ packageHints.length
53
+ ? `Review vulnerable packages before dependency or deployment changes: ${packageHints.join("; ")}.`
54
+ : "Review vulnerability summary before dependency or deployment changes.",
55
+ "aienvmp context --json"
56
+ )];
57
+ }
58
+
59
+ function action(id, priority, category, summary, command = "") {
60
+ return { id, priority, category, summary, command };
61
+ }
62
+
63
+ function dedupeActions(actions) {
64
+ const seen = new Set();
65
+ return actions.filter((item) => {
66
+ if (seen.has(item.id)) return false;
67
+ seen.add(item.id);
68
+ return true;
69
+ });
70
+ }
71
+
72
+ function isStaleWarning(warnings) {
73
+ return warnings.some((warning) => warning.code === "manifest-stale" || warning.code === "stale-open-intent");
74
+ }
75
+
76
+ function hasRuntimePolicyWarning(warnings) {
77
+ return warnings.some((warning) => [
78
+ "node-version-mismatch",
79
+ "python-version-mismatch",
80
+ "mixed-node-lockfiles",
81
+ "python-missing",
82
+ "docker-missing",
83
+ "policy-node-mismatch",
84
+ "policy-python-mismatch",
85
+ "policy-package-manager-mismatch"
86
+ ].includes(warning.code));
87
+ }
@@ -4,6 +4,7 @@ import { openIntents, readJsonl, readTimeline } from "../timeline.js";
4
4
  import { intentsPath, manifestPath, timelinePath, workspaceDir } from "../paths.js";
5
5
  import { renderContext } from "../render.js";
6
6
  import { loadPolicy, policyWarnings } from "../policy.js";
7
+ import { recommendedActions } from "../actions.js";
7
8
 
8
9
  export async function contextWorkspace(args) {
9
10
  const dir = workspaceDir(args);
@@ -14,10 +15,12 @@ export async function contextWorkspace(args) {
14
15
  const policy = await loadPolicy(dir);
15
16
  const warnings = [...diagnose(manifest, { timeline, intents }), ...policyWarnings(manifest, policy)];
16
17
  const decision = contextDecision(warnings, intents);
18
+ const actions = recommendedActions(manifest, { warnings, intents });
17
19
  if (args.json) {
18
20
  console.log(JSON.stringify({
19
21
  status: warnings.length ? "review-required" : "clear",
20
22
  decision,
23
+ recommendedActions: actions,
21
24
  trust: manifest.trust || {},
22
25
  guidance: decision,
23
26
  workspace: manifest.workspace,
@@ -35,7 +38,7 @@ export async function contextWorkspace(args) {
35
38
  }, null, 2));
36
39
  return;
37
40
  }
38
- console.log(renderContext(manifest, timeline, warnings, intents, policy));
41
+ console.log(renderContext(manifest, timeline, warnings, intents, policy, actions));
39
42
  }
40
43
 
41
44
  function securitySummary(security = {}) {
@@ -3,6 +3,7 @@ import { readJson } from "../fsutil.js";
3
3
  import { openIntents, readJsonl, readTimeline } from "../timeline.js";
4
4
  import { intentsPath, manifestPath, timelinePath, workspaceDir } from "../paths.js";
5
5
  import { loadPolicy, policyWarnings } from "../policy.js";
6
+ import { recommendedActions } from "../actions.js";
6
7
 
7
8
  export async function doctorWorkspace(args) {
8
9
  const dir = workspaceDir(args);
@@ -12,13 +13,15 @@ export async function doctorWorkspace(args) {
12
13
  const timeline = await readTimeline(timelinePath(dir));
13
14
  const intents = openIntents(await readJsonl(intentsPath(dir)));
14
15
  const warnings = [...diagnose(manifest, { timeline, intents }), ...policyWarnings(manifest, policy)];
16
+ const actions = recommendedActions(manifest, { warnings, intents });
15
17
  if (args.json) {
16
18
  console.log(JSON.stringify({
17
19
  status: warnings.length ? "warning" : "ok",
18
20
  trust: manifest.trust || {},
19
21
  policy,
20
22
  openIntentCount: intents.length,
21
- warnings
23
+ warnings,
24
+ recommendedActions: actions
22
25
  }, null, 2));
23
26
  if (args.ci && warnings.length) {
24
27
  process.exitCode = 1;
@@ -32,6 +35,10 @@ export async function doctorWorkspace(args) {
32
35
  for (const warning of warnings) {
33
36
  console.log(`[${warning.code}] ${warning.message}`);
34
37
  }
38
+ console.log("recommended actions:");
39
+ for (const item of actions) {
40
+ console.log(`- [${item.priority}] ${item.summary}${item.command ? ` (${item.command})` : ""}`);
41
+ }
35
42
  console.log("doctor: warnings are non-blocking by default; pass --ci to fail automation.");
36
43
  if (args.ci) {
37
44
  process.exitCode = 1;
package/src/render.js CHANGED
@@ -84,7 +84,7 @@ Before changing runtimes, package managers, Docker settings, global packages, or
84
84
  \`aienvmp\` does not replace this instruction file. It provides the live env map, lightweight runtime SBOM, intent log, timeline, and dashboard.`;
85
85
  }
86
86
 
87
- export function renderContext(manifest, timeline = [], warnings = [], intents = [], policy = {}) {
87
+ export function renderContext(manifest, timeline = [], warnings = [], intents = [], policy = {}, recommendedActions = []) {
88
88
  const status = warnings.length ? "review-required" : "clear";
89
89
  const next = warnings.length ? "Review warnings before changing the environment." : "Continue with project-local work. Record intent before environment changes.";
90
90
  return [
@@ -115,6 +115,9 @@ export function renderContext(manifest, timeline = [], warnings = [], intents =
115
115
  "Warnings:",
116
116
  ...(warnings.length ? warnings.map((w) => `- ${w.message}`) : ["- none"]),
117
117
  "",
118
+ "Recommended actions:",
119
+ ...(recommendedActions.length ? recommendedActions.map((item) => `- [${item.priority}] ${item.summary}${item.command ? ` (${item.command})` : ""}`) : ["- none"]),
120
+ "",
118
121
  "Open intents:",
119
122
  ...(intents.length ? intents.slice(-5).reverse().map((i) => `- ${i.actor}: ${i.action}`) : ["- none"]),
120
123
  "",
@@ -220,7 +223,9 @@ const inventoryHtml=manifest.inventory?.enabled?('<table>'+Object.entries(invent
220
223
  const sec=manifest.security||{};
221
224
  const secSummary=sec.summary||{total:0,critical:0,high:0,moderate:0,low:0,info:0};
222
225
  const secPackages=sec.topPackages||[];
223
- const securityHtml=sec.enabled?\`<table><tr><th>Total</th><td><code>\${esc(secSummary.total||0)}</code></td></tr><tr><th>Critical</th><td><code>\${esc(secSummary.critical||0)}</code></td></tr><tr><th>High</th><td><code>\${esc(secSummary.high||0)}</code></td></tr><tr><th>Moderate</th><td><code>\${esc(secSummary.moderate||0)}</code></td></tr><tr><th>Low</th><td><code>\${esc(secSummary.low||0)}</code></td></tr></table>\${secPackages.length?'<div class="timeline">'+secPackages.slice(0,5).map(p=>\`<div class="event"><time>\${esc(p.severity)}</time><div><b>\${esc(p.name)}</b> \${p.fixAvailable?'fix available':'review required'}</div></div>\`).join('')+'</div>':'<div class="okline" style="margin-top:10px">No vulnerable packages reported.</div>'}\`:'<div class="okline">Security scan is off. Run <code>aienvmp sync --security</code> for read-only vulnerability summary.</div>';
226
+ const securityFix=p=>p.fixVersions?.length?\`fix \${p.fixVersions.slice(0,3).join(', ')}\`:(p.fixAvailable?'fix available':'review required');
227
+ const securityRefs=p=>p.advisories?.length?\` · \${p.advisories.map(a=>a.id||a.title).filter(Boolean).slice(0,2).join(', ')}\`:'';
228
+ const securityHtml=sec.enabled?\`<table><tr><th>Total</th><td><code>\${esc(secSummary.total||0)}</code></td></tr><tr><th>Critical</th><td><code>\${esc(secSummary.critical||0)}</code></td></tr><tr><th>High</th><td><code>\${esc(secSummary.high||0)}</code></td></tr><tr><th>Moderate</th><td><code>\${esc(secSummary.moderate||0)}</code></td></tr><tr><th>Low</th><td><code>\${esc(secSummary.low||0)}</code></td></tr></table>\${secPackages.length?'<div class="timeline">'+secPackages.slice(0,5).map(p=>\`<div class="event"><time>\${esc(p.severity)}</time><div><b>\${esc(p.name)}</b> \${esc(securityFix(p))}\${esc(securityRefs(p))}</div></div>\`).join('')+'</div>':'<div class="okline" style="margin-top:10px">No vulnerable packages reported.</div>'}\`:'<div class="okline">Security scan is off. Run <code>aienvmp sync --security</code> for read-only vulnerability summary.</div>';
224
229
  const change=c=>c.type==='changed'?\`\${c.scope} \${c.key}: \${c.before} -> \${c.after}\`:\`\${c.scope} \${c.key}: \${c.type} \${c.after||c.before}\`;
225
230
  const timelineLabel=t=>t.change?change(t.change):(t.summary||t.action||t.type||'recorded change');
226
231
  const agentNames={agents:'Codex',claude:'Claude',gemini:'Gemini'};
@@ -350,12 +355,21 @@ function securityLines(security = {}) {
350
355
  if (packages.length) {
351
356
  lines.push("- Top vulnerable packages:");
352
357
  for (const pkg of packages.slice(0, 8)) {
353
- lines.push(` - ${pkg.name}: ${pkg.severity}; ${pkg.fixAvailable ? "fix available" : "review required"}`);
358
+ lines.push(` - ${pkg.name}: ${pkg.severity}; ${securityPackageNote(pkg)}`);
354
359
  }
355
360
  }
356
361
  return lines;
357
362
  }
358
363
 
364
+ function securityPackageNote(pkg) {
365
+ const fix = pkg.fixVersions?.length ? `fix ${pkg.fixVersions.slice(0, 3).join(", ")}` : pkg.fixAvailable ? "fix available" : "review required";
366
+ const advisories = (pkg.advisories || [])
367
+ .map((item) => item.id || item.title)
368
+ .filter(Boolean)
369
+ .slice(0, 2);
370
+ return advisories.length ? `${fix}; advisories ${advisories.join(", ")}` : fix;
371
+ }
372
+
359
373
  function policyLines(policy) {
360
374
  const lines = [];
361
375
  if (policy.node) lines.push(`- Node version policy: ${policy.node}`);
package/src/security.js CHANGED
@@ -80,12 +80,18 @@ export function parseNpmAudit(raw) {
80
80
  const vulnerabilities = parsed.vulnerabilities || {};
81
81
  const severityCounts = Object.fromEntries(SEVERITIES.map((severity) => [severity, Number(metadata.vulnerabilities?.[severity] || 0)]));
82
82
  const total = Number(metadata.vulnerabilities?.total || Object.values(severityCounts).reduce((sum, value) => sum + value, 0));
83
- const vulnerablePackages = Object.entries(vulnerabilities).slice(0, 20).map(([name, value]) => ({
84
- name,
85
- severity: value.severity || "unknown",
86
- viaCount: Array.isArray(value.via) ? value.via.length : 0,
87
- fixAvailable: Boolean(value.fixAvailable)
88
- }));
83
+ const vulnerablePackages = Object.entries(vulnerabilities).slice(0, 20).map(([name, value]) => {
84
+ const fixVersions = npmFixVersions(value.fixAvailable);
85
+ const advisories = npmAdvisories(value.via);
86
+ return {
87
+ name,
88
+ severity: value.severity || "unknown",
89
+ viaCount: Array.isArray(value.via) ? value.via.length : 0,
90
+ fixAvailable: hasFixAvailable(value.fixAvailable),
91
+ fixVersions,
92
+ advisories
93
+ };
94
+ });
89
95
  return {
90
96
  available: true,
91
97
  summary: { total, ...severityCounts },
@@ -109,7 +115,8 @@ export function parsePipAudit(raw) {
109
115
  severity: "unknown",
110
116
  viaCount: dependency.vulns.length,
111
117
  fixAvailable: dependency.vulns.some((vuln) => Array.isArray(vuln.fix_versions) && vuln.fix_versions.length),
112
- fixVersions: unique(dependency.vulns.flatMap((vuln) => vuln.fix_versions || [])).slice(0, 5)
118
+ fixVersions: unique(dependency.vulns.flatMap((vuln) => vuln.fix_versions || [])).slice(0, 5),
119
+ advisories: pipAdvisories(dependency.vulns)
113
120
  }));
114
121
  const total = vulnerablePackages.reduce((sum, pkg) => sum + pkg.viaCount, 0);
115
122
  return {
@@ -151,3 +158,38 @@ function unavailable(scanner, reason) {
151
158
  function unique(items) {
152
159
  return [...new Set(items.filter(Boolean))];
153
160
  }
161
+
162
+ function hasFixAvailable(value) {
163
+ if (typeof value === "boolean") return value;
164
+ return Boolean(value && typeof value === "object");
165
+ }
166
+
167
+ function npmFixVersions(value) {
168
+ if (!value || typeof value !== "object") return [];
169
+ return unique([value.version]).slice(0, 5);
170
+ }
171
+
172
+ function npmAdvisories(via = []) {
173
+ if (!Array.isArray(via)) return [];
174
+ return via
175
+ .filter((item) => item && typeof item === "object")
176
+ .map((item) => ({
177
+ id: String(item.source || item.id || item.cve || item.name || "").trim(),
178
+ title: String(item.title || "").trim(),
179
+ url: String(item.url || "").trim(),
180
+ severity: item.severity || "unknown"
181
+ }))
182
+ .filter((item) => item.id || item.title || item.url)
183
+ .slice(0, 5);
184
+ }
185
+
186
+ function pipAdvisories(vulns = []) {
187
+ return vulns
188
+ .map((vuln) => ({
189
+ id: String(vuln.id || vuln.aliases?.[0] || "").trim(),
190
+ aliases: Array.isArray(vuln.aliases) ? vuln.aliases.slice(0, 5) : [],
191
+ fixVersions: Array.isArray(vuln.fix_versions) ? vuln.fix_versions.slice(0, 5) : []
192
+ }))
193
+ .filter((item) => item.id || item.aliases.length || item.fixVersions.length)
194
+ .slice(0, 5);
195
+ }