@indigoai-us/hq-cli 5.146.0 → 5.147.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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,15 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.147.0] — 2026-09-23
6
+
7
+ ### Fixed
8
+
9
+ - `hq mesh context prd-sync` now moves the Board. Its task_status events carry the session's company and project, and take sequence numbers from the session's shared counter (the same file the hooks use), so the server no longer rejects them as duplicates. Rejections print their codes.
10
+ - The mesh daemon no longer treats a missing company slug, a membership timeout, or a lookup error as proof that the device owner is not a member. A device-default session that was cleared to `needs_company` is checked once against the configured default company and, when that membership is active, the company is put back and held events flush. The same cleared session is not counted as a new denial on every later flush.
11
+ - `hq mesh project ensure` no longer fails when the server refuses to close the extra genesis thread; it leaves that thread open and prints one line. Registration and the calling session's binding complete.
12
+ - The mesh daemon re-checks a `needs_project` or unresolved session when it flushes that session's events. If the project Board is now an exact or clear catalog match, the batch is posted with that project and the session file is bound. A tie or near miss stays `needs_project`. The check runs at most once a minute per session.
13
+
5
14
  ## [5.146.0] — 2026-09-23
6
15
 
7
16
  ### Fixed
@@ -5,6 +5,7 @@
5
5
  * require hq-pack-work-mesh on disk and does not start MQTT listen.
6
6
  * Distinct from `hq doctor` (hook guardrails).
7
7
  */
8
+ import { resolve as resolvePath } from "node:path";
8
9
  import * as os from "node:os";
9
10
  import { readFileSync } from "node:fs";
10
11
  import { findHqRoot } from "../utils/manifest.js";
@@ -42,6 +43,7 @@ import { findLocalPrd, listNeedsProjectSessions } from "../lib/work-context/bind
42
43
  import { bindCallingSession, formatSessionBoundLine } from "../lib/work-context/bind-calling-session.js";
43
44
  import { rebindNeedsProjectOnPrompt } from "../lib/work-context/prompt-rebind.js";
44
45
  import { planPrdSync } from "../lib/work-context/prd-sync.js";
46
+ import { reserveSessionSeq } from "../lib/mesh/live/session-seq.js";
45
47
  import { WORK_CONTEXT_CONTRACT_VERSION } from "../lib/work-context/contract.js";
46
48
  import { loadObservationFromFile, markUntracked, parseObservationJson, reconcileObservation, } from "../lib/work-context/reconcile.js";
47
49
  import { resolveCompany } from "../lib/work-context/company.js";
@@ -565,6 +567,15 @@ async function runPrdSync(opts) {
565
567
  console.error("work-mesh: prd-sync has changes but no token; snapshot not advanced");
566
568
  return;
567
569
  }
