@dadado/agent-kit-cli 5.3.0 → 5.4.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.
@@ -0,0 +1,25 @@
1
+ # Mission Control runtime
2
+
3
+ The dashboard the CLI serves. Plain ES modules, no build step: `serve.mjs` reads this directory
4
+ directly, and `packages/cli/dashboard/` is a generated copy (gitignored, produced by
5
+ `scripts/sync-cli-dashboard.mjs` at build/prepack time so the npm tarball can ship it).
6
+
7
+ | Path | Role |
8
+ | --- | --- |
9
+ | `serve.mjs` | HTTP server, auth gate, SSE |
10
+ | `start.mjs` / `start-broadcast.mjs` | loopback and LAN entry points, per-workspace port allocation |
11
+ | `dashboard-data.mjs` | snapshot builder for the panel |
12
+ | `dashboard.html`, `open.html` | panel and share shell |
13
+ | `lib/*.mjs` | guards, semantic model, live refresh, browser open, terminal snapshot |
14
+ | `lib/guards.d.mts` | hand-written types consumed by the CLI package (parity is pinned by a test) |
15
+
16
+ ## Where the tests live
17
+
18
+ Tests for these modules are in `packages/cli/src/dashboard/*.test.ts`, not next to the source. The
19
+ CLI package owns the only test runner in the workspace (vitest), and those suites import the `.mjs`
20
+ files directly (`../../../../dashboard/lib/...`) so there is one implementation under test rather
21
+ than a copy. Lint and format are covered from the repository root (`pnpm lint` checks `dashboard/**`
22
+ before it fans out to the workspace packages).
23
+
24
+ `dashboard.html` is outside Biome's scope; CSS/HTML-only changes are covered by
25
+ `packages/cli/src/dashboard/plugin-ux-validation.test.ts` instead.
@@ -47,12 +47,12 @@ export function escapePerlDoubleQuoted(value) {
47
47
 
48
48
  /**
49
49
  * Resolve the repository root Mission Control should snapshot.
50
- * @param {NodeJS.ProcessEnv | Record<string, string | undefined>} [env]
50
+ * @param {NodeJS.ProcessEnv | Record<string, string | undefined> | undefined} env - `undefined` falls back to `process.env`
51
51
  * @param {string} kitRoot - absolute path to the kit tree (parent of `dashboard/`)
52
52
  * @returns {string} absolute snapshot root
53
53
  */
54
- export function resolveSnapshotRepoRoot(env = process.env, kitRoot) {
55
- const raw = env?.[REPO_ROOT_ENV];
54
+ export function resolveSnapshotRepoRoot(env, kitRoot) {
55
+ const raw = (env ?? process.env)?.[REPO_ROOT_ENV];
56
56
  if (typeof raw === "string" && raw.trim()) {
57
57
  return resolve(raw.trim());
58
58
  }
@@ -400,7 +400,7 @@ export function tokensMatch(a, b) {
400
400
  /**
401
401
  * Resolve bind + token gate for Mission Control serve.
402
402
  * Non-loopback bind requires a valid MISSION_CONTROL_TOKEN (no warn-only 0.0.0.0).
403
- * @param {NodeJS.ProcessEnv | Record<string, string | undefined>} [env]
403
+ * @param {NodeJS.ProcessEnv | Record<string, string | undefined> | undefined} env - `undefined` falls back to `process.env`
404
404
  * @returns
405
405
  * | { ok: true, host: string, tokenRequired: boolean, token: string | null, broadcast: boolean }
406
406
  * | { ok: false, error: string }
@@ -11,8 +11,7 @@
11
11
  * ## Follow-up plan (also "Followup plan")
12
12
  * ## Residuals plan
13
13
  */
14
- export const TRIAGE_HEADING_RE =
15
- /^#{2,6}\s+(?:Triage note|Follow-?up plan|Residuals plan)\b/im;
14
+ export const TRIAGE_HEADING_RE = /^#{2,6}\s+(?:Triage note|Follow-?up plan|Residuals plan)\b/im;
16
15
 
17
16
  /** True when markdown carries a durable triage heading. */
18
17
  export function hasTriageHeading(text) {
package/dist/index.js CHANGED
@@ -214,7 +214,9 @@ var KNOWN_SHIPPED_OVERLAY_HASHES = /* @__PURE__ */ new Set([
214
214
  "73cd50bac290b84df6245d3647e686dee8ec3b0434736d08fdba6e30d342ef0a",
215
215
  "61b635ea8a08171062bd329a4615d7426eb787eb796271aa882f55f39db56810",
216
216
  "86afbea8f64de68a79ad5e374f3132bdbe2582b94321fb3d438314838e36c776",
217
- "65cc1c0293b145e48ed73ad0ca9ab33cbba5ed834a5bfb31f195ed30c2f143df"
217
+ "65cc1c0293b145e48ed73ad0ca9ab33cbba5ed834a5bfb31f195ed30c2f143df",
218
+ "f04fcfe31354d1b09aeb256a17e4aab91c98ea48a5cff25e4a0281af3cfb289f",
219
+ "34e559ad9036d93d9cbc96d394bc2bbeb10ce50ead00d58a914158ae19daa76c"
218
220
  ]);
219
221
 
220
222
  // src/lifecycle/paths.ts
@@ -1088,17 +1090,38 @@ async function findPack(rootDir, packId) {
1088
1090
  }
1089
1091
 
1090
1092
  // src/registry/install.ts
1091
- import { readFile as readFile5 } from "fs/promises";
1093
+ import { readFile as readFile5, readdir as readdir3 } from "fs/promises";
1092
1094
  import path8 from "path";
1095
+ function skillTargetDir(skillPath, skillId) {
1096
+ const category = skillPath.includes("/core/") ? "core" : "community";
1097
+ return path8.posix.join(".cursor", "skills", category, skillId);
1098
+ }
1099
+ async function skillFileTargets(registryRoot, skillPath, skillId) {
1100
+ const targetDir = skillTargetDir(skillPath, skillId);
1101
+ const pair = (rel) => ({
1102
+ sourceRel: path8.posix.join(skillPath, rel),
1103
+ targetRel: path8.posix.join(targetDir, rel)
1104
+ });
1105
+ let companions = [];
1106
+ try {
1107
+ const dirAbs = resolveContained(registryRoot, skillPath);
1108
+ const entries = await readdir3(dirAbs, { withFileTypes: true, recursive: true });
1109
+ companions = entries.filter((entry) => entry.isFile()).map((entry) => {
1110
+ const parent = path8.relative(dirAbs, entry.parentPath ?? dirAbs);
1111
+ return path8.join(parent, entry.name).split(path8.sep).join("/");
1112
+ }).filter((rel) => rel !== "SKILL.md" && !rel.split("/").some((seg) => seg.startsWith("."))).sort();
1113
+ } catch {
1114
+ companions = [];
1115
+ }
1116
+ return [pair("SKILL.md"), ...companions.map(pair)];
1117
+ }
1093
1118
  function targetForMember(member) {
1094
1119
  switch (member.kind) {
1095
- case "skill": {
1096
- const category = member.source.includes("/core/") ? "core" : "community";
1120
+ case "skill":
1097
1121
  return {
1098
1122
  sourceRel: path8.posix.join(member.source, "SKILL.md"),
1099
- targetRel: path8.posix.join(".cursor", "skills", category, member.id, "SKILL.md")
1123
+ targetRel: path8.posix.join(skillTargetDir(member.source, member.id), "SKILL.md")
1100
1124
  };
1101
- }
1102
1125
  case "rule":
1103
1126
  return {
1104
1127
  sourceRel: member.source,
@@ -1139,19 +1162,22 @@ function targetForMember(member) {
1139
1162
  }
1140
1163
  async function installSkill(registryRoot, projectRoot, skill, options = {}) {
1141
1164
  const stats = emptyStats();
1142
- const category = skill.path.includes("/core/") ? "core" : "community";
1143
- const sourceRel = path8.posix.join(skill.path, "SKILL.md");
1144
- const targetRel = path8.posix.join(".cursor", "skills", category, skill.id, "SKILL.md");
1145
1165
  const managedHashes = await loadManagedHashLedger(projectRoot);
1146
- const outcome = await copyRegistryFile(
1166
+ for (const { sourceRel, targetRel } of await skillFileTargets(
1147
1167
  registryRoot,
1148
- projectRoot,
1149
- sourceRel,
1150
- targetRel,
1151
- options.protectedGlobs ?? [],
1152
- { managedHashes, persistManagedHashes: false }
1153
- );
1154
- recordOutcome(stats, targetRel, outcome);
1168
+ skill.path,
1169
+ skill.id
1170
+ )) {
1171
+ const outcome = await copyRegistryFile(
1172
+ registryRoot,
1173
+ projectRoot,
1174
+ sourceRel,
1175
+ targetRel,
1176
+ options.protectedGlobs ?? [],
1177
+ { managedHashes, persistManagedHashes: false }
1178
+ );
1179
+ recordOutcome(stats, targetRel, outcome);
1180
+ }
1155
1181
  await saveManagedHashLedger(projectRoot, managedHashes);
1156
1182
  return stats;
1157
1183
  }
@@ -1185,16 +1211,18 @@ async function installPack(registryRoot, projectRoot, packId, options = {}) {
1185
1211
  const managedHashes = await loadManagedHashLedger(projectRoot);
1186
1212
  const copyOpts = { managedHashes, persistManagedHashes: false };
1187
1213
  for (const member of packManifest.members) {
1188
- const { sourceRel, targetRel } = targetForMember(member);
1189
- const outcome = await copyRegistryFile(
1190
- registryRoot,
1191
- projectRoot,
1192
- sourceRel,
1193
- targetRel,
1194
- protectedGlobs,
1195
- copyOpts
1196
- );
1197
- recordOutcome(stats, targetRel, outcome);
1214
+ const pairs = member.kind === "skill" ? await skillFileTargets(registryRoot, member.source, member.id) : [targetForMember(member)];
1215
+ for (const { sourceRel, targetRel } of pairs) {
1216
+ const outcome = await copyRegistryFile(
1217
+ registryRoot,
1218
+ projectRoot,
1219
+ sourceRel,
1220
+ targetRel,
1221
+ protectedGlobs,
1222
+ copyOpts
1223
+ );
1224
+ recordOutcome(stats, targetRel, outcome);
1225
+ }
1198
1226
  }
1199
1227
  await saveManagedHashLedger(projectRoot, managedHashes);
1200
1228
  return stats;
@@ -1620,8 +1648,10 @@ async function buildRegistryPathMap(registryRoot, manifest) {
1620
1648
  for (const packId of manifest.packs ?? []) {
1621
1649
  const pack = await loadPackManifest(registryRoot, packId);
1622
1650
  for (const member of pack.members) {
1623
- const { sourceRel, targetRel } = packMemberTargets(member);
1624
- map.set(targetRel.split(path9.sep).join("/"), sourceRel.split(path9.sep).join("/"));
1651
+ const pairs = member.kind === "skill" ? await skillFileTargets(registryRoot, member.source, member.id) : [packMemberTargets(member)];
1652
+ for (const { sourceRel, targetRel } of pairs) {
1653
+ map.set(targetRel.split(path9.sep).join("/"), sourceRel.split(path9.sep).join("/"));
1654
+ }
1625
1655
  }
1626
1656
  }
1627
1657
  if ((manifest.skills ?? []).length > 0) {
@@ -1630,25 +1660,28 @@ async function buildRegistryPathMap(registryRoot, manifest) {
1630
1660
  for (const id of manifest.skills ?? []) {
1631
1661
  const skill = pool.find((s) => s.id === id);
1632
1662
  if (!skill) continue;
1633
- const category = skill.path.includes("/core/") ? "core" : "community";
1634
- const sourceRel = path9.posix.join(skill.path, "SKILL.md");
1635
- const targetRel = path9.posix.join(".cursor", "skills", category, skill.id, "SKILL.md");
1636
- map.set(targetRel, sourceRel);
1663
+ for (const { sourceRel, targetRel } of await skillFileTargets(
1664
+ registryRoot,
1665
+ skill.path,
1666
+ skill.id
1667
+ )) {
1668
+ map.set(targetRel, sourceRel);
1669
+ }
1637
1670
  }
1638
1671
  }
1639
1672
  return map;
1640
1673
  }
1641
1674
  function guessRegistryPath(projectRel) {
1642
1675
  const p = projectRel.split(path9.sep).join("/");
1643
- if (p.startsWith(".cursor/skills/") && p.endsWith("/SKILL.md")) {
1676
+ if (p.startsWith(".cursor/skills/")) {
1644
1677
  const rest = p.slice(".cursor/skills/".length);
1645
1678
  const parts = rest.split("/");
1646
- if (parts.length === 3 && (parts[0] === "core" || parts[0] === "community")) {
1679
+ if (parts.length >= 3 && (parts[0] === "core" || parts[0] === "community")) {
1647
1680
  return path9.posix.join("registry/skills", rest);
1648
1681
  }
1649
1682
  const skillId = parts[0];
1650
- if (parts.length === 2 && skillId) {
1651
- return path9.posix.join("registry/skills", "community", skillId, "SKILL.md");
1683
+ if (parts.length >= 2 && skillId) {
1684
+ return path9.posix.join("registry/skills", "community", rest);
1652
1685
  }
1653
1686
  }
1654
1687
  if (p.startsWith(".cursor/rules/")) {
@@ -2751,8 +2784,10 @@ async function diffAgainstRegistry(registryRoot, projectRoot, manifest) {
2751
2784
  for (const packId of manifest.packs ?? []) {
2752
2785
  const pack = await loadPackManifest(registryRoot, packId);
2753
2786
  for (const member of pack.members) {
2754
- const { sourceRel, targetRel } = packMemberTargets(member);
2755
- await pushUnique(sourceRel, targetRel);
2787
+ const pairs = member.kind === "skill" ? await skillFileTargets(registryRoot, member.source, member.id) : [packMemberTargets(member)];
2788
+ for (const { sourceRel, targetRel } of pairs) {
2789
+ await pushUnique(sourceRel, targetRel);
2790
+ }
2756
2791
  }
2757
2792
  }
2758
2793
  if ((manifest.skills ?? []).length > 0) {
@@ -2764,10 +2799,13 @@ async function diffAgainstRegistry(registryRoot, projectRoot, manifest) {
2764
2799
  entries.push({ path: `skill:${id}`, status: "missing-registry" });
2765
2800
  continue;
2766
2801
  }
2767
- const category = skill.path.includes("/core/") ? "core" : "community";
2768
- const sourceRel = path13.posix.join(skill.path, "SKILL.md");
2769
- const targetRel = path13.posix.join(".cursor", "skills", category, skill.id, "SKILL.md");
2770
- await pushUnique(sourceRel, targetRel);
2802
+ for (const { sourceRel, targetRel } of await skillFileTargets(
2803
+ registryRoot,
2804
+ skill.path,
2805
+ skill.id
2806
+ )) {
2807
+ await pushUnique(sourceRel, targetRel);
2808
+ }
2771
2809
  }
2772
2810
  }
2773
2811
  return entries;
@@ -2843,7 +2881,7 @@ import { defineCommand as defineCommand7 } from "citty";
2843
2881
 
2844
2882
  // src/invariants/hooks-health.ts
2845
2883
  import { execFile as execFile2 } from "child_process";
2846
- import { constants as constants2, access as access5, readFile as readFile9, readdir as readdir3, stat as stat2 } from "fs/promises";
2884
+ import { constants as constants2, access as access5, readFile as readFile9, readdir as readdir4, stat as stat2 } from "fs/promises";
2847
2885
  import path14 from "path";
2848
2886
  import { promisify as promisify2 } from "util";
2849
2887
  var execFileAsync2 = promisify2(execFile2);
@@ -2927,7 +2965,7 @@ async function assessGitHooksInstallDrift(rootDir) {
2927
2965
  const installHint = "Install or refresh with: `cp git-hooks/<name> .git/hooks/<name> && chmod +x .git/hooks/<name>` (see git-hooks/README.md)";
2928
2966
  let names = [...GIT_HOOK_CANONICAL_NAMES];
2929
2967
  try {
2930
- const listed = await readdir3(canonicalDir);
2968
+ const listed = await readdir4(canonicalDir);
2931
2969
  const fromDisk = listed.filter(
2932
2970
  (n) => GIT_HOOK_CANONICAL_NAMES.includes(n)
2933
2971
  );
@@ -4772,7 +4810,7 @@ var guardCommand = defineCommand8({
4772
4810
 
4773
4811
  // src/commands/handoff.ts
4774
4812
  import { spawn as spawn4 } from "child_process";
4775
- import { readFile as readFile13, readdir as readdir4, writeFile as writeFile6 } from "fs/promises";
4813
+ import { readFile as readFile13, readdir as readdir5, writeFile as writeFile6 } from "fs/promises";
4776
4814
  import path25 from "path";
4777
4815
  import { defineCommand as defineCommand9 } from "citty";
4778
4816
  function parsePlanFrontmatter(raw) {
@@ -4791,7 +4829,7 @@ function parsePlanFrontmatter(raw) {
4791
4829
  }
4792
4830
  async function findActivePlan(plansDir) {
4793
4831
  if (!await fileExists(plansDir)) return null;
4794
- const files = (await readdir4(plansDir)).filter((f) => f.endsWith(".plan.md")).sort().reverse();
4832
+ const files = (await readdir5(plansDir)).filter((f) => f.endsWith(".plan.md")).sort().reverse();
4795
4833
  for (const file of files) {
4796
4834
  const raw = await readFile13(path25.join(plansDir, file), "utf8");
4797
4835
  const fm = parsePlanFrontmatter(raw);
@@ -6425,7 +6463,7 @@ import { defineCommand as defineCommand13 } from "citty";
6425
6463
 
6426
6464
  // src/invariants/monitors-untriaged.ts
6427
6465
  import { execFile as execFile5 } from "child_process";
6428
- import { readFile as readFile17, readdir as readdir5, stat as stat3 } from "fs/promises";
6466
+ import { readFile as readFile17, readdir as readdir6, stat as stat3 } from "fs/promises";
6429
6467
  import path34 from "path";
6430
6468
  import { promisify as promisify5 } from "util";
6431
6469
 
@@ -6450,7 +6488,7 @@ function hasOpenGaps(content) {
6450
6488
  }
6451
6489
  async function listMonitorFiles(memoryDir) {
6452
6490
  try {
6453
- const names = await readdir5(memoryDir);
6491
+ const names = await readdir6(memoryDir);
6454
6492
  return names.filter((n) => n.startsWith("plan-monitor-") && n.endsWith(".md")).sort();
6455
6493
  } catch {
6456
6494
  return [];
@@ -6858,7 +6896,7 @@ function createPersonaBannerPrinter(persona) {
6858
6896
  }
6859
6897
 
6860
6898
  // src/plan-loop/plan-state.ts
6861
- import { readFile as readFile18, readdir as readdir6 } from "fs/promises";
6899
+ import { readFile as readFile18, readdir as readdir7 } from "fs/promises";
6862
6900
  import path38 from "path";
6863
6901
  function countPendingTodos(raw) {
6864
6902
  const lines = raw.split(/\r?\n/);
@@ -6886,7 +6924,7 @@ function countPendingTodos(raw) {
6886
6924
  }
6887
6925
  async function findActivePlanFile(plansDir) {
6888
6926
  if (!await fileExists(plansDir)) return null;
6889
- const files = (await readdir6(plansDir)).filter((f) => f.endsWith(".plan.md")).sort();
6927
+ const files = (await readdir7(plansDir)).filter((f) => f.endsWith(".plan.md")).sort();
6890
6928
  return files[0] ? path38.join(plansDir, files[0]) : null;
6891
6929
  }
6892
6930
  async function readPlan(planPath) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dadado/agent-kit-cli",
3
- "version": "5.3.0",
3
+ "version": "5.4.0",
4
4
  "description": "Agent Kit CLI: HITL framework install and tooling for AI-assisted IDEs (rules, skills, plan/handoff, context).",
5
5
  "license": "PolyForm-Noncommercial-1.0.0",
6
6
  "type": "module",