@indigoai-us/hq-cli 5.345.162 → 5.345.164

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 (35) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/dist/command-catalog.generated.d.ts +341 -1
  3. package/dist/command-catalog.generated.js +422 -1
  4. package/dist/command-registration-plan.d.ts +6 -0
  5. package/dist/command-registration-plan.js +1 -0
  6. package/dist/commands/billing-usage.d.ts +16 -2
  7. package/dist/commands/billing-usage.js +63 -9
  8. package/dist/commands/billing.d.ts +34 -0
  9. package/dist/commands/billing.js +108 -1
  10. package/dist/commands/deploy-domains.d.ts +53 -0
  11. package/dist/commands/deploy-domains.js +802 -0
  12. package/dist/commands/feedback-triage.d.ts +67 -0
  13. package/dist/commands/feedback-triage.js +294 -0
  14. package/dist/commands/feedback.d.ts +13 -1
  15. package/dist/commands/feedback.js +32 -8
  16. package/dist/lib/billing/billing-status-lines.d.ts +14 -0
  17. package/dist/lib/billing/billing-status-lines.js +43 -0
  18. package/dist/lib/billing/usage-only.d.ts +18 -0
  19. package/dist/lib/billing/usage-only.js +18 -1
  20. package/dist/lib/doctor/checks/sync-runtime.d.ts +34 -0
  21. package/dist/lib/doctor/checks/sync-runtime.js +217 -8
  22. package/dist/lib/doctor/fix/apply.js +1 -0
  23. package/dist/lib/doctor/fix/refresh-sync-outcome.d.ts +4 -1
  24. package/dist/lib/doctor/fix/refresh-sync-outcome.js +15 -0
  25. package/dist/lib/doctor/fix/refresh-sync-progress.d.ts +42 -0
  26. package/dist/lib/doctor/fix/refresh-sync-progress.js +100 -0
  27. package/dist/lib/doctor/json-output.d.ts +7 -0
  28. package/dist/lib/doctor/json-output.js +8 -1
  29. package/dist/lib/doctor/types.d.ts +1 -1
  30. package/dist/lib/doctor/types.js +1 -0
  31. package/dist/lib/mesh/live/daemon/work-subscribe.d.ts +1 -1
  32. package/dist/lib/mesh/live/daemon/work-subscribe.js +121 -38
  33. package/dist/lib/mesh/live/paths.d.ts +1 -0
  34. package/dist/lib/mesh/live/paths.js +1 -0
  35. package/package.json +7 -7
@@ -4,46 +4,67 @@
4
4
  */
5
5
  import * as fs from "node:fs";
6
6
  import * as path from "node:path";
7
+ import { randomUUID } from "node:crypto";
7
8
  import { createPersonalRealtimeFetcher, } from "./credentials.js";
8
9
  import { presignIotWssUrl } from "./presign.js";
9
10
  import { applyProjectViewPush, parseProjectViewPush } from "./live-cache.js";
10
11
  import { backoffDelayMs, RECONNECT_MIN_DELAY_MS } from "./presence.js";
11
- import { AGENT_INBOX_PUSH_STATE_BASENAME } from "../paths.js";
12
- export { AGENT_INBOX_PUSH_STATE_BASENAME } from "../paths.js";
12
+ import { AGENT_INBOX_PUSH_STATE_BASENAME, SYNC_WAKE_STATE_BASENAME } from "../paths.js";
13
+ export { AGENT_INBOX_PUSH_STATE_BASENAME, SYNC_WAKE_STATE_BASENAME } from "../paths.js";
13
14
  const INBOX_PUSH_HEARTBEAT_MS = 5_000;