570
+ // Attribution evidence: without company and project the server files these
571
+ // as unassigned and the Board never moves.
572
+ const bound = readSessionState(sessionId, root);
573
+ const evidenceBase = {
574
+ ...(bound?.companySlug ? { companySlug: bound.companySlug } : {}),
575
+ ...(bound?.projectId ? { project: bound.projectId } : {}),
576
+ touchedPaths: [resolvePath(file)],
577
+ };
578
+ const seqFirst = reserveSessionSeq(sessionId, plan.events.length);
568
579
  const events = plan.events.map((event, index) => buildMeshEmitEvent({
569
580
  eventId: generateUlid(Date.now() + index),
570
581
  kind: "task_status",
@@ -572,18 +583,24 @@ async function runPrdSync(opts) {
572
583
  harness: "hq-sessions",
573
584
  adapterVersion: "1.0.0",
574
585
  at: new Date().toISOString(),
575
- seq: plan.seqStart + index + 1,
586
+ seq: seqFirst + index,
576
587
  source: "hooks",
577
588
  taskId: event.taskId,
578
589
  status: event.status,
579
590
  summary: event.status === "done" ? `${event.taskId} passes` : `${event.taskId} in progress`,
591
+ evidence: { ...evidenceBase, task: event.taskId },
580
592
  }));
581
593
  const poster = createEmitPoster({ token, baseUrl: resolveVaultApiBase(process.env) });
582
- await emitEvents({
594
+ const emitted = await emitEvents({
583
595
  workMeshRoot: workMeshHomeRoot(),
584
596
  poster,
585
597
  newEvents: events,
586
598
  });
599
+ if (emitted.unassigned > 0 || emitted.rejected > 0) {
600
+ console.error(`work-mesh: prd-sync sent ${events.length}; unassigned=${emitted.unassigned} rejected=${emitted.rejected}`);
601
+ for (const line of formatEmitRejectionLines(emitted.rejections))
602
+ console.error(line);
603
+ }
587
604
  plan.commit();
588
605
  if (plan.taskId) {
589
606
  updateSessionState(sessionId, root, (current) => {
@@ -78,7 +78,7 @@ export declare function listActiveMembershipCompanies(token: string): Promise<Me
78
78
  * Returns null when the caller is not an active member. Throws on transport /
79
79
  * auth failures so the caller can surface DEFAULT_COMPANY_UNAVAILABLE.
80
80
  */
81
- export declare function resolveActiveMembershipCompany(token: string, slug: string): Promise<MeshCompany | null>;
81
+ export declare function resolveActiveMembershipCompany(token: string, slug: string, signal?: AbortSignal): Promise<MeshCompany | null>;
82
82
  export interface RequireTokenOptions {
83
83
  /**
84
84
  * False from detached or unattended callers (the lane watcher). A person
@@ -113,11 +113,15 @@ export async function listActiveMembershipCompanies(token) {
113
113
  * Returns null when the caller is not an active member. Throws on transport /
114
114
  * auth failures so the caller can surface DEFAULT_COMPANY_UNAVAILABLE.
115
115
  */
116
- export async function resolveActiveMembershipCompany(token, slug) {
116
+ export async function resolveActiveMembershipCompany(token, slug, signal) {
117
117
  const trimmed = slug.trim();
118
118
  if (!trimmed)
119
119
  return null;
120
- const payload = asRecord(await meshJson(token, "/membership/me"));
120
+ // An aborted lookup is not "not a member". Throw so callers defer.
121
+ if (signal?.aborted) {
122
+ throw new Error("membership lookup aborted");
123
+ }
124
+ const payload = asRecord(await meshJson(token, "/membership/me", { signal }));
121
125
  if (!payload || !Array.isArray(payload.memberships)) {
122
126
  throw new Error("Malformed membership response: expected memberships array");
123
127
  }
@@ -165,8 +165,20 @@ export async function runMeshDaemon(deps = {}) {
165
165
  ? readSessionState(input.sessionId, ctxRoot)
166
166
  : null);
167
167
  const slug = input.companySlug ?? state?.companySlug;
168
+ // A uid alone is enough to ask /membership/me. Missing slug is not a
169
+ // denial. A device-default session whose company was cleared can be
170
+ // checked against the configured default; that is the same company the
171
+ // daemon assigned before the false denial.
172
+ const configuredDefault = input.deviceDefault
173
+ ? readDeviceConfig({ root: ctxRoot }).defaultCompany
174
+ : undefined;
175
+ const defaultCompany = configuredDefault?.enabled ? configuredDefault : undefined;
176
+ const usingDefaultFallback = !slug && !state?.companyUid && Boolean(defaultCompany?.slug || defaultCompany?.uid);
177
+ const lookupKey = slug || state?.companyUid || (usingDefaultFallback
178
+ ? (defaultCompany?.slug || defaultCompany?.uid)
179
+ : undefined);
168
180
  let authorization;
169
- if (!slug) {
181
+ if (!lookupKey) {
170
182
  authorization = { status: "denied" };
171
183
  }
172
184
  else {
@@ -175,7 +187,14 @@ export async function runMeshDaemon(deps = {}) {
175
187
  try {
176
188
  authorization = await Promise.race([
177
189
  (async () => {
178
- const membership = await resolveMembership(await getToken(), slug, controller.signal);
190
+ const membership = await resolveMembership(await getToken(), lookupKey, controller.signal);
191
+ // Timeout wins even if the probe then returns null.
192
+ if (controller.signal.aborted) {
193
+ return {
194
+ status: "unavailable",
195
+ error: `membership lookup timed out after ${DEVICE_DEFAULT_MEMBERSHIP_TIMEOUT_MS}ms`,
196
+ };
197
+ }
179
198
  if (membership)
180
199
  return { status: "authorized", uid: membership.companyUid };
181
200
  // A machine token often cannot read /membership/me (401/403, or an
@@ -221,8 +240,43 @@ export async function runMeshDaemon(deps = {}) {
221
240
  if (authorization.status === "authorized" &&
222
241
  (!input.companyUid || input.companyUid === authorization.uid) &&
223
242
  (!state?.companyUid || state.companyUid === authorization.uid)) {
243
+ // A prior false denial cleared the company and left needs_company.
244
+ // Membership just confirmed the company, so put it back and let flush post.
245
+ if (state &&
246
+ state.companyConfidence === "device_default" &&
247
+ state.contextStatus === "needs_company") {
248
+ const restoredSlug = state.companySlug || slug || defaultCompany?.slug;
249
+ const restoredUid = authorization.uid;
250
+ updateSessionState(state.sessionId, ctxRoot, (current) => {
251
+ if (!current || current.contextStatus !== "needs_company")
252
+ return null;
253
+ if (current.companyUid && current.companyUid !== restoredUid)
254
+ return null;
255
+ if (current.companySlug &&
256
+ restoredSlug &&
257
+ !sameCompanyValue(current.companySlug, restoredSlug)) {
258
+ return null;
259
+ }
260
+ return mergeLocalSessionFields({
261
+ ...current,
262
+ companyUid: restoredUid,
263
+ ...(restoredSlug ? { companySlug: restoredSlug } : {}),
264
+ contextStatus: current.projectId
265
+ ? (current.taskId ? "bound" : "needs_task")
266
+ : "needs_project",
267
+ updatedAt: now().toISOString(),
268
+ }, current, { preserveCompanyBinding: false });
269
+ });
270
+ }
224
271
  return authorization;
225
272
  }
273
+ // Already stripped. Do not revoke, count, or log this again on every flush.
274
+ if (state &&
275
+ state.contextStatus === "needs_company" &&
276
+ !state.companyUid &&
277
+ !state.companySlug) {
278
+ return { status: "denied" };
279
+ }
226
280
  try {
227
281
  invalidateDefaultCompanyMemberships({ root: ctxRoot, now });
228
282
  }
@@ -16,6 +16,11 @@ export declare const HELD_TTL_MS: number;
16
16
  export declare const HELD_RETRY_INTERVAL_MS: number;
17
17
  /** Cap held.jsonl size; oldest overflow is dead-lettered with HELD_OVERFLOW. */
18
18
  export declare const HELD_MAX_LINES = 20000;
19
+ /**
20
+ * Re-check an unbound session against the project catalog at most this often.
21
+ * The catalog itself is also cached inside bind-best.
22
+ */
23
+ export declare const PROJECT_REBIND_INTERVAL_MS = 60000;
19
24
  export type HeldReason = "STATE_ABSENT" | "NEEDS_COMPANY" | "COMPANY_CONFLICT" | "NEEDS_PROJECT" | "HELD_EXPIRED" | "HELD_OVERFLOW";
20
25
  export interface HeldLineRecord {
21
26
  /** Original spool event fields. */
@@ -46,6 +51,9 @@ export interface FlushDeps {
46
51
  log?: (message: string) => void;
47
52
  /** Authoritative gate for state derived from a device default. */
48
53
  authorizeDeviceDefaultTenant?: (state: SessionStateFile) => Promise<boolean>;
54
+ /** Passed through to bind-best (catalog cache root and HQ_WORK_MESH_CACHE). */
55
+ env?: NodeJS.ProcessEnv;
56
+ home?: string;
49
57
  }
50
58
  export interface FlushSummary {
51
59
  claimed: number;
@@ -12,6 +12,7 @@ import * as fs from "node:fs";
12
12
  import * as path from "node:path";
13
13
  import { resolveCompany } from "../../work-context/company.js";
14
14
  import { deriveRemoteOwnerSlug, deriveRepoIdentityKey, } from "../../work-context/repo-remote.js";
15
+ import { rebindNeedsProjectOnPrompt } from "../../work-context/prompt-rebind.js";
15
16
  import { readSessionState, updateSessionState } from "../../work-context/state.js";
16
17
  import { repairAckedRegisterStates } from "../../work-context/outbox.js";
17
18
  import { workContextRoot } from "../../work-context/paths.js";
@@ -25,6 +26,12 @@ export const HELD_TTL_MS = 7 * 24 * 60 * 60 * 1000;
25
26
  export const HELD_RETRY_INTERVAL_MS = 5 * 60 * 1000;
26
27
  /** Cap held.jsonl size; oldest overflow is dead-lettered with HELD_OVERFLOW. */
27
28
  export const HELD_MAX_LINES = 20_000;
29
+ /**
30
+ * Re-check an unbound session against the project catalog at most this often.
31
+ * The catalog itself is also cached inside bind-best.
32
+ */
33
+ export const PROJECT_REBIND_INTERVAL_MS = 60_000;
34
+ const lastProjectRebindAtMs = new Map();
28
35
  function parseLine(line) {
29
36
  try {
30
37
  const v = JSON.parse(line);
@@ -224,18 +231,25 @@ export async function flushSessionEvents(deps) {
224
231
  };
225
232
  const loggedProject404 = new Set();
226
233
  for (const [sessionId, items] of bySession) {
227
- const state = readSessionState(sessionId, deps.workContextRoot);
234
+ let state = readSessionState(sessionId, deps.workContextRoot);
228
235
  if (state?.companyConfidence === "device_default" &&
229
- deps.authorizeDeviceDefaultTenant &&
230
- !(await deps.authorizeDeviceDefaultTenant(state))) {
231
- const heldAt = now().toISOString();
232
- for (const item of items) {
233
- appendHeldLine(heldEnvelope(item.event, "NEEDS_COMPANY", item.heldAt ?? heldAt), deps.workMeshRoot);
234
- summary.held += 1;
235
- markDisposed(item.event);
236
+ deps.authorizeDeviceDefaultTenant) {
237
+ const allowed = await deps.authorizeDeviceDefaultTenant(state);
238
+ // Authorization may restore a company the gate previously cleared.
239
+ state = readSessionState(sessionId, deps.workContextRoot) ?? state;
240
+ if (!allowed) {
241
+ const heldAt = now().toISOString();
242
+ for (const item of items) {
243
+ appendHeldLine(heldEnvelope(item.event, "NEEDS_COMPANY", item.heldAt ?? heldAt), deps.workMeshRoot);
244
+ summary.held += 1;
245
+ markDisposed(item.event);
246
+ }
247
+ continue;
236
248
  }
237
- continue;
238
249
  }
250
+ // After the company gate (which may restore a cleared company), try to
251
+ // bind a needs_project session to an existing Board before classifying.
252
+ state = rebindUnboundSession(sessionId, items, state, deps, nowMs);
239
253
  const decision = classifySession(state);
240
254
  if (decision.action === "drop") {
241
255
  appendDroppedReceipt({
@@ -502,6 +516,54 @@ function canonicalizeForRestore(event) {
502
516
  }
503
517
  return out;
504
518
  }
519
+ function eventText(event, key) {
520
+ const value = event[key];
521
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
522
+ }
523
+ /**
524
+ * Before a needs_project (or unresolved-with-company) batch is classified,
525
+ * run the prompt-submit bind-best refinement. A bind updates the session
526
+ * file so the batch posts with that projectId. Ties stay unbound. At most
527
+ * one attempt per session per minute; failures stay on the existing state.
528
+ */
529
+ function rebindUnboundSession(sessionId, items, state, deps, nowMs) {
530
+ if (!state?.companyUid)
531
+ return state;
532
+ if (state.contextStatus !== "needs_project" && state.contextStatus !== "unresolved") {
533
+ return state;
534
+ }
535
+ const key = `${deps.workContextRoot}\0${sessionId}`;
536
+ const previous = lastProjectRebindAtMs.get(key);
537
+ if (previous !== undefined && nowMs - previous < PROJECT_REBIND_INTERVAL_MS) {
538
+ return state;
539
+ }
540
+ lastProjectRebindAtMs.set(key, nowMs);
541
+ const sample = items[0]?.event ?? {};
542
+ const started = Date.now();
543
+ try {
544
+ rebindNeedsProjectOnPrompt({
545
+ sessionId,
546
+ root: deps.workContextRoot,
547
+ hqRoot: eventText(sample, "hqRoot"),
548
+ cwd: eventText(sample, "cwd"),
549
+ companySlug: state.companySlug ?? eventText(sample, "companySlug"),
550
+ companyUid: state.companyUid,
551
+ hint: eventText(sample, "project"),
552
+ env: deps.env,
553
+ home: deps.home,
554
+ nowMs,
555
+ now: deps.now,
556
+ });
557
+ }
558
+ catch (err) {
559
+ deps.log?.(`work-mesh: session ${sessionId} project rebind failed: ${err instanceof Error ? err.message : "unknown"}`);
560
+ }
561
+ const elapsed = Date.now() - started;
562
+ if (elapsed > 300) {
563
+ deps.log?.(`work-mesh: session ${sessionId} project rebind took ${elapsed}ms`);
564
+ }
565
+ return readSessionState(sessionId, deps.workContextRoot) ?? state;
566
+ }
505
567
  function classifySession(state) {
506
568
  if (!state) {
507
569
  return { action: "hold", heldReason: "STATE_ABSENT" };
@@ -0,0 +1,4 @@
1
+ export declare function sessionSeqDir(env?: NodeJS.ProcessEnv): string;
2
+ /** Reserve `count` consecutive seq numbers; returns the first one. */
3
+ export declare function reserveSessionSeq(sessionId: string, count: number, env?: NodeJS.ProcessEnv): number;
4
+ //# sourceMappingURL=session-seq.d.ts.map
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Shared per-session event sequence, the same file the core hooks use
3
+ * (`~/.hq/work-mesh/seq/<sid>`, see core/scripts/lib/work-mesh-live-hook.sh).
4
+ * Every emitter for a session must take numbers from here, or the server
5
+ * treats the new events as duplicates of earlier ones and drops them.
6
+ */
7
+ import * as fs from "node:fs";
8
+ import * as os from "node:os";
9
+ import * as path from "node:path";
10
+ export function sessionSeqDir(env = process.env) {
11
+ return env.WORK_MESH_SEQ_DIR?.trim() || path.join(os.homedir(), ".hq", "work-mesh", "seq");
12
+ }
13
+ /** Reserve `count` consecutive seq numbers; returns the first one. */
14
+ export function reserveSessionSeq(sessionId, count, env = process.env) {
15
+ if (!/^[A-Za-z0-9._-]+$/.test(sessionId))
16
+ throw new Error("invalid session id");
17
+ const dir = sessionSeqDir(env);
18
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
19
+ const file = path.join(dir, sessionId);
20
+ let current = 0;
21
+ try {
22
+ const raw = fs.readFileSync(file, "utf8").trim();
23
+ if (/^\d+$/.test(raw))
24
+ current = Number(raw);
25
+ }
26
+ catch {
27
+ current = 0;
28
+ }
29
+ const first = current + 1;
30
+ fs.writeFileSync(file, `${current + Math.max(0, count)}\n`, { mode: 0o600 });
31
+ return first;
32
+ }
33
+ //# sourceMappingURL=session-seq.js.map
@@ -11,9 +11,17 @@ export async function closeSupersededProjectThread(input) {
11
11
  return;
12
12
  if (!input.openedThreadId || input.openedThreadId === input.canonicalThreadId)
13
13
  return;
14
- await appendThreadEvent(input.token, input.companyUid, input.openedThreadId, "done", {
15
- summary: "Superseded by the registered project thread.",
16
- });
14
+ try {
15
+ await appendThreadEvent(input.token, input.companyUid, input.openedThreadId, "done", {
16
+ summary: "Superseded by the registered project thread.",
17
+ });
18
+ }
19
+ catch (err) {
20
+ // Best effort: the server may refuse (e.g. only the thread owner may mark
21
+ // it done). A leftover open thread must never fail registration.
22
+ const reason = err instanceof Error ? err.message : String(err);
23
+ process.stderr.write(`Work mesh: left extra thread ${input.openedThreadId} open (${reason.slice(0, 120)})\n`);
24
+ }
17
25
  }
18
26
  /**
19
27
  * Create a missing Board. Register returns 403 PROJECT_NOT_IN_COMPANY until
@@ -1,6 +1,7 @@
1
1
  /**
2
- * US-040: UserPromptSubmit re-runs bind-best for a session still in
3
- * needs_project. Catalog reads are cached. Failures stay local.
2
+ * US-040: re-run bind-best for a session still in needs_project, or
3
+ * unresolved once it has a company. The prompt hook and the mesh daemon
4
+ * flush both call this. Catalog reads are cached. Failures stay local.
4
5
  */
5
6
  export type PromptRebindResult = {
6
7
  action: "bound" | "skipped" | "unchanged";
@@ -14,7 +15,10 @@ export declare function rebindNeedsProjectOnPrompt(input: {
14
15
  cwd?: string;
15
16
  companySlug?: string;
16
17
  companyUid?: string;
18
+ /** Used when the session has no projectHint (for example a spool `project` field). */
19
+ hint?: string;
17
20
  env?: NodeJS.ProcessEnv;
21
+ home?: string;
18
22
  nowMs?: number;
19
23
  now?: () => Date;
20
24
  }): PromptRebindResult;
@@ -1,6 +1,7 @@
1
1
  /**
2
- * US-040: UserPromptSubmit re-runs bind-best for a session still in
3
- * needs_project. Catalog reads are cached. Failures stay local.
2
+ * US-040: re-run bind-best for a session still in needs_project, or
3
+ * unresolved once it has a company. The prompt hook and the mesh daemon
4
+ * flush both call this. Catalog reads are cached. Failures stay local.
4
5
  */
5
6
  import { projectIdFromCwd } from "./company.js";
6
7
  import { readMetaProjectTask } from "./project.js";
@@ -17,13 +18,17 @@ export function rebindNeedsProjectOnPrompt(input) {
17
18
  }
18
19
  if (!state)
19
20
  return { action: "skipped", reason: "no_state" };
20
- if (state.contextStatus !== "needs_project") {
21
+ const companyUid = input.companyUid ?? state.companyUid;
22
+ const retryable = state.contextStatus === "needs_project" ||
23
+ (state.contextStatus === "unresolved" && Boolean(companyUid));
24
+ if (!retryable) {
21
25
  return { action: "skipped", reason: "not_needs_project" };
22
26
  }
23
27
  if (state.projectId)
24
28
  return { action: "skipped", reason: "different_project" };
25
29
  const env = input.env ?? process.env;
26
30
  const hint = state.projectHint?.trim() ||
31
+ input.hint?.trim() ||
27
32
  env.HQ_SPAWN_PROJECT?.trim() ||
28
33
  readMetaProjectTask(input.sessionId, input.hqRoot).projectId ||
29
34
  projectIdFromCwd(input.cwd, input.hqRoot) ||
@@ -36,9 +41,10 @@ export function rebindNeedsProjectOnPrompt(input) {
36
41
  hint,
37
42
  hqRoot: input.hqRoot,
38
43
  companySlug: input.companySlug ?? state.companySlug,
39
- companyUid: input.companyUid ?? state.companyUid,
44
+ companyUid,
40
45
  repoPath: input.cwd,
41
46
  env,
47
+ home: input.home,
42
48
  nowMs: input.nowMs ?? Date.now(),
43
49
  });
44
50
  }
@@ -52,7 +58,10 @@ export function rebindNeedsProjectOnPrompt(input) {
52
58
  const reason = refined.reason;
53
59
  const nowIso = (input.now ?? (() => new Date()))().toISOString();
54
60
  const updated = updateSessionState(input.sessionId, input.root, (current) => {
55
- if (!current || current.contextStatus !== "needs_project")
61
+ if (!current || (current.contextStatus !== "needs_project" && current.contextStatus !== "unresolved")) {
62
+ return current;
63
+ }
64
+ if (current.contextStatus === "unresolved" && !current.companyUid)
56
65
  return current;
57
66
  if (current.projectId && current.projectId !== projectId)
58
67
  return current;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.146.0",
3
+ "version": "5.147.0",
4
4
  "description": "HQ by Indigo management CLI \u2014 modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {