aienvmp 0.1.5 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.7
4
+
5
+ - Added stale open intent warnings for long-running environment change plans.
6
+ - Added stale handoff warnings when environment changes happen after the last recorded AI handoff.
7
+ - Added optional `aienvmp handoff --record --actor <agent:id>` timeline entries.
8
+
9
+ ## 0.1.6
10
+
11
+ - Added optional `sync --deep` / `scan --deep` read-only global tool inventory.
12
+ - Kept the default scan lightweight while exposing deep inventory summaries to `AIENV.md`, `context`, `handoff`, and the dashboard.
13
+ - Added parsers for npm global packages, pipx tools, uv tools, and Homebrew package versions.
14
+
3
15
  ## 0.1.5
4
16
 
5
17
  - Added machine-readable trust states for observed, planned, changed, review, verified, and stale environment facts.
package/README.md CHANGED
@@ -18,6 +18,13 @@ Core loop: scan the env, give AI a preflight context, and hand off safe next ste
18
18
  npx aienvmp sync
19
19
  npx aienvmp context
20
20
  npx aienvmp handoff
21
+ npx aienvmp handoff --record --actor agent:codex
22
+ ```
23
+
24
+ Optional deeper read-only inventory:
25
+
26
+ ```bash
27
+ npx aienvmp sync --deep
21
28
  ```
22
29
 
23
30
  ## Output
@@ -59,7 +66,8 @@ aienvmp doctor --ci # strict CI check
59
66
  - simple by default
60
67
  - AI-first
61
68
  - lightweight
62
- - non-blocking unless strict mode is requested
69
+ - one advisory engine, optional enforcement with `doctor --ci`
70
+ - non-blocking unless strict mode is explicitly requested
63
71
 
64
72
  ## Development
65
73
 
package/ROADMAP.md CHANGED
@@ -6,6 +6,7 @@
6
6
 
7
7
  - Strengthen trust states: observed, planned, changed, review, verified, stale
8
8
  - Detect multi-agent environment intent conflicts
9
+ - Keep one advisory decision engine with optional strict enforcement
9
10
  - Stabilize `.aienvmp/manifest.json` and JSON command schemas
10
11
  - Keep `sync`, `context`, and `handoff` as the simple core flow
11
12
  - Improve the dashboard for 10-second human review
@@ -17,13 +18,11 @@
17
18
  - pyenv, uv, conda
18
19
  - mise, asdf
19
20
  - Global tool inventory:
20
- - `npm -g`
21
- - `pipx list`
22
- - `uv tool list`
21
+ - richer summaries for `npm -g`, `pipx list`, `uv tool list`, and Homebrew
22
+ - optional `--deep` scanners for more toolchains
23
23
  - Conflict detection:
24
- - stale unresolved intents
25
- - recent runtime changes without a fresh handoff
26
24
  - package manager policy vs lockfile mismatch
25
+ - monorepo/project boundary aware intent targets
27
26
  - CI mode:
28
27
  - stable exit codes
29
28
  - GitHub Action example
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aienvmp",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "AI-first environment maps and lightweight runtime SBOMs for shared development machines.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.js CHANGED
@@ -74,9 +74,9 @@ function printUsage() {
74
74
  console.log(`aienvmp - AI-first env map + lightweight runtime SBOM
75
75
 
76
76
  Usage:
77
- aienvmp sync [--dir .] [--json] [--quiet]
77
+ aienvmp sync [--dir .] [--json] [--quiet] [--deep]
78
78
  aienvmp context [--dir .] [--json]
79
- aienvmp handoff [--dir .] [--json]
79
+ aienvmp handoff [--dir .] [--json] [--record --actor agent:id]
80
80
 
81
81
  Common:
82
82
  aienvmp sync update AIENV.md, manifest, ledger, intents, and dashboard
@@ -87,7 +87,7 @@ Common:
87
87
 
88
88
  Advanced:
89
89
  aienvmp init [--dir .]
90
- aienvmp scan [--dir .]
90
+ aienvmp scan [--dir .] [--deep]
91
91
  aienvmp intent [--dir .] --actor agent:codex --action "install pnpm"
92
92
  aienvmp resolve [--dir .] --actor human:you --id <intent-id> [--status resolved|cancelled]
93
93
  aienvmp record [--dir .] --actor agent:codex --summary "updated .nvmrc" [--target node] [--before 20] [--after 24]
@@ -24,6 +24,7 @@ export async function contextWorkspace(args) {
24
24
  runtimes: manifest.runtimes,
25
25
  packageManagers: manifest.packageManagers,
26
26
  containers: manifest.containers,
27
+ inventory: inventorySummary(manifest.inventory),
27
28
  projectHints: manifest.projectHints,
28
29
  warnings,
29
30
  policy,
@@ -36,6 +37,15 @@ export async function contextWorkspace(args) {
36
37
  console.log(renderContext(manifest, timeline, warnings, intents, policy));
37
38
  }
38
39
 
40
+ function inventorySummary(inventory = {}) {
41
+ const tools = inventory.tools || {};
42
+ return {
43
+ mode: inventory.mode || "basic",
44
+ enabled: inventory.enabled === true,
45
+ groups: Object.fromEntries(Object.entries(tools).map(([name, items]) => [name, items.length]))
46
+ };
47
+ }
48
+
39
49
  function contextDecision(warnings, intents) {
40
50
  const reviewRequired = warnings.length > 0 || intents.length > 0;
41
51
  return {
@@ -1,9 +1,10 @@
1
1
  import { diagnose } from "../doctor.js";
2
- import { readJson } from "../fsutil.js";
2
+ import { appendJsonLine, readJson } from "../fsutil.js";
3
3
  import { loadPolicy, policyWarnings } from "../policy.js";
4
4
  import { intentsPath, manifestPath, timelinePath, workspaceDir } from "../paths.js";
5
5
  import { renderHandoff } from "../render.js";
6
6
  import { openIntents, readJsonl, readTimeline } from "../timeline.js";
7
+ import { changedTrust } from "../trust.js";
7
8
 
8
9
  export async function handoffWorkspace(args) {
9
10
  const dir = workspaceDir(args);
@@ -14,6 +15,9 @@ export async function handoffWorkspace(args) {
14
15
  const policy = await loadPolicy(dir);
15
16
  const warnings = [...diagnose(manifest, { timeline, intents }), ...policyWarnings(manifest, policy)];
16
17
  const handoff = buildHandoff(manifest, timeline, warnings, intents, policy);
18
+ if (args.record) {
19
+ await recordHandoff(timelinePath(dir), handoff, args.actor || "agent:unknown");
20
+ }
17
21
 
18
22
  if (args.json) {
19
23
  console.log(JSON.stringify(handoff, null, 2));
@@ -23,6 +27,19 @@ export async function handoffWorkspace(args) {
23
27
  return handoff;
24
28
  }
25
29
 
30
+ async function recordHandoff(file, handoff, actor) {
31
+ const now = new Date();
32
+ await appendJsonLine(file, {
33
+ at: now.toISOString(),
34
+ actor,
35
+ type: "agent-handoff",
36
+ summary: `handoff ${handoff.status}`,
37
+ status: handoff.status,
38
+ warnings: handoff.warnings.map((warning) => warning.code),
39
+ trust: changedTrust(now, handoff.status !== "clear")
40
+ });
41
+ }
42
+
26
43
  export function buildHandoff(manifest, timeline = [], warnings = [], intents = [], policy = {}) {
27
44
  const reviewRequired = warnings.length > 0 || intents.length > 0;
28
45
  return {
@@ -35,6 +52,7 @@ export function buildHandoff(manifest, timeline = [], warnings = [], intents = [
35
52
  python: manifest.runtimes?.python || manifest.runtimes?.python3 || "not detected",
36
53
  docker: manifest.containers?.docker ? "available" : "not detected"
37
54
  },
55
+ inventory: inventorySummary(manifest.inventory),
38
56
  policy: {
39
57
  node: policy.node || "not set",
40
58
  python: policy.python || "not set",
@@ -55,3 +73,12 @@ export function buildHandoff(manifest, timeline = [], warnings = [], intents = [
55
73
  : "continue with project-local work; record intent before environment changes"
56
74
  };
57
75
  }
76
+
77
+ function inventorySummary(inventory = {}) {
78
+ const tools = inventory.tools || {};
79
+ return {
80
+ mode: inventory.mode || "basic",
81
+ enabled: inventory.enabled === true,
82
+ groups: Object.fromEntries(Object.entries(tools).map(([name, items]) => [name, items.length]))
83
+ };
84
+ }
@@ -11,7 +11,7 @@ export async function scanWorkspace(args) {
11
11
  if (previous && await exists(currentPath)) {
12
12
  await fs.copyFile(currentPath, previousManifestPath(dir));
13
13
  }
14
- const manifest = await buildManifest(dir);
14
+ const manifest = await buildManifest(dir, { deep: args.deep });
15
15
  await writeJson(currentPath, manifest);
16
16
  const changes = diffManifests(previous, manifest);
17
17
  for (const change of changes) {
package/src/doctor.js CHANGED
@@ -43,6 +43,8 @@ export function diagnose(manifest, context = {}) {
43
43
  });
44
44
  }
45
45
  warnings.push(...coordinationWarnings(context.intents || []));
46
+ warnings.push(...staleIntentWarnings(context.intents || []));
47
+ warnings.push(...handoffWarnings(context.timeline || []));
46
48
  return warnings;
47
49
  }
48
50
 
@@ -69,6 +71,30 @@ export function coordinationWarnings(intents = []) {
69
71
  return warnings;
70
72
  }
71
73
 
74
+ export function staleIntentWarnings(intents = [], now = new Date(), maxAgeHours = 4) {
75
+ const warnings = [];
76
+ for (const intent of intents) {
77
+ if (!isStaleTimestamp(intent.at, now, maxAgeHours)) continue;
78
+ warnings.push({
79
+ code: "stale-open-intent",
80
+ target: intent.target || inferTarget(intent.action) || "",
81
+ message: `Open intent ${intent.id || ""} from ${intent.actor || "unknown"} is older than ${maxAgeHours} hours. Resolve it or confirm it before environment changes.`.replace(/\s+/g, " ").trim()
82
+ });
83
+ }
84
+ return warnings;
85
+ }
86
+
87
+ export function handoffWarnings(timeline = []) {
88
+ const lastEnvChange = [...timeline].reverse().find(isEnvironmentChange);
89
+ if (!lastEnvChange) return [];
90
+ const lastHandoff = [...timeline].reverse().find((item) => item.type === "agent-handoff");
91
+ if (lastHandoff && new Date(lastHandoff.at).getTime() >= new Date(lastEnvChange.at).getTime()) return [];
92
+ return [{
93
+ code: "handoff-stale",
94
+ message: "Environment changes were recorded after the last AI handoff. Run `aienvmp handoff --record --actor agent:id` before the next agent continues."
95
+ }];
96
+ }
97
+
72
98
  function inferTarget(action = "") {
73
99
  const normalized = String(action).toLowerCase();
74
100
  for (const target of ["node", "python", "docker", "npm", "pnpm", "yarn", "uv", "pip", "pipx"]) {
@@ -76,3 +102,24 @@ function inferTarget(action = "") {
76
102
  }
77
103
  return "";
78
104
  }
105
+
106
+ function isEnvironmentChange(item = {}) {
107
+ if (["runtime", "package-manager", "container"].includes(item.change?.scope)) return true;
108
+ if (item.type === "detected-change") return false;
109
+ const text = `${item.type || ""} ${item.target || ""} ${item.summary || ""} ${item.action || ""} ${item.change?.scope || ""} ${item.change?.key || ""}`.toLowerCase();
110
+ return [
111
+ "runtime",
112
+ "node",
113
+ "python",
114
+ "docker",
115
+ "package manager",
116
+ "package-manager",
117
+ "npm",
118
+ "pnpm",
119
+ "yarn",
120
+ "uv",
121
+ "pip",
122
+ "pipx",
123
+ "global"
124
+ ].some((token) => text.includes(token));
125
+ }
@@ -0,0 +1,88 @@
1
+ import { commandOutput } from "./shell.js";
2
+
3
+ const MAX_ITEMS = 40;
4
+
5
+ export async function scanGlobalInventory(options = {}) {
6
+ if (!options.deep) {
7
+ return {
8
+ mode: "basic",
9
+ enabled: false,
10
+ note: "Run `aienvmp sync --deep` to collect read-only global tool inventory."
11
+ };
12
+ }
13
+
14
+ return {
15
+ mode: "deep",
16
+ enabled: true,
17
+ note: "Read-only global tool inventory. Use for AI awareness, not enforcement.",
18
+ tools: compact({
19
+ npmGlobal: await scanNpmGlobal(),
20
+ pipx: await scanPipx(),
21
+ uvTools: await scanUvTools(),
22
+ brew: await scanBrew()
23
+ })
24
+ };
25
+ }
26
+
27
+ export async function scanNpmGlobal() {
28
+ const command = process.platform === "win32" ? "npm.cmd" : "npm";
29
+ const raw = await commandOutput(command, ["list", "-g", "--depth=0", "--json"], { timeout: 5000, maxBuffer: 2 * 1024 * 1024 });
30
+ return parseNpmGlobal(raw);
31
+ }
32
+
33
+ export async function scanPipx() {
34
+ const raw = await commandOutput("pipx", ["list", "--json"], { timeout: 5000, maxBuffer: 2 * 1024 * 1024 });
35
+ return parsePipx(raw);
36
+ }
37
+
38
+ export async function scanUvTools() {
39
+ const raw = await commandOutput("uv", ["tool", "list"], { timeout: 5000 });
40
+ return parseNameVersionLines(raw);
41
+ }
42
+
43
+ export async function scanBrew() {
44
+ if (process.platform === "win32") return [];
45
+ const raw = await commandOutput("brew", ["list", "--versions"], { timeout: 5000, maxBuffer: 2 * 1024 * 1024 });
46
+ return parseNameVersionLines(raw);
47
+ }
48
+
49
+ export function parseNpmGlobal(raw) {
50
+ try {
51
+ const parsed = JSON.parse(raw);
52
+ return Object.entries(parsed.dependencies || {})
53
+ .slice(0, MAX_ITEMS)
54
+ .map(([name, value]) => ({ name, version: value.version || "unknown" }));
55
+ } catch {
56
+ return [];
57
+ }
58
+ }
59
+
60
+ export function parsePipx(raw) {
61
+ try {
62
+ const parsed = JSON.parse(raw);
63
+ return Object.entries(parsed.venvs || {})
64
+ .slice(0, MAX_ITEMS)
65
+ .map(([name, value]) => ({
66
+ name,
67
+ version: value.metadata?.main_package?.package_version || "unknown"
68
+ }));
69
+ } catch {
70
+ return [];
71
+ }
72
+ }
73
+
74
+ export function parseNameVersionLines(raw) {
75
+ return String(raw || "")
76
+ .split(/\r?\n/)
77
+ .map((line) => line.trim())
78
+ .filter(Boolean)
79
+ .slice(0, MAX_ITEMS)
80
+ .map((line) => {
81
+ const [name, ...rest] = line.split(/\s+/);
82
+ return { name, version: rest.join(" ") || "unknown" };
83
+ });
84
+ }
85
+
86
+ function compact(obj) {
87
+ return Object.fromEntries(Object.entries(obj).filter(([, value]) => Array.isArray(value) && value.length));
88
+ }
package/src/manifest.js CHANGED
@@ -4,8 +4,9 @@ import path from "node:path";
4
4
  import { commandOutput, commandVersion } from "./shell.js";
5
5
  import { exists } from "./fsutil.js";
6
6
  import { observedTrust } from "./trust.js";
7
+ import { scanGlobalInventory } from "./inventory.js";
7
8
 
8
- export async function buildManifest(dir) {
9
+ export async function buildManifest(dir, options = {}) {
9
10
  const now = new Date().toISOString();
10
11
  const manifest = {
11
12
  schemaName: "aienvmp.runtime-sbom",
@@ -13,7 +14,7 @@ export async function buildManifest(dir) {
13
14
  generatedAt: now,
14
15
  generatedBy: {
15
16
  name: "aienvmp",
16
- command: "aienvmp sync"
17
+ command: options.deep ? "aienvmp sync --deep" : "aienvmp sync"
17
18
  },
18
19
  trust: observedTrust(new Date(now)),
19
20
  workspace: {
@@ -25,13 +26,14 @@ export async function buildManifest(dir) {
25
26
  packageManagers: await scanPackageManagers(),
26
27
  containers: await scanContainers(),
27
28
  projectHints: await scanProjectHints(dir),
29
+ inventory: await scanGlobalInventory({ deep: options.deep }),
28
30
  agentFiles: await scanAgentFiles(dir),
29
31
  agentProtocol: {
30
32
  sourceOfTruth: "AIENV.md",
31
33
  preflightCommand: "aienvmp context",
32
34
  handoffCommand: "aienvmp handoff",
33
- intentCommand: "aienvmp intent --actor <agent:id> --action <planned-change>",
34
- recordCommand: "aienvmp record --actor <agent:id> --summary <what-changed>",
35
+ intentCommand: "aienvmp intent --actor agent:id --action planned-change",
36
+ recordCommand: "aienvmp record --actor agent:id --summary what-changed",
35
37
  afterEnvironmentChange: ["aienvmp sync"],
36
38
  trustModel: {
37
39
  agentWritable: ["observed", "planned", "changed", "review", "stale"],
@@ -69,6 +71,13 @@ export async function buildManifest(dir) {
69
71
  docker: "docker --version",
70
72
  compose: "docker compose version or docker-compose --version"
71
73
  },
74
+ globalInventory: {
75
+ mode: "basic by default; deep only when requested",
76
+ npmGlobal: "npm list -g --depth=0 --json",
77
+ pipx: "pipx list --json",
78
+ uvTools: "uv tool list",
79
+ brew: "brew list --versions"
80
+ },
72
81
  projectHints: [
73
82
  ".nvmrc",
74
83
  ".python-version",
package/src/render.js CHANGED
@@ -12,9 +12,10 @@ export function renderAIEnv(manifest, timeline = [], warnings = [], intents = []
12
12
  lines.push("1. Run `aienvmp context`.");
13
13
  lines.push("2. Prefer project-local version files such as `.nvmrc`, `.python-version`, `mise.toml`, and `.tool-versions`.");
14
14
  lines.push("3. Ask the user before changing global environment state.");
15
- lines.push("4. Record planned environment changes with `aienvmp intent --actor <agent:id> --action <planned-change>`.");
15
+ lines.push("4. Record planned environment changes with `aienvmp intent --actor agent:id --action planned-change`.");
16
16
  lines.push("5. After environment changes, run `aienvmp sync`.");
17
- lines.push("6. Record what changed with `aienvmp record --actor <agent:id> --summary <what-changed>`.", "");
17
+ lines.push("6. Record what changed with `aienvmp record --actor agent:id --summary what-changed`.");
18
+ lines.push("7. At handoff, run `aienvmp handoff --record --actor agent:id`.", "");
18
19
  lines.push("## Current Policy", "");
19
20
  lines.push(...policyLines(policy));
20
21
  lines.push("- Enforcement: non-blocking by default; warnings require review but do not lock the machine.");
@@ -28,6 +29,8 @@ export function renderAIEnv(manifest, timeline = [], warnings = [], intents = []
28
29
  pushMap(lines, "Runtimes", manifest.runtimes);
29
30
  pushMap(lines, "Package Managers", manifest.packageManagers);
30
31
  pushMap(lines, "Containers", manifest.containers);
32
+ lines.push("## Global Tool Inventory", "");
33
+ lines.push(...inventoryLines(manifest.inventory), "");
31
34
  lines.push("## Project Requirements And Hints", "");
32
35
  pushMap(lines, "Detected", manifest.projectHints);
33
36
  lines.push("## Drift And Warnings", "");
@@ -74,6 +77,7 @@ Before changing runtimes, package managers, Docker settings, global packages, or
74
77
  4. Record planned environment changes with \`aienvmp intent\`.
75
78
  5. After environment changes, run \`aienvmp sync\`.
76
79
  6. Record what changed with \`aienvmp record\`.
80
+ 7. At handoff, run \`aienvmp handoff --record --actor agent:id\`.
77
81
 
78
82
  \`aienvmp\` does not replace this instruction file. It provides the live env map, lightweight runtime SBOM, intent log, timeline, and dashboard.`;
79
83
  }
@@ -91,6 +95,7 @@ export function renderContext(manifest, timeline = [], warnings = [], intents =
91
95
  `Node: ${manifest.runtimes.node || "not detected"}`,
92
96
  `Python: ${manifest.runtimes.python || manifest.runtimes.python3 || "not detected"}`,
93
97
  `Docker: ${manifest.containers.docker ? "available" : "not detected"}`,
98
+ `Inventory: ${manifest.inventory?.mode || "basic"}${manifest.inventory?.enabled ? " enabled" : " disabled"}`,
94
99
  `Policy Node: ${policy.node || "not set"}`,
95
100
  `Policy Python: ${policy.python || "not set"}`,
96
101
  `Policy Package Manager: ${policy.packageManager || "not set"}`,
@@ -99,9 +104,10 @@ export function renderContext(manifest, timeline = [], warnings = [], intents =
99
104
  "- Ask the user before global runtime, package manager, Docker, or global package changes.",
100
105
  "- Treat policy mismatches as review-required, not as permission to break ongoing operations.",
101
106
  "- Prefer project-local version files and local environments.",
102
- "- Before planned env changes, run `aienvmp intent --actor <agent:id> --action <planned-change>`.",
107
+ "- Before planned env changes, run `aienvmp intent --actor agent:id --action planned-change`.",
103
108
  "- After env changes, run `aienvmp sync`.",
104
- "- Then run `aienvmp record --actor <agent:id> --summary <what-changed>`.",
109
+ "- Then run `aienvmp record --actor agent:id --summary what-changed`.",
110
+ "- Before handing work to another AI, run `aienvmp handoff --record --actor agent:id`.",
105
111
  "",
106
112
  "Warnings:",
107
113
  ...(warnings.length ? warnings.map((w) => `- ${w.message}`) : ["- none"]),
@@ -128,6 +134,7 @@ export function renderHandoff(handoff) {
128
134
  `- Node: ${handoff.safeRuntime.node}`,
129
135
  `- Python: ${handoff.safeRuntime.python}`,
130
136
  `- Docker: ${handoff.safeRuntime.docker}`,
137
+ `- Inventory: ${handoff.inventory?.mode || "basic"}${handoff.inventory?.enabled ? " enabled" : " disabled"}`,
131
138
  "",
132
139
  "Open intents:",
133
140
  ...(handoff.openIntents.length ? handoff.openIntents.map((i) => `- ${i.actor}: ${i.action}${i.target ? ` (${i.target})` : ""}`) : ["- none"]),
@@ -203,6 +210,9 @@ const {manifest,timeline,warnings,intents,policy}=JSON.parse(document.getElement
203
210
  function esc(s){return String(s).replaceAll('&','&amp;').replaceAll('<','&lt;').replaceAll('>','&gt;').replaceAll('"','&quot;')}
204
211
  const entries=o=>Object.entries(o||{});
205
212
  const rows=o=>entries(o).map(([k,v])=>\`<tr><th>\${esc(k)}</th><td><code>\${esc(String(v))}</code></td></tr>\`).join('')||'<tr><td colspan="2">None detected</td></tr>';
213
+ const inventoryGroups=manifest.inventory?.tools||{};
214
+ const inventoryCount=Object.values(inventoryGroups).reduce((sum,items)=>sum+(Array.isArray(items)?items.length:0),0);
215
+ const inventoryHtml=manifest.inventory?.enabled?('<table>'+Object.entries(inventoryGroups).map(([k,v])=>\`<tr><th>\${esc(k)}</th><td><code>\${Array.isArray(v)?v.length:0} tools</code></td></tr>\`).join('')+'</table>'):'<div class="okline">Deep global inventory is off. Run <code>aienvmp sync --deep</code> when an AI needs global tool awareness.</div>';
206
216
  const change=c=>c.type==='changed'?\`\${c.scope} \${c.key}: \${c.before} -> \${c.after}\`:\`\${c.scope} \${c.key}: \${c.type} \${c.after||c.before}\`;
207
217
  const timelineLabel=t=>t.change?change(t.change):(t.summary||t.action||t.type||'recorded change');
208
218
  const agentNames={agents:'Codex',claude:'Claude',gemini:'Gemini'};
@@ -246,6 +256,7 @@ document.getElementById('app').innerHTML=\`
246
256
  \${card('Package Managers',\`<span class="pill">\${entries(manifest.packageManagers).length} found</span>\`,\`<table>\${rows(manifest.packageManagers)}</table>\`)}
247
257
  \${card('Containers',manifest.containers?.docker?'<span class="pill">available</span>':'<span class="pill off">not detected</span>',\`<table>\${rows(manifest.containers)}</table>\`)}
248
258
  \${card('Project Hints',\`<span class="pill">\${entries(manifest.projectHints).length} hints</span>\`,\`<table>\${rows(manifest.projectHints)}</table>\`)}
259
+ \${card('Global Inventory',manifest.inventory?.enabled?'<span class="pill">deep</span>':'<span class="pill off">basic</span>',inventoryHtml)}
249
260
  </div>
250
261
  <aside>
251
262
  \${card('Environment Health',warnings.length?'<span class="pill warn">attention</span>':'<span class="pill">clear</span>',warnHtml)}
@@ -304,6 +315,17 @@ function contextLines(manifest, warnings, intents) {
304
315
  ];
305
316
  }
306
317
 
318
+ function inventoryLines(inventory = {}) {
319
+ if (!inventory.enabled) return ["- Mode: basic", "- Deep global inventory is disabled. Run `aienvmp sync --deep` when needed."];
320
+ const groups = Object.entries(inventory.tools || {});
321
+ if (!groups.length) return ["- Mode: deep", "- No global tools detected by optional scanners."];
322
+ const lines = ["- Mode: deep"];
323
+ for (const [name, items] of groups) {
324
+ lines.push(`- ${name}: ${items.length} tools`);
325
+ }
326
+ return lines;
327
+ }
328
+
307
329
  function policyLines(policy) {
308
330
  const lines = [];
309
331
  if (policy.node) lines.push(`- Node version policy: ${policy.node}`);
package/src/shell.js CHANGED
@@ -15,10 +15,11 @@ export async function commandVersion(command, args = ["--version"]) {
15
15
  }
16
16
  }
17
17
 
18
- export async function commandOutput(command, args = []) {
18
+ export async function commandOutput(command, args = [], options = {}) {
19
19
  try {
20
20
  const { stdout } = await execFileAsync(command, args, {
21
- timeout: 2500,
21
+ timeout: options.timeout || 2500,
22
+ maxBuffer: options.maxBuffer || 1024 * 1024,
22
23
  windowsHide: true
23
24
  });
24
25
  return stdout.trim();