14
- function readInboxWakeSequence(meshRoot, onInfo) {
15
- const target = path.join(meshRoot, AGENT_INBOX_PUSH_STATE_BASENAME);
15
+ function readWakeSequence(meshRoot, basename, label, onInfo) {
16
+ const target = path.join(meshRoot, basename);
16
17
  try {
17
18
  const info = fs.lstatSync(target);
18
19
  if (!info.isFile() || info.isSymbolicLink()) {
19
- onInfo?.("agent inbox push state read ignored (not a regular file)");
20
- return 0;
20
+ onInfo?.(`${label} state read ignored (not a regular file)`);
21
+ return { sequence: 0, writable: false };
21
22
  }
22
23
  const parsed = JSON.parse(fs.readFileSync(target, "utf8"));
23
24
  if (Number.isSafeInteger(parsed.wakeSequence) && Number(parsed.wakeSequence) >= 0) {
24
- return Number(parsed.wakeSequence);
25
+ return { sequence: Number(parsed.wakeSequence), writable: true };
25
26
  }
26
- onInfo?.("agent inbox push state read ignored (invalid wake sequence)");
27
- return 0;
27
+ onInfo?.(`${label} state read ignored (invalid wake sequence)`);
28
+ return { sequence: 0, writable: false };
28
29
  }
29
30
  catch (error) {
30
31
  if (error.code !== "ENOENT") {
31
32
  const name = error instanceof Error ? error.name : "UnknownError";
32
- onInfo?.(`agent inbox push state read failed (${name})`);
33
+ onInfo?.(`${label} state read failed (${name})`);
33
34
  }
34
- return 0;
35
+ return { sequence: 0, writable: error.code === "ENOENT" };
35
36
  }
36
37
  }
37
- function writeInboxPushState(meshRoot, state) {
38
+ function writeWakeState(meshRoot, basename, state, durable) {
38
39
  fs.mkdirSync(meshRoot, { recursive: true, mode: 0o700 });
39
- const target = path.join(meshRoot, AGENT_INBOX_PUSH_STATE_BASENAME);
40
- const temporary = `${target}.${process.pid}.tmp`;
40
+ const rootInfo = fs.lstatSync(meshRoot);
41
+ if (!rootInfo.isDirectory() || rootInfo.isSymbolicLink()) {
42
+ throw new Error("wake state root is not a regular directory");
43
+ }
44
+ const target = path.join(meshRoot, basename);
45
+ const temporary = path.join(meshRoot, `.${basename}.${process.pid}.${randomUUID()}.tmp`);
46
+ let file;
41
47
  try {
42
- fs.writeFileSync(temporary, `${JSON.stringify(state)}\n`, { mode: 0o600 });
43
- fs.chmodSync(temporary, 0o600);
48
+ file = fs.openSync(temporary, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_NOFOLLOW, 0o600);
49
+ fs.writeFileSync(file, `${JSON.stringify(state)}\n`);
50
+ if (durable)
51
+ fs.fsyncSync(file);
52
+ fs.closeSync(file);
53
+ file = undefined;
44
54
  fs.renameSync(temporary, target);
55
+ if (durable && process.platform !== "win32") {
56
+ const directory = fs.openSync(meshRoot, fs.constants.O_RDONLY);
57
+ try {
58
+ fs.fsyncSync(directory);
59
+ }
60
+ finally {
61
+ fs.closeSync(directory);
62
+ }
63
+ }
45
64
  }
46
65
  catch (error) {
66
+ if (file !== undefined)
67
+ fs.closeSync(file);
47
68
  try {
48
69
  fs.unlinkSync(temporary);
49
70
  }
@@ -79,37 +100,46 @@ export async function startWorkCacheSubscriber(opts) {
79
100
  let heartbeatTimer = null;
80
101
  let generation = 0;
81
102
  let inboxSubscribed = false;
82
- let inboxAgentUid;
83
- // Carry the sequence forward from the last state marker so a daemon restart
84
- // cannot reuse a value the watcher has already consumed.
85
- let wakeSequence = readInboxWakeSequence(opts.meshRoot, opts.onInfo);
103
+ let syncSubscribed = false;
104
+ let actorUid;
105
+ let inboxWakeSequence = readWakeSequence(opts.meshRoot, AGENT_INBOX_PUSH_STATE_BASENAME, "agent inbox push", opts.onInfo).sequence;
106
+ const syncWakeState = readWakeSequence(opts.meshRoot, SYNC_WAKE_STATE_BASENAME, "sync wake", opts.onInfo);
107
+ let syncWakeSequence = syncWakeState.sequence;
108
+ const syncStateWritable = syncWakeState.writable;
86
109
  const clearInboxHeartbeat = () => {
87
110
  if (heartbeatTimer !== null)
88
111
  timers.clearTimeout(heartbeatTimer);
89
112
  heartbeatTimer = null;
90
113
  };
91
- const persistInboxState = (connected) => {
114
+ const persistState = (basename, label, connected, wakeSequence, identity, durable = true) => {
92
115
  try {
93
- writeInboxPushState(opts.meshRoot, {
116
+ writeWakeState(opts.meshRoot, basename, {
94
117
  connected,
95
118
  heartbeatAtMs: now().getTime(),
96
119
  wakeSequence,
97
- ...(connected && inboxAgentUid ? { agentUid: inboxAgentUid } : {}),
98
- });
120
+ ...(connected && actorUid && identity ? { [identity]: actorUid } : {}),
121
+ }, durable);
99
122
  }
100
123
  catch (error) {
101
124
  const name = error instanceof Error ? error.name : "UnknownError";
102
- opts.onInfo?.(`agent inbox push state write failed (${name})`);
125
+ opts.onInfo?.(`${label} state write failed (${name})`);
103
126
  }
104
127
  };
128
+ const persistInboxState = (connected, durable = true) => persistState(AGENT_INBOX_PUSH_STATE_BASENAME, "agent inbox push", connected, inboxWakeSequence, "agentUid", durable);
129
+ const persistSyncState = (connected, durable = true) => syncStateWritable
130
+ ? persistState(SYNC_WAKE_STATE_BASENAME, "sync wake", connected, syncWakeSequence, undefined, durable)
131
+ : undefined;
105
132
  const scheduleInboxHeartbeat = () => {
106
- if (stopped || !inboxSubscribed)
133
+ if (stopped || (!inboxSubscribed && !syncSubscribed))
107
134
  return;
108
135
  clearInboxHeartbeat();
109
136
  heartbeatTimer = timers.setTimeout(() => {
110
137
  heartbeatTimer = null;
111
- if (!stopped && inboxSubscribed) {
112
- persistInboxState(true);
138
+ if (!stopped && (inboxSubscribed || syncSubscribed)) {
139
+ if (inboxSubscribed)
140
+ persistInboxState(true, false);
141
+ if (syncSubscribed)
142
+ persistSyncState(true, false);
113
143
  scheduleInboxHeartbeat();
114
144
  }
115
145
  }, INBOX_PUSH_HEARTBEAT_MS);
@@ -118,9 +148,11 @@ export async function startWorkCacheSubscriber(opts) {
118
148
  if (stopped)
119
149
  return;
120
150
  inboxSubscribed = false;
121
- inboxAgentUid = undefined;
151
+ syncSubscribed = false;
152
+ actorUid = undefined;
122
153
  clearInboxHeartbeat();
123
154
  persistInboxState(false);
155
+ persistSyncState(false);
124
156
  const gen = ++generation;
125
157
  let bundle;
126
158
  try {
@@ -137,14 +169,15 @@ export async function startWorkCacheSubscriber(opts) {
137
169
  bundle.topics.inbox === `hq/${bundle.actorUid}/inbox`
138
170
  ? bundle.topics.inbox
139
171
  : undefined;
140
- inboxAgentUid = inboxTopic ? bundle.actorUid : undefined;
141
- if (!topic && !inboxTopic) {
172
+ const syncTopic = syncStateWritable ? `hq/${bundle.actorUid}/sync` : undefined;
173
+ actorUid = inboxTopic || syncTopic ? bundle.actorUid : undefined;
174
+ if (!topic && !inboxTopic && !syncTopic) {
142
175
  opts.onInfo?.("work cache subscribe skipped: credentials have no supported topic");
143
176
  return;
144
177
  }
145
178
  const url = presignIotWssUrl(bundle.credentials, bundle.iotEndpoint, bundle.region, now());
146
179
  const next = opts.mqttConnect(url, {
147
- clientId: `${bundle.clientId}-work`,
180
+ clientId: bundle.clientId,
148
181
  clean: true,
149
182
  reconnectPeriod: 0,
150
183
  protocolVersion: 4,
@@ -154,7 +187,8 @@ export async function startWorkCacheSubscriber(opts) {
154
187
  if (stopped || gen !== generation)
155
188
  return;
156
189
  reconnectLogged = false;
157
- let pendingSubscriptions = Number(Boolean(topic)) + Number(Boolean(inboxTopic));
190
+ let pendingSubscriptions = Number(Boolean(topic)) + Number(Boolean(inboxTopic)) +
191
+ Number(Boolean(syncTopic));
158
192
  let subscriptionFailed = false;
159
193
  const finishSubscription = (err) => {
160
194
  if (err) {
@@ -171,6 +205,13 @@ export async function startWorkCacheSubscriber(opts) {
171
205
  if (pendingSubscriptions === 0)
172
206
  attempt = 0;
173
207
  };
208
+ const finishOptionalSubscription = () => {
209
+ if (subscriptionFailed)
210
+ return;
211
+ pendingSubscriptions -= 1;
212
+ if (pendingSubscriptions === 0)
213
+ attempt = 0;
214
+ };
174
215
  if (topic) {
175
216
  next.subscribe(topic, { qos: 1 }, (err) => finishSubscription(err));
176
217
  }
@@ -180,7 +221,6 @@ export async function startWorkCacheSubscriber(opts) {
180
221
  return;
181
222
  if (err) {
182
223
  inboxSubscribed = false;
183
- inboxAgentUid = undefined;
184
224
  clearInboxHeartbeat();
185
225
  persistInboxState(false);
186
226
  opts.onInfo?.("agent inbox push subscribe failed");
@@ -193,17 +233,37 @@ export async function startWorkCacheSubscriber(opts) {
193
233
  finishSubscription();
194
234
  });
195
235
  }
236
+ if (syncTopic) {
237
+ next.subscribe(syncTopic, { qos: 1 }, (err) => {
238
+ if (stopped || gen !== generation)
239
+ return;
240
+ if (err) {
241
+ syncSubscribed = false;
242
+ persistSyncState(false);
243
+ opts.onInfo?.("sync wake subscribe failed");
244
+ finishOptionalSubscription();
245
+ return;
246
+ }
247
+ syncSubscribed = true;
248
+ persistSyncState(true);
249
+ scheduleInboxHeartbeat();
250
+ finishSubscription();
251
+ });
252
+ }
196
253
  });
197
254
  next.on("message", (_topic, payload) => {
198
255
  if (stopped || gen !== generation)
199
256
  return;
200
257
  if (inboxTopic && _topic === inboxTopic && inboxSubscribed) {
201
- // A topic message is a doorbell only. Never parse, persist, or log its
202
- // content; the authenticated inbox GET remains the source of truth.
203
- wakeSequence += 1;
258
+ inboxWakeSequence += 1;
204
259
  persistInboxState(true);
205
260
  return;
206
261
  }
262
+ if (syncTopic && _topic === syncTopic && syncSubscribed) {
263
+ syncWakeSequence += 1;
264
+ persistSyncState(true);
265
+ return;
266
+ }
207
267
  if (_topic !== topic)
208
268
  return;
209
269
  const push = parseProjectViewPush(payload);
@@ -214,13 +274,33 @@ export async function startWorkCacheSubscriber(opts) {
214
274
  opts.onInfo?.(`project-view cache cursor=${result.cursor} ${push.companyUid}/${push.projectId}`);
215
275
  }
216
276
  });
277
+ next.on("error", (_error) => {
278
+ if (stopped || gen !== generation)
279
+ return;
280
+ inboxSubscribed = false;
281
+ syncSubscribed = false;
282
+ actorUid = undefined;
283
+ clearInboxHeartbeat();
284
+ persistInboxState(false);
285
+ persistSyncState(false);
286
+ opts.onInfo?.("work cache mqtt error");
287
+ schedule();
288
+ try {
289
+ next.end(true);
290
+ }
291
+ catch {
292
+ return;
293
+ }
294
+ });
217
295
  next.on("close", () => {
218
296
  if (stopped || gen !== generation)
219
297
  return;
220
298
  inboxSubscribed = false;
221
- inboxAgentUid = undefined;
299
+ syncSubscribed = false;
300
+ actorUid = undefined;
222
301
  clearInboxHeartbeat();
223
302
  persistInboxState(false);
303
+ persistSyncState(false);
224
304
  schedule();
225
305
  });
226
306
  };
@@ -250,7 +330,10 @@ export async function startWorkCacheSubscriber(opts) {
250
330
  timers.clearTimeout(timer);
251
331
  clearInboxHeartbeat();
252
332
  inboxSubscribed = false;
333
+ syncSubscribed = false;
334
+ actorUid = undefined;
253
335
  persistInboxState(false);
336
+ persistSyncState(false);
254
337
  const current = client;
255
338
  client = null;
256
339
  if (!current)
@@ -4,6 +4,7 @@
4
4
  */
5
5
  /** Content-free sequence file written by the mesh MQTT inbox subscriber. */
6
6
  export declare const AGENT_INBOX_PUSH_STATE_BASENAME = "agent-inbox-push.json";
7
+ export declare const SYNC_WAKE_STATE_BASENAME = "sync-wake.json";
7
8
  export declare function workMeshRoot(home?: string, env?: NodeJS.ProcessEnv): string;
8
9
  export declare function workMeshSpoolPath(root?: string): string;
9
10
  export declare function workMeshHeldPath(root?: string): string;
@@ -7,6 +7,7 @@ import * as path from "node:path";
7
7
  const WORK_MESH_DIRNAME = "work-mesh";
8
8
  /** Content-free sequence file written by the mesh MQTT inbox subscriber. */
9
9
  export const AGENT_INBOX_PUSH_STATE_BASENAME = "agent-inbox-push.json";
10
+ export const SYNC_WAKE_STATE_BASENAME = "sync-wake.json";
10
11
  export function workMeshRoot(home = os.homedir(), env = process.env) {
11
12
  const override = env.HQ_WORK_MESH_ROOT?.trim();
12
13
  if (override)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.345.162",
3
+ "version": "5.345.164",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -114,11 +114,11 @@
114
114
  "*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}": "eslint --cache --no-error-on-unmatched-pattern"
115
115
  },
116
116
  "optionalDependencies": {
117
- "@indigoai-us/hq-hook-win32-x64": "5.345.162",
118
- "@indigoai-us/hq-hook-win32-arm64": "5.345.162",
119
- "@indigoai-us/hq-hook-darwin-x64": "5.345.162",
120
- "@indigoai-us/hq-hook-darwin-arm64": "5.345.162",
121
- "@indigoai-us/hq-hook-linux-x64": "5.345.162",
122
- "@indigoai-us/hq-hook-linux-arm64": "5.345.162"
117
+ "@indigoai-us/hq-hook-win32-x64": "5.345.164",
118
+ "@indigoai-us/hq-hook-win32-arm64": "5.345.164",
119
+ "@indigoai-us/hq-hook-darwin-x64": "5.345.164",
120
+ "@indigoai-us/hq-hook-darwin-arm64": "5.345.164",
121
+ "@indigoai-us/hq-hook-linux-x64": "5.345.164",
122
+ "@indigoai-us/hq-hook-linux-arm64": "5.345.164"
123
123
  }
124
124
  }