@dadado/agent-kit-cli 5.2.1 → 5.3.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.
@@ -52,6 +52,39 @@ export function resolveMissionControlPort(args: {
52
52
  probe: (port: number) => { listening: boolean; repoRoot: string | null };
53
53
  opts?: PortOpts;
54
54
  }): { port: number; reuse: boolean; explicit: boolean };
55
+ type BroadcastProbe = {
56
+ listening: boolean;
57
+ repoRoot?: string | null;
58
+ acceptsToken?: boolean;
59
+ tokenGated?: boolean;
60
+ lanReachable?: boolean | null;
61
+ };
62
+ type BroadcastListenerKind =
63
+ | "free"
64
+ | "self-broadcast"
65
+ | "self-other-mode"
66
+ | "foreign"
67
+ | "token-gated"
68
+ | "unknown";
69
+ export function classifyBroadcastListener(
70
+ info: BroadcastProbe,
71
+ repoRoot: string,
72
+ ): BroadcastListenerKind;
73
+ export function describeBroadcastListener(
74
+ kind: string,
75
+ info?: { port?: number | null; repoRoot?: string | null },
76
+ ): string;
77
+ export function resolveBroadcastPort(args: {
78
+ repoRoot: string;
79
+ envPort?: string | number | null;
80
+ probe: (port: number) => BroadcastProbe;
81
+ opts?: PortOpts;
82
+ }): {
83
+ port: number;
84
+ reuse: boolean;
85
+ explicit: boolean;
86
+ skipped: Array<{ port: number; kind: string; repoRoot: string | null }>;
87
+ };
55
88
  export function isSafeRepoRelativePath(relPath: unknown): boolean;
56
89
  export function resolveBindHost(envHost?: string | null): string;
57
90
  export function isLoopbackBindHost(host: string | undefined | null): boolean;
@@ -194,6 +194,134 @@ export function resolveMissionControlPort({ repoRoot, envPort, probe, opts = {}
194
194
  );
195
195
  }
196
196
 
197
+ /**
198
+ * Classify what holds a candidate Mission Control port, from the point of view
199
+ * of a **broadcast** (LAN) start.
200
+ *
201
+ * Broadcast needs a non-loopback, token-gated listener of its own. A loopback
202
+ * `/dashboard` for this same workspace answers `?token=…` with 200 (the query is
203
+ * ignored when no token is required), so "same repoRoot + accepts our token" is
204
+ * not sufficient: reuse also requires the listener to answer on a LAN address.
205
+ * `lanReachable` may be `null`/omitted when the host has no LAN IPv4 to probe —
206
+ * then a root + token match is accepted.
207
+ *
208
+ * `token-gated` means: something Mission-Control-shaped is listening but refuses
209
+ * our token, so its owner cannot be confirmed. Like `foreign` and `unknown` it is
210
+ * never killed and never reused.
211
+ *
212
+ * @param {{ listening: boolean, repoRoot?: string|null, acceptsToken?: boolean, tokenGated?: boolean, lanReachable?: boolean|null }} info
213
+ * @param {string} repoRoot
214
+ * @returns {"free"|"self-broadcast"|"self-other-mode"|"foreign"|"token-gated"|"unknown"}
215
+ */
216
+ export function classifyBroadcastListener(info, repoRoot) {
217
+ if (!info || info.listening !== true) return "free";
218
+ const mine = sameRepoRoot(info.repoRoot, repoRoot);
219
+ if (mine && info.acceptsToken === true) {
220
+ return info.lanReachable === false ? "self-other-mode" : "self-broadcast";
221
+ }
222
+ if (mine) return "self-other-mode";
223
+ if (info.repoRoot != null) return "foreign";
224
+ return info.tokenGated === true ? "token-gated" : "unknown";
225
+ }
226
+
227
+ /** Operator-facing wording for each {@link classifyBroadcastListener} kind. */
228
+ const BROADCAST_LISTENER_LABELS = {
229
+ free: "free",
230
+ "self-broadcast": "this workspace's broadcast",
231
+ "self-other-mode":
232
+ "this workspace, but not a broadcast listener (loopback /dashboard, or another token)",
233
+ foreign: "another workspace",
234
+ "token-gated": "a token-gated Mission Control whose owner this token cannot confirm",
235
+ unknown: "an unidentified process",
236
+ exhausted: "no free port in this workspace's range",
237
+ };
238
+
239
+ /**
240
+ * One honest line about who holds a port. Used by the broadcast preflight so the
241
+ * operator is told *which* instance is in the way instead of a blind kill recipe.
242
+ *
243
+ * @param {string} kind
244
+ * @param {{ port?: number | null, repoRoot?: string | null }} [info]
245
+ * @returns {string}
246
+ */
247
+ export function describeBroadcastListener(kind, info = {}) {
248
+ const label = BROADCAST_LISTENER_LABELS[kind] || BROADCAST_LISTENER_LABELS.unknown;
249
+ const owner =
250
+ kind === "foreign" && info.repoRoot ? `another workspace (${info.repoRoot})` : label;
251
+ return info.port == null ? owner : `${info.port}: ${owner}`;
252
+ }
253
+
254
+ /**
255
+ * Pick a listen port for a broadcast (LAN) Mission Control instance.
256
+ *
257
+ * Same per-workspace candidate walk as {@link resolveMissionControlPort}, with
258
+ * broadcast-aware reuse: only this workspace's own broadcast listener is reused.
259
+ * Anything else on a candidate port (our loopback panel, another workspace, an
260
+ * unidentified process) is **skipped, never killed** — that is what allows more
261
+ * than one Mission Control instance to run at once.
262
+ *
263
+ * Explicit `PORT` still refuses rather than walking, so an operator who pinned a
264
+ * port is told the truth instead of silently landing somewhere else.
265
+ *
266
+ * @param {object} args
267
+ * @param {string} args.repoRoot
268
+ * @param {string | number | undefined | null} [args.envPort] - raw `PORT` env
269
+ * @param {(port: number) => { listening: boolean, repoRoot?: string|null, acceptsToken?: boolean, tokenGated?: boolean, lanReachable?: boolean|null }} args.probe
270
+ * @param {{ base?: number, range?: number }} [args.opts]
271
+ * @returns {{ port: number, reuse: boolean, explicit: boolean, skipped: Array<{ port: number, kind: string, repoRoot: string | null }> }}
272
+ */
273
+ export function resolveBroadcastPort({ repoRoot, envPort, probe, opts = {} }) {
274
+ const root = resolve(String(repoRoot || "").trim() || ".");
275
+ const raw =
276
+ envPort != null && String(envPort).trim() !== ""
277
+ ? Number.parseInt(String(envPort), 10)
278
+ : Number.NaN;
279
+ /** @type {Array<{ port: number, kind: string, repoRoot: string | null }>} */
280
+ const skipped = [];
281
+
282
+ if (Number.isFinite(raw) && raw > 0) {
283
+ const info = probe(raw) || { listening: false };
284
+ const kind = classifyBroadcastListener(info, root);
285
+ if (kind === "free") return { port: raw, reuse: false, explicit: true, skipped };
286
+ if (kind === "self-broadcast") return { port: raw, reuse: true, explicit: true, skipped };
287
+ const owner = info.repoRoot ? String(info.repoRoot) : null;
288
+ skipped.push({ port: raw, kind, repoRoot: owner });
289
+ const detail =
290
+ kind === "self-other-mode"
291
+ ? "it is this workspace's Mission Control but not a broadcast listener (loopback /dashboard, or a different MISSION_CONTROL_TOKEN)"
292
+ : kind === "foreign"
293
+ ? `it is Mission Control for ${owner}, and broadcast never touches another workspace`
294
+ : kind === "token-gated"
295
+ ? "a token-gated Mission Control is there whose owner this token cannot confirm, so it is left alone"
296
+ : "an unidentified process is there, so it is left alone";
297
+ const err = new Error(
298
+ `PORT ${raw} is not available for broadcast: ${detail}. Unset PORT to let broadcast pick a free per-workspace port instead.`,
299
+ );
300
+ Object.assign(err, {
301
+ broadcast: { port: raw, kind, repoRoot: owner, explicit: true, skipped },
302
+ });
303
+ throw err;
304
+ }
305
+
306
+ for (const port of portCandidatesForRepoRoot(root, opts)) {
307
+ const info = probe(port) || { listening: false };
308
+ const kind = classifyBroadcastListener(info, root);
309
+ if (kind === "free") return { port, reuse: false, explicit: false, skipped };
310
+ if (kind === "self-broadcast") return { port, reuse: true, explicit: false, skipped };
311
+ skipped.push({ port, kind, repoRoot: info.repoRoot ? String(info.repoRoot) : null });
312
+ }
313
+
314
+ const base = Number.isFinite(opts.base) ? opts.base : DEFAULT_PORT_BASE;
315
+ const range = Number.isFinite(opts.range) && opts.range > 0 ? opts.range : DEFAULT_PORT_RANGE;
316
+ const err = new Error(
317
+ `No free Mission Control broadcast port in ${base}-${base + range - 1} for ${root}. Stop one of your own unused instances, or set PORT to a free port.`,
318
+ );
319
+ Object.assign(err, {
320
+ broadcast: { port: null, kind: "exhausted", repoRoot: null, explicit: false, skipped },
321
+ });
322
+ throw err;
323
+ }
324
+
197
325
  export const MAX_STRING = {
198
326
  branch: 64,
199
327
  lastCommit: 120,
@@ -5,11 +5,17 @@
5
5
  * Opt-in LAN bind: HOST=0.0.0.0 (or explicit non-loopback), requires
6
6
  * MISSION_CONTROL_TOKEN (generated when unset), detach-starts serve.mjs,
7
7
  * prints LAN URL(s) with token. Does not weaken loopback `/dashboard`.
8
+ *
9
+ * Multi-instance: the listen port is the same per-workspace allocation the
10
+ * loopback starter uses (hash of the snapshot root in the 3333-3588 range unless
11
+ * PORT is set). Candidate ports held by this workspace's loopback panel, another
12
+ * workspace, or an unidentified process are skipped - never killed - so a
13
+ * broadcast can come up beside an already-running Mission Control.
8
14
  */
9
15
 
10
16
  import { execFileSync, execSync, spawn } from "node:child_process";
11
17
  import { existsSync, openSync, realpathSync } from "node:fs";
12
- import { basename, dirname, join } from "node:path";
18
+ import { basename, dirname, join, resolve } from "node:path";
13
19
  import { fileURLToPath } from "node:url";
14
20
  import {
15
21
  buildBroadcastShareUrl,
@@ -19,13 +25,17 @@ import {
19
25
  } from "./lib/broadcast-share.mjs";
20
26
  import {
21
27
  BROADCAST_TOKEN_ENV,
28
+ REPO_ROOT_ENV,
29
+ describeBroadcastListener,
22
30
  escapePerlDoubleQuoted,
23
31
  generateBroadcastToken,
24
32
  isLoopbackBindHost,
25
33
  isValidBroadcastToken,
26
34
  listLanIPv4Addresses,
27
35
  normalizeAuthToken,
36
+ repoRootLogId,
28
37
  resolveBindHost,
38
+ resolveBroadcastPort,
29
39
  resolveContextConfigPath,
30
40
  resolveSnapshotRepoRoot,
31
41
  } from "./lib/guards.mjs";
@@ -37,11 +47,14 @@ const KIT_ROOT = join(__dirname, "..");
37
47
  const ROOT = resolveSnapshotRepoRoot(process.env, KIT_ROOT);
38
48
  process.title = `Mission Control · ${basename(ROOT) || "workspace"}`;
39
49
  const SERVE = join(__dirname, "serve.mjs");
40
- const LOG = process.env.MISSION_CONTROL_LOG || "/tmp/mission-control-broadcast.log";
41
- const PORT = Number.parseInt(process.env.PORT || "3333", 10);
50
+ const LOG =
51
+ process.env.MISSION_CONTROL_LOG || `/tmp/mission-control-broadcast-${repoRootLogId(ROOT)}.log`;
42
52
  const READY_TIMEOUT_MS = 20_000;
43
53
  const READY_POLL_MS = 250;
44
54
 
55
+ /** Allocated listen port for this run (see resolveBroadcastPort in main). */
56
+ let PORT = 0;
57
+
45
58
  function resolveBroadcastEnv() {
46
59
  const env = { ...process.env };
47
60
  let host = resolveBindHost(env.HOST);
@@ -58,30 +71,84 @@ function resolveBroadcastEnv() {
58
71
  return { env, host, token };
59
72
  }
60
73
 
61
- function urlsForProbe(token) {
74
+ function urlsForProbe(token, port = PORT) {
62
75
  const q = `?token=${encodeURIComponent(token)}`;
63
- const urls = [`http://127.0.0.1:${PORT}/${q}`];
76
+ const urls = [`http://127.0.0.1:${port}/${q}`];
64
77
  for (const ip of listLanIPv4Addresses()) {
65
- urls.push(`http://${ip}:${PORT}/${q}`);
78
+ urls.push(`http://${ip}:${port}/${q}`);
66
79
  }
67
80
  return urls;
68
81
  }
69
82
 
83
+ /** HTTP status code as a string; "000" when the connection failed. */
84
+ function httpStatus(url) {
85
+ try {
86
+ return execFileSync(
87
+ "curl",
88
+ ["-s", "-o", "/dev/null", "-w", "%{http_code}", "--max-time", "3", url],
89
+ { encoding: "utf8", timeout: 5000 },
90
+ ).trim();
91
+ } catch {
92
+ return "000";
93
+ }
94
+ }
95
+
70
96
  function probeHttp(url) {
97
+ return httpStatus(url) === "200";
98
+ }
99
+
100
+ /** Snapshot root reported by a listener that accepts our token, else null. */
101
+ function snapshotRootAt(port, token) {
102
+ const url = `http://127.0.0.1:${port}/dashboard-data.json?token=${encodeURIComponent(token)}`;
71
103
  try {
72
- const code = execFileSync("curl", ["-sf", "-o", "/dev/null", "-w", "%{http_code}", url], {
104
+ const raw = execFileSync("curl", ["-sf", url], {
73
105
  encoding: "utf8",
74
- timeout: 3000,
75
- }).trim();
76
- return code === "200";
106
+ timeout: 8000,
107
+ maxBuffer: 10 * 1024 * 1024,
108
+ });
109
+ const root = JSON.parse(raw)?.system?.repoRoot;
110
+ return typeof root === "string" && root.trim() ? resolve(root.trim()) : null;
77
111
  } catch {
78
- return false;
112
+ return null;
79
113
  }
80
114
  }
81
115
 
82
- function listeningPids() {
116
+ /**
117
+ * Probe one candidate port for `resolveBroadcastPort`.
118
+ *
119
+ * `lanReachable` is what separates our own broadcast (reusable) from our own
120
+ * loopback panel: a loopback listener needs no token, so it answers `?token=…`
121
+ * with 200 on 127.0.0.1 while staying unreachable on every LAN address.
122
+ */
123
+ function probeBroadcastPort(port, token) {
124
+ const q = `?token=${encodeURIComponent(token)}`;
125
+ const loopbackStatus = httpStatus(`http://127.0.0.1:${port}/${q}`);
126
+ const listening = loopbackStatus !== "000" || listeningPids(port).length > 0;
127
+ if (!listening) {
128
+ return { listening: false, repoRoot: null, acceptsToken: false, tokenGated: false };
129
+ }
130
+ const acceptsToken = loopbackStatus === "200";
131
+ if (!acceptsToken) {
132
+ return {
133
+ listening: true,
134
+ repoRoot: null,
135
+ acceptsToken: false,
136
+ tokenGated: loopbackStatus === "401" || loopbackStatus === "403",
137
+ };
138
+ }
139
+ const ips = listLanIPv4Addresses();
140
+ return {
141
+ listening: true,
142
+ repoRoot: snapshotRootAt(port, token),
143
+ acceptsToken: true,
144
+ tokenGated: false,
145
+ lanReachable: ips.length > 0 ? ips.some((ip) => probeHttp(`http://${ip}:${port}/${q}`)) : null,
146
+ };
147
+ }
148
+
149
+ function listeningPids(port = PORT) {
83
150
  try {
84
- const out = execFileSync("lsof", ["-nP", `-iTCP:${PORT}`, "-sTCP:LISTEN", "-t"], {
151
+ const out = execFileSync("lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-t"], {
85
152
  encoding: "utf8",
86
153
  timeout: 3000,
87
154
  }).trim();
@@ -124,6 +191,7 @@ function detachStart(env) {
124
191
  const hostEsc = escapePerlDoubleQuoted(String(env.HOST));
125
192
  const tokenEsc = escapePerlDoubleQuoted(String(env[BROADCAST_TOKEN_ENV]));
126
193
  const portEsc = escapePerlDoubleQuoted(String(PORT));
194
+ const snapEsc = escapePerlDoubleQuoted(ROOT);
127
195
  const perl = [
128
196
  "use POSIX qw(setsid);",
129
197
  "exit if fork;",
@@ -136,6 +204,7 @@ function detachStart(env) {
136
204
  `$ENV{HOST}="${hostEsc}";`,
137
205
  `$ENV{${BROADCAST_TOKEN_ENV}}="${tokenEsc}";`,
138
206
  `$ENV{PORT}="${portEsc}";`,
207
+ `$ENV{${REPO_ROOT_ENV}}="${snapEsc}";`,
139
208
  `exec("node","${serveEsc}");`,
140
209
  ].join(" ");
141
210
 
@@ -159,6 +228,68 @@ async function waitReady(urls) {
159
228
  return null;
160
229
  }
161
230
 
231
+ /**
232
+ * Preflight: name every candidate port we walked past. Nothing here is killed —
233
+ * a busy port means another Mission Control (ours or someone else's) keeps
234
+ * running and broadcast lands on the next free per-workspace port.
235
+ */
236
+ function reportSkipped(skipped) {
237
+ if (!skipped || skipped.length === 0) return;
238
+ console.log("Ports already held (left running):");
239
+ for (const entry of skipped) {
240
+ console.log(` ${describeBroadcastListener(entry.kind, entry)}`);
241
+ }
242
+ if (skipped.some((entry) => entry.kind === "token-gated")) {
243
+ console.log(
244
+ ` Export ${BROADCAST_TOKEN_ENV} with an existing broadcast's token to reuse it instead of starting another.`,
245
+ );
246
+ }
247
+ }
248
+
249
+ /** Recovery text for a port we may not take. Never a blind kill of a foreign listener. */
250
+ function recoveryLines(kind, port) {
251
+ switch (kind) {
252
+ case "self-other-mode":
253
+ return [
254
+ ` That listener is this workspace (${ROOT}). Stop it yourself if you want this exact port:`,
255
+ ` kill "$(lsof -nP -iTCP:${port} -sTCP:LISTEN -t)"`,
256
+ " Or retry with PORT unset: broadcast will take a free per-workspace port and leave it running.",
257
+ ];
258
+ case "foreign":
259
+ return [
260
+ " Leave that workspace's Mission Control running. Retry with PORT unset to take a free per-workspace port.",
261
+ ];
262
+ case "token-gated":
263
+ return [
264
+ ` Export ${BROADCAST_TOKEN_ENV} with that instance's token to reuse it, or retry with PORT unset.`,
265
+ ];
266
+ case "exhausted":
267
+ return [
268
+ " Every candidate port in this workspace's range is held by an instance that is not ours to stop.",
269
+ " Stop one of your own Mission Control instances, or set PORT to a port you know is free.",
270
+ ];
271
+ default:
272
+ return [
273
+ " Leave that process alone. Retry with PORT unset to take a free per-workspace port.",
274
+ ];
275
+ }
276
+ }
277
+
278
+ function reportRefusal(err) {
279
+ console.error(err instanceof Error ? err.message : String(err));
280
+ const info = err?.broadcast;
281
+ if (!info) return;
282
+ if (info.kind === "exhausted" && info.skipped?.length) {
283
+ console.error("Ports checked (all left running):");
284
+ for (const entry of info.skipped.slice(0, 8)) {
285
+ console.error(` ${describeBroadcastListener(entry.kind, entry)}`);
286
+ }
287
+ }
288
+ for (const line of recoveryLines(info.kind, info.port)) {
289
+ console.error(line);
290
+ }
291
+ }
292
+
162
293
  async function main() {
163
294
  const { env, host, token } = resolveBroadcastEnv();
164
295
  if (isLoopbackBindHost(host)) {
@@ -168,6 +299,26 @@ async function main() {
168
299
  process.exit(1);
169
300
  }
170
301
 
302
+ let allocation;
303
+ try {
304
+ allocation = resolveBroadcastPort({
305
+ repoRoot: ROOT,
306
+ envPort: process.env.PORT,
307
+ probe: (port) => probeBroadcastPort(port, token),
308
+ });
309
+ } catch (err) {
310
+ reportRefusal(err);
311
+ process.exit(1);
312
+ }
313
+
314
+ reportSkipped(allocation.skipped);
315
+
316
+ PORT = allocation.port;
317
+ // Pin the allocation for this process and the detached server.
318
+ process.env.PORT = String(PORT);
319
+ env.PORT = String(PORT);
320
+ env[REPO_ROOT_ENV] = ROOT;
321
+
171
322
  const urls = urlsForProbe(token);
172
323
  const primaryLan = listLanIPv4Addresses()[0];
173
324
  const displayUrl =
@@ -175,15 +326,9 @@ async function main() {
175
326
  ? `http://${primaryLan}:${PORT}/?token=${encodeURIComponent(token)}`
176
327
  : urls[0];
177
328
 
178
- const already = listeningPids().length > 0 && urls.some((u) => probeHttp(u));
179
- if (!already) {
180
- if (listeningPids().length > 0) {
181
- console.error(
182
- `Port ${PORT} is listening but did not accept the broadcast token. Stop the existing Mission Control instance (loopback /dashboard) first, then retry.`,
183
- );
184
- console.error(` kill "$(lsof -nP -iTCP:${PORT} -sTCP:LISTEN -t)"`);
185
- process.exit(1);
186
- }
329
+ if (allocation.reuse) {
330
+ console.log(`Mission Control broadcast already listening on port ${PORT} for ${ROOT}`);
331
+ } else {
187
332
  console.log(`Starting Mission Control broadcast on ${host}:${PORT}…`);
188
333
  detachStart(env);
189
334
  const ready = await waitReady(urls);
@@ -192,8 +337,6 @@ async function main() {
192
337
  console.error(`Check the log: ${LOG}`);
193
338
  process.exit(1);
194
339
  }
195
- } else {
196
- console.log(`Mission Control broadcast already listening on port ${PORT}`);
197
340
  }
198
341
 
199
342
  const shareBase = resolveShareBase(process.env);
@@ -233,7 +376,10 @@ async function main() {
233
376
  " Share is a cosmetic Mission Kit (or BYO) link; phone must still reach this LAN.",
234
377
  );
235
378
  }
236
- console.log(" Config writes stay loopback-only. Stop: kill the LISTEN pid on this port.");
379
+ console.log(` Root: ${ROOT}`);
380
+ console.log(
381
+ " Config writes stay loopback-only. Stop this workspace only: kill the LISTEN pid on this port.",
382
+ );
237
383
  console.log(" Firewall: allow inbound TCP on this port for your LAN profile if needed.");
238
384
  console.log("");
239
385
 
package/dist/index.js CHANGED
@@ -198,7 +198,23 @@ var KNOWN_SHIPPED_OVERLAY_HASHES = /* @__PURE__ */ new Set([
198
198
  "a8756742197c3a2e1a6d64f4bde2a88db48abb66a0f449d625cd2e1c7b8d0cb4",
199
199
  "6a5f8795a4a26b419f167e249576b798781c95308dc1ea50958351de713231d4",
200
200
  "4009c5e2faf9775d5708cdff0b5f9da80fd3ce390cc42aed0bb94272f6b05b1b",
201
- "a8e070fae187908ef7b2cf2a41605e3079329403b1802b5fc67e83294c96a070"
201
+ "a8e070fae187908ef7b2cf2a41605e3079329403b1802b5fc67e83294c96a070",
202
+ "81186b528105f2873818ea91f51d20c95a68c0c4b3d128aa8896dd38a1bb3159",
203
+ "b2c7db73a0e18bd475651001722aa34b39276af9f73f0921102b680eb42bea0e",
204
+ "d03445b726cb6afe2ff37a936de97605cc60e441896e06385abfc15a127e2f45",
205
+ "f5ef970adaf4cfa7af551e35cb41b4052576109f5444a280b1e26562338b39e8",
206
+ "fe3f8e2339a7f183545f2aad6b7f02d3687277ca67597fa7620bfee269392bcd",
207
+ "b2489e381ea7daaef7299e5aede437a8baaa39baaddaac849efbf0a6ccb84a2b",
208
+ "0ae7d076904fd342de3ba38745f38fed8ca5c1f02d7b099d27deeb0a9be79740",
209
+ "15ca459811f0f050f4eeff77d4567b45c0790beb77904ecf37e145d65a858c77",
210
+ "1dc329c0c0695a4395bb7c30db0fb7b6fb06bcf5fdc1d6a475656ac6a525aa5f",
211
+ "35bb0008ef52af147a45fba667f7dceb841d537ad5e185317672577afbcbc189",
212
+ "42b701d224f4e690dc027d66fcc1214bab31a0ae0c50ef982124f57d4dae4982",
213
+ "65272f85a32f8fe95ed19d0823fa9d1d6dee5bc18a0244f7b553ea5162ba9b36",
214
+ "73cd50bac290b84df6245d3647e686dee8ec3b0434736d08fdba6e30d342ef0a",
215
+ "61b635ea8a08171062bd329a4615d7426eb787eb796271aa882f55f39db56810",
216
+ "86afbea8f64de68a79ad5e374f3132bdbe2582b94321fb3d438314838e36c776",
217
+ "65cc1c0293b145e48ed73ad0ca9ab33cbba5ed834a5bfb31f195ed30c2f143df"
202
218
  ]);
203
219
 
204
220
  // src/lifecycle/paths.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dadado/agent-kit-cli",
3
- "version": "5.2.1",
3
+ "version": "5.3.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",