youmd 0.8.7 → 0.8.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/dist/commands/agent.d.ts +4 -0
  2. package/dist/commands/agent.d.ts.map +1 -1
  3. package/dist/commands/agent.js +4 -0
  4. package/dist/commands/agent.js.map +1 -1
  5. package/dist/commands/machine.d.ts.map +1 -1
  6. package/dist/commands/machine.js +145 -2
  7. package/dist/commands/machine.js.map +1 -1
  8. package/dist/commands/project.d.ts.map +1 -1
  9. package/dist/commands/project.js +31 -0
  10. package/dist/commands/project.js.map +1 -1
  11. package/dist/commands/skill.d.ts.map +1 -1
  12. package/dist/commands/skill.js +679 -1
  13. package/dist/commands/skill.js.map +1 -1
  14. package/dist/commands/stack.d.ts.map +1 -1
  15. package/dist/commands/stack.js +97 -1
  16. package/dist/commands/stack.js.map +1 -1
  17. package/dist/commands/sync.d.ts.map +1 -1
  18. package/dist/commands/sync.js +84 -3
  19. package/dist/commands/sync.js.map +1 -1
  20. package/dist/index.js +10 -2
  21. package/dist/index.js.map +1 -1
  22. package/dist/lib/api.d.ts +179 -0
  23. package/dist/lib/api.d.ts.map +1 -1
  24. package/dist/lib/api.js +41 -4
  25. package/dist/lib/api.js.map +1 -1
  26. package/dist/lib/machine-bootstrap-prompt.d.ts.map +1 -1
  27. package/dist/lib/machine-bootstrap-prompt.js +26 -4
  28. package/dist/lib/machine-bootstrap-prompt.js.map +1 -1
  29. package/dist/lib/machine-verify.d.ts +27 -0
  30. package/dist/lib/machine-verify.d.ts.map +1 -1
  31. package/dist/lib/machine-verify.js +84 -0
  32. package/dist/lib/machine-verify.js.map +1 -1
  33. package/dist/lib/realtime-sync.d.ts +26 -0
  34. package/dist/lib/realtime-sync.d.ts.map +1 -1
  35. package/dist/lib/realtime-sync.js +44 -1
  36. package/dist/lib/realtime-sync.js.map +1 -1
  37. package/dist/mcp/registry.d.ts +2 -1
  38. package/dist/mcp/registry.d.ts.map +1 -1
  39. package/dist/mcp/registry.js +63 -1
  40. package/dist/mcp/registry.js.map +1 -1
  41. package/package.json +1 -1
  42. package/scripts/local-agent-stack-inventory.mjs +1150 -0
  43. package/scripts/skillstack-sync/README.md +11 -3
@@ -48,6 +48,7 @@ const path = __importStar(require("path"));
48
48
  const os = __importStar(require("os"));
49
49
  const readline = __importStar(require("readline"));
50
50
  const yaml = __importStar(require("js-yaml"));
51
+ const child_process_1 = require("child_process");
51
52
  const chalk_1 = __importDefault(require("chalk"));
52
53
  const skill_catalog_1 = require("../lib/skill-catalog");
53
54
  const skills_1 = require("../lib/skills");
@@ -110,6 +111,300 @@ function truncateAtWord(text, max) {
110
111
  const lastSpace = truncated.lastIndexOf(" ");
111
112
  return (lastSpace > max * 0.5 ? truncated.slice(0, lastSpace) : truncated) + "...";
112
113
  }
114
+ function sourcePrefix(source) {
115
+ const idx = source.indexOf(":");
116
+ return idx > 0 ? source.slice(0, idx) : "path";
117
+ }
118
+ function parseFlagArgs(args) {
119
+ const positional = [];
120
+ const flags = {};
121
+ for (let i = 0; i < args.length; i += 1) {
122
+ const arg = args[i];
123
+ if (!arg.startsWith("--")) {
124
+ positional.push(arg);
125
+ continue;
126
+ }
127
+ const eq = arg.indexOf("=");
128
+ if (eq > 0) {
129
+ flags[arg.slice(2, eq)] = arg.slice(eq + 1);
130
+ continue;
131
+ }
132
+ const key = arg.slice(2);
133
+ const next = args[i + 1];
134
+ if (next && !next.startsWith("--")) {
135
+ flags[key] = next;
136
+ i += 1;
137
+ }
138
+ else {
139
+ flags[key] = true;
140
+ }
141
+ }
142
+ return { positional, flags };
143
+ }
144
+ function flagString(flags, key) {
145
+ const value = flags[key];
146
+ return typeof value === "string" && value.length > 0 ? value : undefined;
147
+ }
148
+ function resolveInventoryScript() {
149
+ const candidates = [
150
+ process.env.YOUMD_AGENT_STACK_INVENTORY_SCRIPT,
151
+ path.join(os.homedir(), ".agent-shared", "claude-skills", "agent-stack-inventory", "scripts", "local-agent-stack-inventory.mjs"),
152
+ path.join(os.homedir(), ".youmd", "skills", "agent-stack-inventory", "scripts", "local-agent-stack-inventory.mjs"),
153
+ path.resolve(__dirname, "../../scripts/local-agent-stack-inventory.mjs"),
154
+ path.resolve(__dirname, "../../../scripts/local-agent-stack-inventory.mjs"),
155
+ ].filter(Boolean);
156
+ for (const candidate of candidates) {
157
+ if (fs.existsSync(candidate))
158
+ return candidate;
159
+ }
160
+ return null;
161
+ }
162
+ function readInventorySnapshot(file) {
163
+ return JSON.parse(fs.readFileSync(file, "utf-8"));
164
+ }
165
+ function inventoryNames(snapshot) {
166
+ return new Set((snapshot.uniqueSkills || []).map((skill) => skill.name).filter(Boolean));
167
+ }
168
+ function sortInventoryJsonPaths(files) {
169
+ return files.sort((a, b) => {
170
+ const delta = fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs;
171
+ if (delta !== 0)
172
+ return delta;
173
+ return path.basename(b).localeCompare(path.basename(a));
174
+ });
175
+ }
176
+ function findLatestInventoryJson(outDir, sinceMs = 0) {
177
+ if (!fs.existsSync(outDir))
178
+ return null;
179
+ const files = sortInventoryJsonPaths(fs.readdirSync(outDir)
180
+ .filter((file) => /^local-agent-stack-inventory-.*\.json$/.test(file))
181
+ .map((file) => path.join(outDir, file))
182
+ .filter((file) => {
183
+ try {
184
+ return fs.statSync(file).mtimeMs >= sinceMs - 1000;
185
+ }
186
+ catch {
187
+ return false;
188
+ }
189
+ }));
190
+ return files[0] || null;
191
+ }
192
+ function renderInventoryTotals(totals) {
193
+ const rows = [
194
+ ["unique skill names", totals.uniqueSkillNames],
195
+ ["real SKILL.md files", totals.uniqueRealSkillFiles],
196
+ ["direct host exposures", totals.directExposureSkillRecords],
197
+ ["You.md catalog skills", totals.youmdCatalogSkills],
198
+ ["missing from You.md catalog", totals.missingFromYoumdCatalog],
199
+ ["DRY review cases", totals.duplicateNameDifferentRealpaths],
200
+ ["healthy mirror clusters", totals.sameRealpathMirrors],
201
+ ["project/context signals", totals.projectSignals],
202
+ ];
203
+ const maxLabel = Math.max(...rows.map(([label]) => label.length));
204
+ for (const [label, value] of rows) {
205
+ if (typeof value !== "number")
206
+ continue;
207
+ console.log(` ${DIM(label.padEnd(maxLabel + 2))}${chalk_1.default.cyan(value.toLocaleString())}`);
208
+ }
209
+ }
210
+ function renderInventoryPhaseTimings(snapshot) {
211
+ const timings = (snapshot.phaseTimings || [])
212
+ .filter((phase) => typeof phase.durationMs === "number")
213
+ .sort((a, b) => (b.durationMs || 0) - (a.durationMs || 0))
214
+ .slice(0, 4);
215
+ if (timings.length === 0)
216
+ return;
217
+ console.log(` ${DIM("slowest phases")}`);
218
+ for (const phase of timings) {
219
+ const seconds = Math.max(0, Math.round((phase.durationMs || 0) / 1000));
220
+ const label = phase.label || phase.phase || "unknown phase";
221
+ const status = phase.failed ? ACCENT(" failed") : "";
222
+ console.log(` ${DIM(label.padEnd(50))}${chalk_1.default.cyan(`${seconds}s`)}${status}`);
223
+ }
224
+ }
225
+ function runInventoryScript(scriptArgs, spinner) {
226
+ return new Promise((resolve, reject) => {
227
+ const child = (0, child_process_1.spawn)(process.execPath, scriptArgs, {
228
+ stdio: ["ignore", "pipe", "pipe"],
229
+ });
230
+ const stdout = [];
231
+ const stderr = [];
232
+ let progressBuffer = "";
233
+ child.stdout.on("data", (chunk) => {
234
+ stdout.push(chunk);
235
+ });
236
+ child.stderr.on("data", (chunk) => {
237
+ const text = chunk.toString("utf-8");
238
+ stderr.push(chunk);
239
+ progressBuffer += text;
240
+ const lines = progressBuffer.split(/\r?\n/);
241
+ progressBuffer = lines.pop() || "";
242
+ for (const line of lines) {
243
+ const trimmed = line.trim();
244
+ if (!trimmed.startsWith("youmd-inventory-progress "))
245
+ continue;
246
+ try {
247
+ const event = JSON.parse(trimmed.slice("youmd-inventory-progress ".length));
248
+ if (event.label && event.event === "start") {
249
+ spinner.update(event.label);
250
+ }
251
+ else if (event.label && event.event === "failed") {
252
+ spinner.update(`${event.label} failed`);
253
+ }
254
+ }
255
+ catch {
256
+ // Non-JSON stderr is collected for failure output below.
257
+ }
258
+ }
259
+ });
260
+ child.on("error", reject);
261
+ child.on("close", (code) => {
262
+ if (code === 0) {
263
+ resolve(Buffer.concat(stdout).toString("utf-8"));
264
+ return;
265
+ }
266
+ const detail = Buffer.concat(stderr).toString("utf-8").trim();
267
+ reject(new Error(detail || `inventory exited with code ${code}`));
268
+ });
269
+ });
270
+ }
271
+ function compactDate(value) {
272
+ if (typeof value === "string" && value)
273
+ return value.replace(/\.\d{3}Z$/, "Z");
274
+ if (typeof value === "number" && Number.isFinite(value))
275
+ return new Date(value).toISOString().replace(/\.\d{3}Z$/, "Z");
276
+ return "unknown";
277
+ }
278
+ function inventoryTotalsFromSynced(row) {
279
+ return {
280
+ uniqueSkillNames: row.uniqueSkillNames,
281
+ uniqueRealSkillFiles: row.uniqueRealSkillFiles,
282
+ directExposureSkillRecords: row.directExposureSkillRecords,
283
+ canonicalSkillFiles: row.canonicalSkillFiles,
284
+ youmdCatalogSkills: row.youmdCatalogSkills,
285
+ missingFromYoumdCatalog: row.missingFromYoumdCatalog,
286
+ duplicateNameDifferentRealpaths: row.duplicateNameDifferentRealpaths,
287
+ sameRealpathMirrors: row.sameRealpathMirrors,
288
+ projectSignals: row.projectSignals,
289
+ };
290
+ }
291
+ function inventoryDelta(localValue, remoteValue) {
292
+ if (typeof localValue !== "number" || typeof remoteValue !== "number")
293
+ return DIM("?");
294
+ const delta = remoteValue - localValue;
295
+ if (delta === 0)
296
+ return chalk_1.default.green("0");
297
+ return delta > 0 ? chalk_1.default.green(`+${delta}`) : ACCENT(String(delta));
298
+ }
299
+ function renderInventoryDrift(drift) {
300
+ if (!drift?.baseline)
301
+ return;
302
+ console.log("");
303
+ console.log(` ${DIM("drift baseline")} ${chalk_1.default.cyan(drift.baseline.hostName)}${DIM(` · ${compactDate(drift.baseline.generatedAt)} · ${drift.baseline.counts.uniqueSkillNames} skills`)}`);
304
+ console.log(` ${DIM("drift summary ")} ` +
305
+ `${chalk_1.default.cyan(String(drift.summary.machineCount))} machines · ` +
306
+ `${drift.summary.driftCount > 0 ? ACCENT(String(drift.summary.driftCount)) : chalk_1.default.green("0")} drift · ` +
307
+ `${drift.summary.staleCount > 0 ? ACCENT(String(drift.summary.staleCount)) : chalk_1.default.green("0")} stale · ` +
308
+ `${drift.summary.unsafeCount > 0 ? ACCENT(String(drift.summary.unsafeCount)) : chalk_1.default.green("0")} unsafe`);
309
+ const issueRows = drift.machines.filter((machine) => machine.status !== "baseline" && machine.issues.length > 0).slice(0, 4);
310
+ for (const machine of issueRows) {
311
+ console.log(` ${ACCENT(machine.status.padEnd(8))} ${chalk_1.default.cyan(machine.hostName)} ${DIM(machine.issues.slice(0, 2).join("; "))}`);
312
+ }
313
+ }
314
+ function inventoryHostName(snapshot) {
315
+ if (typeof snapshot.host === "string" && snapshot.host.trim())
316
+ return snapshot.host.trim();
317
+ if (snapshot.host && typeof snapshot.host === "object" && snapshot.host.hostname) {
318
+ return snapshot.host.hostname;
319
+ }
320
+ return os.hostname();
321
+ }
322
+ function inventorySnapshotLabel(snapshot, fallback) {
323
+ const host = inventoryHostName(snapshot);
324
+ return host || fallback;
325
+ }
326
+ function safeInventoryCount(value) {
327
+ return Number.isFinite(value) ? Math.max(0, Math.trunc(value)) : 0;
328
+ }
329
+ function inventorySampleNames(rows) {
330
+ if (!Array.isArray(rows))
331
+ return [];
332
+ return rows
333
+ .flatMap((row) => {
334
+ if (typeof row === "string")
335
+ return [row];
336
+ if (row?.name)
337
+ return [row.name];
338
+ return [];
339
+ })
340
+ .map((name) => name.trim())
341
+ .filter(Boolean)
342
+ .slice(0, 24);
343
+ }
344
+ function buildAgentStackInventorySyncPayload(snapshot, jsonPath, htmlPath, syncRepo = true) {
345
+ const totals = snapshot.totals || {};
346
+ return {
347
+ inventorySchemaVersion: snapshot.inventorySchemaVersion || "local-agent-stack-inventory/v1",
348
+ generatedAt: snapshot.generatedAt || new Date().toISOString(),
349
+ hostName: inventoryHostName(snapshot),
350
+ platform: `${os.platform()} ${os.release()}`,
351
+ rootDir: snapshot.roots?.workspace || snapshot.repoRoot || process.cwd(),
352
+ secretValuesExposed: false,
353
+ reportJsonPath: jsonPath,
354
+ reportHtmlPath: htmlPath,
355
+ source: "youmd-cli",
356
+ agentName: "youmd skill inventory",
357
+ totals: {
358
+ uniqueSkillNames: safeInventoryCount(totals.uniqueSkillNames),
359
+ uniqueRealSkillFiles: safeInventoryCount(totals.uniqueRealSkillFiles),
360
+ directExposureSkillRecords: safeInventoryCount(totals.directExposureSkillRecords),
361
+ canonicalSkillFiles: safeInventoryCount(totals.canonicalSkillFiles),
362
+ youmdCatalogSkills: safeInventoryCount(totals.youmdCatalogSkills),
363
+ missingFromYoumdCatalog: safeInventoryCount(totals.missingFromYoumdCatalog),
364
+ duplicateNameDifferentRealpaths: safeInventoryCount(totals.duplicateNameDifferentRealpaths),
365
+ sameRealpathMirrors: safeInventoryCount(totals.sameRealpathMirrors),
366
+ projectSignals: safeInventoryCount(totals.projectSignals),
367
+ },
368
+ ownershipRollup: snapshot.ownershipRollup || {},
369
+ syncPolicyRollup: snapshot.syncPolicyRollup || {},
370
+ provenanceRollup: snapshot.provenanceRollup || {},
371
+ missingCatalogSamples: inventorySampleNames(snapshot.missingFromCatalog),
372
+ duplicateNameSamples: inventorySampleNames(snapshot.dryAudit?.duplicateNameDifferentRealpaths),
373
+ mirrorSamples: inventorySampleNames(snapshot.dryAudit?.sameRealpathMirrors),
374
+ syncRepo,
375
+ };
376
+ }
377
+ function buildSkillSyncActivityMetadata(synced, errors) {
378
+ const catalog = (0, skill_catalog_1.readSkillCatalog)();
379
+ const entries = [];
380
+ for (const name of synced) {
381
+ const entry = (0, skill_catalog_1.findSkill)(catalog, name);
382
+ if (entry)
383
+ entries.push(entry);
384
+ }
385
+ const sharedSkills = entries
386
+ .filter((entry) => entry.source.startsWith("shared:"))
387
+ .map((entry) => entry.name)
388
+ .sort();
389
+ const sourcePrefixes = {};
390
+ const scopes = {};
391
+ for (const entry of entries) {
392
+ const prefix = sourcePrefix(entry.source);
393
+ sourcePrefixes[prefix] = (sourcePrefixes[prefix] ?? 0) + 1;
394
+ scopes[entry.scope] = (scopes[entry.scope] ?? 0) + 1;
395
+ }
396
+ return {
397
+ syncedCount: synced.length,
398
+ errorCount: errors.length,
399
+ skills: synced.slice(0, 20),
400
+ sharedSkillCount: sharedSkills.length,
401
+ sharedSkills: sharedSkills.slice(0, 20),
402
+ sourcePrefixes,
403
+ scopes,
404
+ canonicalSharedRoot: "~/.agent-shared/claude-skills",
405
+ secretValuesExposed: false,
406
+ };
407
+ }
113
408
  // ─── Subcommands ──────────────────────────────────────────────────────
114
409
  const RECOMMENDED_SKILLS = new Set([
115
410
  "youstack-start",
@@ -428,6 +723,37 @@ async function syncSkillsCmd() {
428
723
  console.log(` ${ACCENT("\u2717")} ${DIM(err)}`);
429
724
  }
430
725
  }
726
+ if ((0, config_1.isAuthenticated)()) {
727
+ try {
728
+ const metadata = buildSkillSyncActivityMetadata(result.synced, result.errors);
729
+ const sharedSkillCount = metadata.sharedSkillCount;
730
+ await (0, api_1.recordBrainActivity)({
731
+ activityId: `skill-sync:${os.hostname()}`,
732
+ source: "skill-sync",
733
+ channel: "skills",
734
+ kind: result.errors.length > 0 ? "warn" : sharedSkillCount > 0 ? "shared-skill-sync" : "synced",
735
+ status: result.errors.length > 0 ? "warn" : "ok",
736
+ title: sharedSkillCount > 0
737
+ ? `${result.synced.length} skills synced · ${sharedSkillCount} shared`
738
+ : `${result.synced.length} skills synced`,
739
+ detail: result.errors.length > 0
740
+ ? `${result.errors.length} sync warning${result.errors.length === 1 ? "" : "s"}`
741
+ : sharedSkillCount > 0
742
+ ? `shared skills propagated from ~/.agent-shared: ${metadata.sharedSkills.slice(0, 5).join(", ")}`
743
+ : "installed skills re-rendered against the latest identity bundle",
744
+ sourceHost: os.hostname(),
745
+ sourceAgent: "youmd CLI",
746
+ sourceRuntime: process.version,
747
+ entityType: sharedSkillCount > 0 ? "sharedSkillSync" : "skillSync",
748
+ entityId: os.hostname(),
749
+ metadata,
750
+ });
751
+ }
752
+ catch (err) {
753
+ if (process.env.DEBUG)
754
+ console.error(`[skill sync] brain activity failed: ${err}`);
755
+ }
756
+ }
431
757
  console.log("");
432
758
  }
433
759
  function addSkillCmd(args) {
@@ -708,6 +1034,7 @@ async function improveCmd() {
708
1034
  const notInstalled = catalog.skills.filter((s) => !s.installed);
709
1035
  // ─── Live outcome insights (L10) ──────────────────────────────────
710
1036
  let liveInsights = null;
1037
+ const lowPerformers = [];
711
1038
  if ((0, config_1.isAuthenticated)()) {
712
1039
  const insightSpinner = new render_1.BrailleSpinner("fetching outcome telemetry");
713
1040
  insightSpinner.start();
@@ -751,7 +1078,6 @@ async function improveCmd() {
751
1078
  `last used`;
752
1079
  console.log(DIM(headerStr));
753
1080
  console.log(DIM(" " + "-".repeat(headerStr.length - 2)));
754
- const lowPerformers = [];
755
1081
  for (const s of liveInsights) {
756
1082
  const ratePct = Math.round(s.successRate * 100);
757
1083
  const rateStr = `${ratePct}%`.padStart(4);
@@ -907,6 +1233,349 @@ async function improveCmd() {
907
1233
  }
908
1234
  console.log("");
909
1235
  }
1236
+ if ((0, config_1.isAuthenticated)()) {
1237
+ try {
1238
+ await (0, api_1.recordBrainActivity)({
1239
+ activityId: `skill-improve:${os.hostname()}`,
1240
+ source: "skill-improve",
1241
+ channel: "skills",
1242
+ kind: proposals.length > 0 || lowPerformers.length > 0 ? "proposed" : "checked",
1243
+ status: proposals.length > 0 || lowPerformers.length > 0 ? "warn" : "ok",
1244
+ title: proposals.length > 0 || lowPerformers.length > 0
1245
+ ? `skill improvement analysis found ${proposals.length + lowPerformers.length} signal${proposals.length + lowPerformers.length === 1 ? "" : "s"}`
1246
+ : "skill improvement analysis clean",
1247
+ detail: proposals.length > 0
1248
+ ? proposals.slice(0, 3).join(" · ")
1249
+ : lowPerformers.length > 0
1250
+ ? `low performers: ${lowPerformers.map((s) => s.skill).slice(0, 5).join(", ")}`
1251
+ : "no immediate skill improvements suggested",
1252
+ entityType: "skillImprovementAnalysis",
1253
+ entityId: os.hostname(),
1254
+ sourceHost: os.hostname(),
1255
+ sourceAgent: "youmd CLI",
1256
+ sourceRuntime: process.version,
1257
+ metadata: {
1258
+ installedCount: installed.length,
1259
+ notInstalledCount: notInstalled.length,
1260
+ localUseCount: totalUses,
1261
+ identityFieldCount: allFields.size,
1262
+ missingIdentityFields: missing.slice(0, 20),
1263
+ proposalCount: proposals.length,
1264
+ proposals: proposals.slice(0, 12),
1265
+ lowPerformerCount: lowPerformers.length,
1266
+ lowPerformers: lowPerformers.slice(0, 12).map((skill) => ({
1267
+ skill: skill.skill,
1268
+ uses: skill.uses,
1269
+ successRate: skill.successRate,
1270
+ failure: skill.failure,
1271
+ partial: skill.partial,
1272
+ })),
1273
+ liveInsightCount: liveInsights?.length ?? 0,
1274
+ fleetLineCount: fleetLines.length,
1275
+ secretValuesExposed: false,
1276
+ },
1277
+ });
1278
+ }
1279
+ catch (err) {
1280
+ if (process.env.DEBUG)
1281
+ console.error(`[skill improve] brain activity failed: ${err}`);
1282
+ }
1283
+ }
1284
+ }
1285
+ function inventoryDiffCmd(args) {
1286
+ const { positional, flags } = parseFlagArgs(args);
1287
+ const leftPath = flagString(flags, "left") || positional[0];
1288
+ const rightPath = flagString(flags, "right") || positional[1];
1289
+ if (!leftPath || !rightPath) {
1290
+ console.log("");
1291
+ console.log(chalk_1.default.yellow(" usage: youmd skill inventory diff --left mac-mini.json --right laptop.json"));
1292
+ console.log(DIM(" or: youmd skill inventory diff mac-mini.json laptop.json"));
1293
+ console.log("");
1294
+ return;
1295
+ }
1296
+ const left = readInventorySnapshot(path.resolve(leftPath));
1297
+ const right = readInventorySnapshot(path.resolve(rightPath));
1298
+ const leftNames = inventoryNames(left);
1299
+ const rightNames = inventoryNames(right);
1300
+ const onlyLeft = [...leftNames].filter((name) => !rightNames.has(name)).sort();
1301
+ const onlyRight = [...rightNames].filter((name) => !leftNames.has(name)).sort();
1302
+ console.log("");
1303
+ console.log(" " + chalk_1.default.bold("skill inventory diff"));
1304
+ console.log("");
1305
+ console.log(` ${DIM("left ")}${chalk_1.default.cyan(inventorySnapshotLabel(left, path.basename(leftPath)))}${DIM(left.generatedAt ? ` · ${left.generatedAt}` : "")}`);
1306
+ console.log(` ${DIM("right")}${chalk_1.default.cyan(" " + inventorySnapshotLabel(right, path.basename(rightPath)))}${DIM(right.generatedAt ? ` · ${right.generatedAt}` : "")}`);
1307
+ console.log("");
1308
+ const totalRows = [
1309
+ ["unique skill names", left.totals?.uniqueSkillNames, right.totals?.uniqueSkillNames],
1310
+ ["real SKILL.md files", left.totals?.uniqueRealSkillFiles, right.totals?.uniqueRealSkillFiles],
1311
+ ["direct host exposures", left.totals?.directExposureSkillRecords, right.totals?.directExposureSkillRecords],
1312
+ ["You.md catalog skills", left.totals?.youmdCatalogSkills, right.totals?.youmdCatalogSkills],
1313
+ ["missing from catalog", left.totals?.missingFromYoumdCatalog, right.totals?.missingFromYoumdCatalog],
1314
+ ["DRY review cases", left.totals?.duplicateNameDifferentRealpaths, right.totals?.duplicateNameDifferentRealpaths],
1315
+ ["mirror clusters", left.totals?.sameRealpathMirrors, right.totals?.sameRealpathMirrors],
1316
+ ];
1317
+ const maxLabel = Math.max(...totalRows.map(([label]) => label.length));
1318
+ for (const [label, leftValue, rightValue] of totalRows) {
1319
+ const delta = typeof leftValue === "number" && typeof rightValue === "number"
1320
+ ? rightValue - leftValue
1321
+ : undefined;
1322
+ const deltaText = typeof delta === "number"
1323
+ ? delta === 0
1324
+ ? DIM(" 0")
1325
+ : delta > 0
1326
+ ? chalk_1.default.green(` +${delta}`)
1327
+ : ACCENT(` ${delta}`)
1328
+ : "";
1329
+ console.log(` ${DIM(label.padEnd(maxLabel + 2))}` +
1330
+ `${String(leftValue ?? "?").padStart(6)} ${DIM("->")} ${String(rightValue ?? "?").padStart(6)}${deltaText}`);
1331
+ }
1332
+ console.log("");
1333
+ console.log(` ${DIM("skills only in left: ")}${onlyLeft.length}`);
1334
+ for (const name of onlyLeft.slice(0, 20))
1335
+ console.log(` ${ACCENT("-")} ${name}`);
1336
+ if (onlyLeft.length > 20)
1337
+ console.log(DIM(` ... ${onlyLeft.length - 20} more`));
1338
+ console.log("");
1339
+ console.log(` ${DIM("skills only in right: ")}${onlyRight.length}`);
1340
+ for (const name of onlyRight.slice(0, 20))
1341
+ console.log(` ${chalk_1.default.green("+")} ${name}`);
1342
+ if (onlyRight.length > 20)
1343
+ console.log(DIM(` ... ${onlyRight.length - 20} more`));
1344
+ console.log("");
1345
+ }
1346
+ async function inventoryStatusCmd(args) {
1347
+ const { flags } = parseFlagArgs(args);
1348
+ const outDir = path.resolve(flagString(flags, "out-dir") || path.join(os.homedir(), ".youmd", "agent-stack-inventory"));
1349
+ const limitRaw = flagString(flags, "limit");
1350
+ const limit = limitRaw && Number.isFinite(Number(limitRaw))
1351
+ ? Math.max(1, Math.min(50, Math.trunc(Number(limitRaw))))
1352
+ : 12;
1353
+ const jsonOutput = flags.json === true;
1354
+ const latestPath = findLatestInventoryJson(outDir);
1355
+ const local = latestPath ? readInventorySnapshot(latestPath) : null;
1356
+ const localTotals = local?.totals || {};
1357
+ if (!(0, config_1.isAuthenticated)()) {
1358
+ if (jsonOutput) {
1359
+ console.log(JSON.stringify({
1360
+ success: false,
1361
+ error: "not_authenticated",
1362
+ repair: "youmd login",
1363
+ secretValuesExposed: false,
1364
+ }, null, 2));
1365
+ return;
1366
+ }
1367
+ console.log("");
1368
+ console.log(chalk_1.default.yellow(" not authenticated. run: ") + chalk_1.default.cyan("youmd login"));
1369
+ console.log(DIM(" then: ") + chalk_1.default.cyan("youmd skill inventory status"));
1370
+ console.log("");
1371
+ return;
1372
+ }
1373
+ const [res, driftRes] = await Promise.all([
1374
+ (0, api_1.getAgentStackInventories)({ limit }),
1375
+ (0, api_1.getAgentStackInventoryDrift)({ limit }),
1376
+ ]);
1377
+ if (!res.ok) {
1378
+ const message = (0, api_1.apiErrorMessage)(res.data) || `HTTP ${res.status}`;
1379
+ if (jsonOutput) {
1380
+ console.log(JSON.stringify({
1381
+ success: false,
1382
+ error: message,
1383
+ secretValuesExposed: false,
1384
+ }, null, 2));
1385
+ return;
1386
+ }
1387
+ console.log("");
1388
+ console.log(chalk_1.default.yellow(` inventory status unavailable: ${message}`));
1389
+ console.log("");
1390
+ return;
1391
+ }
1392
+ const inventories = res.data?.inventories ?? [];
1393
+ const drift = driftRes.ok ? driftRes.data : null;
1394
+ if (jsonOutput) {
1395
+ console.log(JSON.stringify({
1396
+ success: true,
1397
+ local: local
1398
+ ? {
1399
+ path: latestPath,
1400
+ hostName: inventoryHostName(local),
1401
+ generatedAt: local.generatedAt,
1402
+ rootDir: local.roots?.workspace || local.repoRoot,
1403
+ totals: localTotals,
1404
+ }
1405
+ : null,
1406
+ inventories,
1407
+ drift,
1408
+ secretValuesExposed: false,
1409
+ }, null, 2));
1410
+ return;
1411
+ }
1412
+ console.log("");
1413
+ console.log(" " + chalk_1.default.bold("skill inventory status"));
1414
+ console.log("");
1415
+ if (local && latestPath) {
1416
+ console.log(` ${DIM("local latest")} ${chalk_1.default.cyan(inventoryHostName(local))}${DIM(local.generatedAt ? ` · ${compactDate(local.generatedAt)}` : "")}`);
1417
+ console.log(` ${DIM("local json ")} ${chalk_1.default.cyan(latestPath)}`);
1418
+ if (localTotals) {
1419
+ console.log(` ${DIM("local counts")} ` +
1420
+ `${chalk_1.default.cyan(String(localTotals.uniqueSkillNames ?? "?"))} skills · ` +
1421
+ `${chalk_1.default.cyan(String(localTotals.uniqueRealSkillFiles ?? "?"))} files · ` +
1422
+ `${ACCENT(String(localTotals.missingFromYoumdCatalog ?? 0))} catalog gaps · ` +
1423
+ `${ACCENT(String(localTotals.duplicateNameDifferentRealpaths ?? 0))} DRY reviews`);
1424
+ }
1425
+ }
1426
+ else {
1427
+ console.log(chalk_1.default.yellow(" local latest: missing"));
1428
+ console.log(DIM(" repair: ") + chalk_1.default.cyan(`youmd skill inventory --out-dir ${outDir} --sync`));
1429
+ }
1430
+ console.log("");
1431
+ if (inventories.length === 0) {
1432
+ console.log(chalk_1.default.yellow(" no synced machine inventories found yet."));
1433
+ }
1434
+ else {
1435
+ const rows = inventories.map((row) => {
1436
+ const totals = inventoryTotalsFromSynced(row);
1437
+ const localHost = local ? inventoryHostName(local) : "";
1438
+ const sameHost = localHost && row.hostName === localHost;
1439
+ return {
1440
+ row,
1441
+ totals,
1442
+ marker: sameHost ? "*" : " ",
1443
+ };
1444
+ });
1445
+ console.log(` ${DIM("synced machines")} ${chalk_1.default.cyan(String(inventories.length))}${DIM(` · limit ${limit}`)}`);
1446
+ console.log("");
1447
+ console.log(` ${DIM(" host".padEnd(26))}` +
1448
+ `${DIM("updated".padEnd(22))}` +
1449
+ `${DIM("skills".padStart(8))}` +
1450
+ `${DIM(" files".padStart(8))}` +
1451
+ `${DIM(" gaps".padStart(8))}` +
1452
+ `${DIM(" dry".padStart(7))}` +
1453
+ `${DIM(" Δskills".padStart(10))}`);
1454
+ for (const { row, totals, marker } of rows) {
1455
+ const host = `${marker} ${row.hostName}`.slice(0, 25).padEnd(26);
1456
+ const updated = compactDate(row.updatedAt).slice(0, 21).padEnd(22);
1457
+ console.log(` ${chalk_1.default.cyan(host)}` +
1458
+ `${DIM(updated)}` +
1459
+ `${String(totals.uniqueSkillNames ?? "?").padStart(8)}` +
1460
+ `${String(totals.uniqueRealSkillFiles ?? "?").padStart(8)}` +
1461
+ `${String(totals.missingFromYoumdCatalog ?? "?").padStart(8)}` +
1462
+ `${String(totals.duplicateNameDifferentRealpaths ?? "?").padStart(7)}` +
1463
+ `${inventoryDelta(localTotals.uniqueSkillNames, totals.uniqueSkillNames).padStart(10)}`);
1464
+ if (row.rootDir)
1465
+ console.log(` ${DIM(" root")} ${DIM(row.rootDir)}`);
1466
+ }
1467
+ console.log("");
1468
+ console.log(DIM(" * matches this machine's local latest host name"));
1469
+ }
1470
+ renderInventoryDrift(drift);
1471
+ console.log("");
1472
+ console.log(DIM(" refresh: ") + chalk_1.default.cyan(`youmd skill inventory --out-dir ${outDir} --sync`));
1473
+ console.log(DIM(" verify: ") + chalk_1.default.cyan("youmd machine verify --write-report --sync-report"));
1474
+ console.log(DIM(" exact diff needs two local JSON files: ") + chalk_1.default.cyan("youmd skill inventory diff macbook.json mac-mini.json"));
1475
+ console.log("");
1476
+ }
1477
+ async function inventoryCmd(args) {
1478
+ if (args[0] === "diff") {
1479
+ inventoryDiffCmd(args.slice(1));
1480
+ return;
1481
+ }
1482
+ if (args[0] === "status" || args[0] === "remote" || args[0] === "machines") {
1483
+ await inventoryStatusCmd(args.slice(1));
1484
+ return;
1485
+ }
1486
+ const { flags } = parseFlagArgs(args);
1487
+ const script = resolveInventoryScript();
1488
+ if (!script) {
1489
+ console.log("");
1490
+ console.log(chalk_1.default.yellow(" agent-stack-inventory script not found on this machine."));
1491
+ console.log(DIM(" expected one of:"));
1492
+ console.log(DIM(" ~/.agent-shared/claude-skills/agent-stack-inventory/scripts/local-agent-stack-inventory.mjs"));
1493
+ console.log(DIM(" ~/.youmd/skills/agent-stack-inventory/scripts/local-agent-stack-inventory.mjs"));
1494
+ console.log("");
1495
+ console.log(DIM(" run ") + chalk_1.default.cyan("youmd skill install agent-stack-inventory") + DIM(" or sync your shared skills, then retry."));
1496
+ console.log("");
1497
+ return;
1498
+ }
1499
+ const outDir = path.resolve(flagString(flags, "out-dir") || process.cwd());
1500
+ const workspace = flagString(flags, "workspace");
1501
+ const date = flagString(flags, "date");
1502
+ const shouldSync = flags.sync === true;
1503
+ const shouldSyncRepo = flags["no-repo-sync"] !== true;
1504
+ const startedAt = Date.now();
1505
+ const scriptArgs = [script, "--out-dir", outDir, "--progress"];
1506
+ if (workspace)
1507
+ scriptArgs.push("--workspace", path.resolve(workspace));
1508
+ if (date)
1509
+ scriptArgs.push("--date", date);
1510
+ console.log("");
1511
+ const spinner = new render_1.BrailleSpinner("mapping the local skill mesh");
1512
+ spinner.start();
1513
+ let output = "";
1514
+ try {
1515
+ output = await runInventoryScript(scriptArgs, spinner);
1516
+ }
1517
+ catch (err) {
1518
+ spinner.fail("inventory failed");
1519
+ const message = err instanceof Error ? err.message : String(err);
1520
+ console.log(DIM(` ${message}`));
1521
+ console.log("");
1522
+ return;
1523
+ }
1524
+ const latest = findLatestInventoryJson(outDir, startedAt);
1525
+ const parsed = latest ? readInventorySnapshot(latest) : null;
1526
+ spinner.stop("inventory generated");
1527
+ console.log("");
1528
+ if (parsed?.totals) {
1529
+ renderInventoryTotals(parsed.totals);
1530
+ renderInventoryPhaseTimings(parsed);
1531
+ console.log("");
1532
+ }
1533
+ let htmlPath = latest ? latest.replace(/\.json$/, ".html") : undefined;
1534
+ let jsonPath = latest || undefined;
1535
+ try {
1536
+ const scriptResult = JSON.parse(output);
1537
+ htmlPath = scriptResult.html || htmlPath;
1538
+ jsonPath = scriptResult.json || jsonPath;
1539
+ if (scriptResult.html)
1540
+ console.log(` ${DIM("html")} ${chalk_1.default.cyan(scriptResult.html)}`);
1541
+ if (scriptResult.json)
1542
+ console.log(` ${DIM("json")} ${chalk_1.default.cyan(scriptResult.json)}`);
1543
+ }
1544
+ catch {
1545
+ if (latest) {
1546
+ console.log(` ${DIM("json")} ${chalk_1.default.cyan(latest)}`);
1547
+ console.log(` ${DIM("html")} ${chalk_1.default.cyan(latest.replace(/\.json$/, ".html"))}`);
1548
+ }
1549
+ }
1550
+ if (shouldSync && parsed) {
1551
+ if (!(0, config_1.isAuthenticated)()) {
1552
+ console.log(DIM(" sync skipped: run `youmd login` or set YOUMD_API_KEY to persist this machine inventory."));
1553
+ }
1554
+ else {
1555
+ try {
1556
+ const res = await (0, api_1.syncAgentStackInventory)(buildAgentStackInventorySyncPayload(parsed, jsonPath, htmlPath, shouldSyncRepo));
1557
+ if (res.ok && res.data?.success) {
1558
+ const action = res.data.created ? "created" : "updated";
1559
+ console.log(` ${DIM("sync")} ${chalk_1.default.green(action)} You.md agent stack inventory`);
1560
+ if (res.data.repoSync?.attempted) {
1561
+ const label = res.data.repoSync.ok ? chalk_1.default.green("synced repo snapshot") : ACCENT("repo snapshot skipped");
1562
+ console.log(` ${DIM("repo")} ${label}${res.data.repoSync.error ? DIM(`: ${res.data.repoSync.error}`) : ""}`);
1563
+ }
1564
+ else if (!shouldSyncRepo) {
1565
+ console.log(` ${DIM("repo")} ${DIM("snapshot sync disabled by --no-repo-sync")}`);
1566
+ }
1567
+ }
1568
+ else {
1569
+ console.log(` ${DIM("sync skipped:")} ${(0, api_1.apiErrorMessage)(res.data) || `HTTP ${res.status}`}`);
1570
+ }
1571
+ }
1572
+ catch (err) {
1573
+ const message = err instanceof Error ? err.message : String(err);
1574
+ console.log(` ${DIM("sync skipped:")} ${message}`);
1575
+ }
1576
+ }
1577
+ }
1578
+ console.log("");
910
1579
  }
911
1580
  function searchCmd(args) {
912
1581
  const query = args.join(" ");
@@ -1216,6 +1885,10 @@ async function skillCommand(subcommand, ...args) {
1216
1885
  case "improve":
1217
1886
  await improveCmd();
1218
1887
  break;
1888
+ case "inventory":
1889
+ case "audit":
1890
+ await inventoryCmd(args);
1891
+ break;
1219
1892
  case "metrics":
1220
1893
  case "stats":
1221
1894
  metricsCmd();
@@ -1283,6 +1956,11 @@ async function skillCommand(subcommand, ...args) {
1283
1956
  console.log(` ${chalk_1.default.cyan("link <agent>".padEnd(28))} ${DIM("link to claude | cursor | codex")}`);
1284
1957
  console.log(` ${chalk_1.default.cyan("init-project [--mode auto|additive|zero-touch|scaffold]".padEnd(28))} ${DIM("bootstrap AGENTS/CLAUDE + project-context/ + .you/ + links")}`);
1285
1958
  console.log(` ${chalk_1.default.cyan("improve".padEnd(28))} ${DIM("review metrics, find gaps, propose changes")}`);
1959
+ console.log(` ${chalk_1.default.cyan("inventory [--out-dir dir]".padEnd(28))} ${DIM("map local/global skills, mirrors, catalog gaps, and DRY risks")}`);
1960
+ console.log(` ${chalk_1.default.cyan("inventory --sync".padEnd(28))} ${DIM("persist safe inventory metadata to Convex + repo snapshot")}`);
1961
+ console.log(` ${chalk_1.default.cyan("inventory --no-repo-sync".padEnd(28))} ${DIM("skip GitHub snapshot push when syncing inventory")}`);
1962
+ console.log(` ${chalk_1.default.cyan("inventory status".padEnd(28))} ${DIM("show synced machine inventory summaries and count drift")}`);
1963
+ console.log(` ${chalk_1.default.cyan("inventory diff <a> <b>".padEnd(28))} ${DIM("compare two machine inventory JSON snapshots")}`);
1286
1964
  console.log(` ${chalk_1.default.cyan("metrics".padEnd(28))} ${DIM("usage stats and effectiveness scores")}`);
1287
1965
  console.log(` ${chalk_1.default.cyan("search <query>".padEnd(28))} ${DIM("search skills by name or description")}`);
1288
1966
  console.log(` ${chalk_1.default.cyan("browse".padEnd(28))} ${DIM("browse the public skill registry")}`);