@indigoai-us/hq-cli 5.101.5 → 5.101.6

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
@@ -1,5 +1,27 @@
1
1
  # Changelog
2
2
 
3
+ ## [Unreleased]
4
+
5
+ ## [5.101.6] — 2026-08-17
6
+
7
+ ### Added
8
+
9
+ - **`hq mesh`** — native work-mesh verbs in the CLI (Cognito + hq-pro REST +
10
+ `~/.hq/work-mesh/cache`). No pack helper required. Verbs: `check`
11
+ (`status` / `projects`), `start`, `progress`, `blocked`, `done`, `note`,
12
+ `story`, `doctor` (warms directory + inbox + pair DMs). Does **not** start
13
+ MQTT listen and is not `hq doctor` (hook guardrails). `--apply` Board PUTs
14
+ from local prd.json are not in the CLI yet.
15
+
16
+ ### Fixed
17
+
18
+ - `hq mesh doctor` now keys `directory` / `inbox` / `contacts` on the real
19
+ `prs_*` / `agt_*` principal. `custom:entityUid` is read from the Cognito ID
20
+ token first; if the JWT has only email/sub (typical human access tokens),
21
+ doctor falls back to `/v1/realtime/credentials` and keeps only the uid —
22
+ AWS IoT creds are discarded. Leftover `session.json` is removed once the
23
+ real principal is known.
24
+
3
25
  ## [5.101.5] — 2026-08-17
4
26
 
5
27
  - No user-facing changes recorded.
@@ -0,0 +1,12 @@
1
+ /**
2
+ * `hq mesh` — work-mesh verbs for agents and local sessions.
3
+ *
4
+ * Native hq-cli: Cognito + hq-pro REST + ~/.hq/work-mesh/cache. Does not
5
+ * require hq-pack-work-mesh on disk and does not start MQTT listen.
6
+ * Distinct from `hq doctor` (hook guardrails).
7
+ */
8
+ import { Command } from "commander";
9
+ import { type MeshCompany, type MeshThread } from "../lib/mesh/api.js";
10
+ export declare function formatCheckLines(threads: MeshThread[], company: MeshCompany, projectId?: string): string[];
11
+ export declare function registerMeshCommand(program: Command): void;
12
+ //# sourceMappingURL=mesh.d.ts.map
@@ -0,0 +1,173 @@
1
+ /**
2
+ * `hq mesh` — work-mesh verbs for agents and local sessions.
3
+ *
4
+ * Native hq-cli: Cognito + hq-pro REST + ~/.hq/work-mesh/cache. Does not
5
+ * require hq-pack-work-mesh on disk and does not start MQTT listen.
6
+ * Distinct from `hq doctor` (hook guardrails).
7
+ */
8
+ import chalk from "chalk";
9
+ import { loadCachedTokens } from "../utils/cognito-session.js";
10
+ import { STORY_STATUSES, appendThreadEvent, callerLabelFromToken, ensureProjectThread, eventPayload, listActiveThreads, patchStoryStatus, requireToken, resolveMeshCompany, resolveMeshPrincipalUid, warmMeshConversationCache, } from "../lib/mesh/api.js";
11
+ export function formatCheckLines(threads, company, projectId) {
12
+ if (threads.length === 0) {
13
+ return ["Work mesh: no active project threads found."];
14
+ }
15
+ const scope = projectId
16
+ ? `${company.companySlug || company.companyUid}/${projectId}`
17
+ : company.companySlug || company.companyUid;
18
+ const lines = [`Work mesh: ${threads.length} active thread(s) for ${scope}`];
19
+ for (const thread of threads.slice(0, 8)) {
20
+ const owner = thread.ownerUid ? ` owner=${thread.ownerUid}` : "";
21
+ const summary = thread.progressSummary ||
22
+ thread.sourceSignalSummary ||
23
+ thread.blockedReason ||
24
+ "";
25
+ lines.push(`- ${thread.threadStatus ?? "unknown"} ${thread.threadId ?? "?"}${owner}${summary ? `: ${summary}` : ""}`);
26
+ }
27
+ return lines;
28
+ }
29
+ function fail(message) {
30
+ console.error(chalk.red(message));
31
+ process.exit(1);
32
+ }
33
+ function requireProject(opts) {
34
+ const project = opts.project?.trim();
35
+ if (!project)
36
+ fail("`--project <slug>` is required.");
37
+ return project;
38
+ }
39
+ async function withCompany(opts) {
40
+ const token = await requireToken();
41
+ const company = await resolveMeshCompany(token, opts.company);
42
+ return { token, company };
43
+ }
44
+ async function runCheck(opts) {
45
+ const { token, company } = await withCompany(opts);
46
+ const threads = await listActiveThreads(token, company.companyUid, opts.project);
47
+ if (opts.json) {
48
+ console.log(JSON.stringify({ ok: true, action: "check", company, projectId: opts.project, threads }, null, 2));
49
+ return;
50
+ }
51
+ for (const line of formatCheckLines(threads, company, opts.project)) {
52
+ console.log(line);
53
+ }
54
+ }
55
+ async function runEvent(verb, opts) {
56
+ const { token, company } = await withCompany(opts);
57
+ const projectId = requireProject(opts);
58
+ const ensured = await ensureProjectThread(token, company, projectId, {
59
+ threadId: opts.threadId,
60
+ summary: opts.summary,
61
+ });
62
+ const eventKind = verb === "start" ? "claim" : verb;
63
+ const event = await appendThreadEvent(token, company.companyUid, ensured.threadId, eventKind, eventPayload(eventKind, opts, callerLabelFromToken(token)));
64
+ if (opts.json) {
65
+ console.log(JSON.stringify({
66
+ ok: true,
67
+ action: verb,
68
+ eventKind,
69
+ company,
70
+ projectId,
71
+ threadId: ensured.threadId,
72
+ created: ensured.created,
73
+ ...event,
74
+ }, null, 2));
75
+ return;
76
+ }
77
+ console.log(`Work mesh: ${verb} ${ensured.threadId}${event.eventId ? ` (${event.eventId})` : ""}`);
78
+ }
79
+ async function runStory(opts) {
80
+ const storyId = opts.story?.trim();
81
+ const status = opts.status?.trim();
82
+ if (!storyId || !status || !STORY_STATUSES.has(status)) {
83
+ fail("story requires --story <id> and --status queued|in_progress|review|done");
84
+ }
85
+ const { token, company } = await withCompany(opts);
86
+ const projectId = requireProject(opts);
87
+ const patched = await patchStoryStatus(token, company.companyUid, projectId, storyId, status);
88
+ if (opts.json) {
89
+ console.log(JSON.stringify({ ok: true, action: "story", company, ...patched, storyId }, null, 2));
90
+ return;
91
+ }
92
+ console.log(`Work mesh: story ${storyId} → ${patched.status} (v${patched.version ?? "?"})`);
93
+ }
94
+ async function runDoctor(opts) {
95
+ if (opts.apply) {
96
+ fail("`hq mesh doctor --apply` (paced Board PUTs from local prd.json) is not in the CLI yet.\n" +
97
+ "Omit --apply to warm the conversation cache (directory, inbox, pair DMs).");
98
+ }
99
+ const token = await requireToken();
100
+ const principalUid = await resolveMeshPrincipalUid(token, loadCachedTokens());
101
+ const warmed = await warmMeshConversationCache(token, principalUid);
102
+ if (opts.json) {
103
+ console.log(JSON.stringify({ ok: true, action: "doctor", ...warmed }, null, 2));
104
+ return;
105
+ }
106
+ console.log(`Work mesh cache warmed at ${warmed.cacheRoot}` +
107
+ ` (directory=${warmed.directory} inbox=${warmed.inbox}` +
108
+ ` contacts=${warmed.contacts} pair-threads=${warmed.threads})`);
109
+ }
110
+ function wrap(action) {
111
+ return async () => {
112
+ try {
113
+ await action();
114
+ }
115
+ catch (err) {
116
+ fail(err instanceof Error ? err.message : String(err));
117
+ }
118
+ };
119
+ }
120
+ function collectRepeatable(value, previous = []) {
121
+ return previous.concat(value);
122
+ }
123
+ function addSharedFlags(cmd) {
124
+ return cmd
125
+ .option("--company <slug|uid>", "Company slug or cloud uid")
126
+ .option("--project <slug>", "HQ project slug / projectId")
127
+ .option("--thread-id <id>", "Explicit work thread id")
128
+ .option("--json", "Print machine-readable JSON");
129
+ }
130
+ export function registerMeshCommand(program) {
131
+ const mesh = program
132
+ .command("mesh")
133
+ .description("Work mesh — register project work, report progress, and warm ~/.hq/work-mesh/cache");
134
+ addSharedFlags(mesh
135
+ .command("check")
136
+ .alias("status")
137
+ .alias("projects")
138
+ .description("Show active work-mesh threads for a company/project")).action((opts) => wrap(() => runCheck(opts))());
139
+ addSharedFlags(mesh
140
+ .command("start")
141
+ .description("Ensure a project thread exists and claim/report start")
142
+ .option("--summary <text>", "What you are starting")).action((opts) => wrap(() => runEvent("start", opts))());
143
+ addSharedFlags(mesh
144
+ .command("progress")
145
+ .description("Append a progress event to the project thread")
146
+ .requiredOption("--summary <text>", "Progress summary")).action((opts) => wrap(() => runEvent("progress", opts))());
147
+ addSharedFlags(mesh
148
+ .command("blocked")
149
+ .description("Append a blocked event to the project thread")
150
+ .option("--reason <text>", "Why you are blocked")
151
+ .option("--ask <text>", "Repeatable ask", collectRepeatable)
152
+ .option("--summary <text>", "Optional summary")).action((opts) => wrap(() => runEvent("blocked", opts))());
153
+ addSharedFlags(mesh
154
+ .command("done")
155
+ .description("Mark the project thread done")
156
+ .option("--summary <text>", "What shipped")).action((opts) => wrap(() => runEvent("done", opts))());
157
+ addSharedFlags(mesh
158
+ .command("note")
159
+ .description("Append a note with no status change")
160
+ .option("--summary <text>", "Note text")).action((opts) => wrap(() => runEvent("note", opts))());
161
+ addSharedFlags(mesh
162
+ .command("story")
163
+ .description("PATCH one Board story status")
164
+ .requiredOption("--story <id>", "Story id, e.g. US-001")
165
+ .requiredOption("--status <status>", "queued|in_progress|review|done")).action((opts) => wrap(() => runStory(opts))());
166
+ mesh
167
+ .command("doctor")
168
+ .description("Warm ~/.hq/work-mesh/cache from hq-pro (directory, inbox, pair DMs). Not `hq doctor`.")
169
+ .option("--apply", "Reserved — Board PUTs from local prd are not in the CLI yet")
170
+ .option("--json", "Print machine-readable JSON")
171
+ .action((opts) => wrap(() => runDoctor(opts))());
172
+ }
173
+ //# sourceMappingURL=mesh.js.map
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Native work-mesh client for `hq mesh`.
3
+ *
4
+ * Talks to hq-pro over the shared vault fetch + Cognito session. Writes the
5
+ * machine cache apps already read. Does not spawn the pack helper and does
6
+ * not start MQTT listen.
7
+ */
8
+ export declare const STORY_STATUSES: Set<string>;
9
+ export declare const ACTIVE_STATUSES: Set<string>;
10
+ export interface MeshCompany {
11
+ companyUid: string;
12
+ companySlug: string;
13
+ }
14
+ export interface MeshThread {
15
+ threadId?: string;
16
+ projectId?: string;
17
+ companyUid?: string;
18
+ threadStatus?: string;
19
+ ownerUid?: string;
20
+ progressSummary?: string;
21
+ sourceSignalSummary?: string;
22
+ blockedReason?: string;
23
+ lastActivityAt?: string;
24
+ createdAt?: string;
25
+ }
26
+ export declare function meshJson(token: string, path: string, init?: {
27
+ method?: string;
28
+ body?: Record<string, unknown>;
29
+ query?: Record<string, string>;
30
+ }): Promise<unknown>;
31
+ export declare function resolveMeshCompany(token: string, company?: string): Promise<MeshCompany>;
32
+ export declare function requireToken(): Promise<string>;
33
+ export declare function listActiveThreads(token: string, companyUid: string, projectId?: string): Promise<MeshThread[]>;
34
+ export declare function clamp(value: string, max: number): string;
35
+ export declare function eventPayload(kind: string, opts: {
36
+ summary?: string;
37
+ reason?: string;
38
+ ask?: string[];
39
+ }, claimedBy: string): Record<string, unknown>;
40
+ export declare function ensureProjectThread(token: string, company: MeshCompany, projectId: string, opts: {
41
+ threadId?: string;
42
+ summary?: string;
43
+ }): Promise<{
44
+ threadId: string;
45
+ created: boolean;
46
+ }>;
47
+ export declare function appendThreadEvent(token: string, companyUid: string, threadId: string, eventKind: string, payload: Record<string, unknown>): Promise<{
48
+ eventId?: string;
49
+ createdAt?: string;
50
+ }>;
51
+ export declare function patchStoryStatus(token: string, companyUid: string, projectId: string, storyId: string, status: string): Promise<{
52
+ status: string;
53
+ version?: number;
54
+ projectId: string;
55
+ }>;
56
+ export declare function pairDmUidsFromInbox(inbox: unknown): string[];
57
+ export interface WarmCacheResult {
58
+ directory: boolean;
59
+ inbox: boolean;
60
+ contacts: boolean;
61
+ threads: number;
62
+ cacheRoot: string;
63
+ }
64
+ /** Warm inbox + contacts + pair threads + directory into the local cache. */
65
+ export declare function warmMeshConversationCache(token: string, principalUid: string, cacheRoot?: string): Promise<WarmCacheResult>;
66
+ export declare function entityUidFromJwt(token: string): string | undefined;
67
+ export declare function principalUidFromTopic(topic: unknown): string | undefined;
68
+ /** Extract prs_/agt_ from a realtime credentials payload. Never returns secrets. */
69
+ export declare function principalUidFromRealtimePayload(data: unknown): string | undefined;
70
+ /**
71
+ * Cache key for directory/inbox/contacts. `custom:entityUid` rides the ID
72
+ * token (access tokens only have email/sub), so prefer the cached idToken.
73
+ */
74
+ export declare function meshCachePrincipalUid(tokens: {
75
+ idToken?: string;
76
+ accessToken?: string;
77
+ } | null | undefined, fallbackToken?: string): string;
78
+ /**
79
+ * Resolve the cache principal. JWT first; if the human access/id tokens have
80
+ * no entityUid, ask `/v1/realtime/credentials` (same fallback as the pack
81
+ * doctor). Only the uid is kept — credentials are discarded.
82
+ */
83
+ export declare function resolveMeshPrincipalUid(token: string, tokens?: {
84
+ idToken?: string;
85
+ accessToken?: string;
86
+ } | null): Promise<string>;
87
+ export declare function callerLabelFromToken(token: string): string;
88
+ //# sourceMappingURL=api.d.ts.map
@@ -0,0 +1,319 @@
1
+ /**
2
+ * Native work-mesh client for `hq mesh`.
3
+ *
4
+ * Talks to hq-pro over the shared vault fetch + Cognito session. Writes the
5
+ * machine cache apps already read. Does not spawn the pack helper and does
6
+ * not start MQTT listen.
7
+ */
8
+ import { ensureCognitoToken } from "../../utils/cognito-session.js";
9
+ import { peekIdToken } from "../../utils/id-token.js";
10
+ import { getCompanyUid, vaultApiFetch } from "../../utils/vault-api.js";
11
+ import { meshCacheRoot, removeSessionFallbackFiles, writeMeshCacheFile, } from "./cache.js";
12
+ const PRINCIPAL_UID = /^(prs|agt)_/;
13
+ export const STORY_STATUSES = new Set([
14
+ "queued",
15
+ "in_progress",
16
+ "review",
17
+ "done",
18
+ ]);
19
+ export const ACTIVE_STATUSES = new Set([
20
+ "open",
21
+ "claimed",
22
+ "in-progress",
23
+ "blocked",
24
+ "needs-human",
25
+ ]);
26
+ export async function meshJson(token, path, init = {}) {
27
+ const res = await vaultApiFetch({
28
+ token,
29
+ path,
30
+ method: init.method,
31
+ body: init.body,
32
+ query: init.query,
33
+ });
34
+ const text = await res.text();
35
+ let data = {};
36
+ if (text) {
37
+ try {
38
+ data = JSON.parse(text);
39
+ }
40
+ catch {
41
+ data = { raw: text };
42
+ }
43
+ }
44
+ if (!res.ok) {
45
+ const err = data;
46
+ throw new Error(err.error || err.message || `${res.status} ${res.statusText}`);
47
+ }
48
+ return data;
49
+ }
50
+ export async function resolveMeshCompany(token, company) {
51
+ const explicit = process.env.HQ_WORK_MESH_COMPANY_UID || process.env.HQ_COMPANY_UID;
52
+ if (explicit?.trim()) {
53
+ return { companyUid: explicit.trim(), companySlug: company || explicit.trim() };
54
+ }
55
+ if (company && /^(cmp|co)_/.test(company)) {
56
+ return { companyUid: company, companySlug: company };
57
+ }
58
+ const uid = await getCompanyUid(token, company);
59
+ return { companyUid: uid, companySlug: company || uid };
60
+ }
61
+ export async function requireToken() {
62
+ return ensureCognitoToken();
63
+ }
64
+ function asRecord(value) {
65
+ return value && typeof value === "object" && !Array.isArray(value)
66
+ ? value
67
+ : null;
68
+ }
69
+ export async function listActiveThreads(token, companyUid, projectId) {
70
+ const collected = [];
71
+ for (const status of ACTIVE_STATUSES) {
72
+ let cursor = "";
73
+ for (let page = 0; page < 10; page += 1) {
74
+ const query = {
75
+ companyUid,
76
+ status,
77
+ limit: "100",
78
+ };
79
+ if (cursor)
80
+ query.cursor = cursor;
81
+ const data = asRecord(await meshJson(token, "/v1/work-mesh/threads", { query }));
82
+ const rows = Array.isArray(data?.threads)
83
+ ? data.threads
84
+ : Array.isArray(data?.items)
85
+ ? data.items
86
+ : [];
87
+ for (const row of rows) {
88
+ const rec = asRecord(row);
89
+ if (!rec)
90
+ continue;
91
+ collected.push(rec);
92
+ }
93
+ cursor = typeof data?.nextCursor === "string" ? data.nextCursor : "";
94
+ if (!cursor)
95
+ break;
96
+ }
97
+ }
98
+ return collected
99
+ .filter((thread) => !projectId || thread.projectId === projectId)
100
+ .sort((a, b) => String(b.lastActivityAt || b.createdAt || "").localeCompare(String(a.lastActivityAt || a.createdAt || "")));
101
+ }
102
+ export function clamp(value, max) {
103
+ return value.length <= max ? value : value.slice(0, max);
104
+ }
105
+ export function eventPayload(kind, opts, claimedBy) {
106
+ if (kind === "claim") {
107
+ return {
108
+ claimedBy: clamp(claimedBy, 120),
109
+ leaseTtlIso: new Date(Date.now() + 120 * 60 * 1000).toISOString(),
110
+ note: clamp(opts.summary || "Starting HQ project work.", 280),
111
+ };
112
+ }
113
+ if (kind === "progress") {
114
+ return { summary: clamp(opts.summary || "Project work is in progress.", 280) };
115
+ }
116
+ if (kind === "blocked") {
117
+ return {
118
+ reason: clamp(opts.reason || opts.summary || "Project work is blocked.", 500),
119
+ asks: (opts.ask ?? []).filter(Boolean).slice(0, 5),
120
+ };
121
+ }
122
+ if (kind === "done") {
123
+ return { summary: clamp(opts.summary || "Project work completed.", 280) };
124
+ }
125
+ return {
126
+ text: clamp(opts.summary || "HQ project note.", 500),
127
+ noteKind: "audit",
128
+ };
129
+ }
130
+ export async function ensureProjectThread(token, company, projectId, opts) {
131
+ if (opts.threadId)
132
+ return { threadId: opts.threadId, created: false };
133
+ const existing = (await listActiveThreads(token, company.companyUid, projectId))[0];
134
+ if (existing?.threadId)
135
+ return { threadId: existing.threadId, created: false };
136
+ const created = asRecord(await meshJson(token, "/v1/work-mesh/threads", {
137
+ method: "POST",
138
+ body: {
139
+ companyUid: company.companyUid,
140
+ projectId,
141
+ sourceSignalSummary: clamp(opts.summary || `HQ project ${projectId}`, 280),
142
+ routing: {
143
+ priority: "normal",
144
+ tags: ["hq-project", `project:${projectId}`],
145
+ },
146
+ },
147
+ }));
148
+ const threadId = String(created?.threadId || "").trim();
149
+ if (!threadId)
150
+ throw new Error("Work mesh did not return a threadId");
151
+ return { threadId, created: true };
152
+ }
153
+ export async function appendThreadEvent(token, companyUid, threadId, eventKind, payload) {
154
+ const data = asRecord(await meshJson(token, `/v1/work-mesh/threads/${encodeURIComponent(threadId)}/events`, {
155
+ method: "POST",
156
+ body: { companyUid, eventKind, payload },
157
+ }));
158
+ return {
159
+ eventId: typeof data?.eventId === "string" ? data.eventId : undefined,
160
+ createdAt: typeof data?.createdAt === "string" ? data.createdAt : undefined,
161
+ };
162
+ }
163
+ export async function patchStoryStatus(token, companyUid, projectId, storyId, status) {
164
+ const view = asRecord(await meshJson(token, `/v1/work-mesh/projects/${encodeURIComponent(projectId)}/stories/${encodeURIComponent(storyId)}`, {
165
+ method: "PATCH",
166
+ body: { companyUid, status },
167
+ }));
168
+ if (view && typeof view.projectId === "string" && typeof view.companyUid === "string") {
169
+ writeMeshCacheFile(["projects", view.companyUid, `${view.projectId}.json`], view);
170
+ }
171
+ const stories = Array.isArray(view?.stories) ? view.stories : [];
172
+ const patched = stories
173
+ .map(asRecord)
174
+ .find((row) => row && row.id === storyId);
175
+ return {
176
+ projectId: String(view?.projectId || projectId),
177
+ status: typeof patched?.status === "string" ? patched.status : status,
178
+ version: typeof view?.version === "number" ? view.version : undefined,
179
+ };
180
+ }
181
+ export function pairDmUidsFromInbox(inbox) {
182
+ const rec = asRecord(inbox);
183
+ const ids = new Set();
184
+ const events = Array.isArray(rec?.events) ? rec.events : [];
185
+ for (const event of events) {
186
+ const row = asRecord(event);
187
+ const uid = String(row?.fromPersonUid || "").trim();
188
+ if (/^(prs|agt)_[A-Za-z0-9]+$/.test(uid))
189
+ ids.add(uid);
190
+ }
191
+ const pairs = Array.isArray(rec?.pairUnreads) ? rec.pairUnreads : [];
192
+ for (const pair of pairs) {
193
+ const row = asRecord(pair);
194
+ const uid = String(row?.withPersonUid || "").trim();
195
+ if (/^(prs|agt)_[A-Za-z0-9]+$/.test(uid))
196
+ ids.add(uid);
197
+ }
198
+ return [...ids].sort();
199
+ }
200
+ /** Warm inbox + contacts + pair threads + directory into the local cache. */
201
+ export async function warmMeshConversationCache(token, principalUid, cacheRoot = meshCacheRoot()) {
202
+ const result = {
203
+ directory: false,
204
+ inbox: false,
205
+ contacts: false,
206
+ threads: 0,
207
+ cacheRoot,
208
+ };
209
+ try {
210
+ const directory = await meshJson(token, "/v1/notify/channels", {
211
+ query: { cursor: "" },
212
+ });
213
+ writeMeshCacheFile(["directory", `${principalUid}.json`], directory, cacheRoot);
214
+ result.directory = true;
215
+ }
216
+ catch {
217
+ // Directory warm is best-effort; inbox is the pair-DM path.
218
+ }
219
+ const inbox = await meshJson(token, "/v1/notify/inbox", {
220
+ query: { limit: "50" },
221
+ });
222
+ writeMeshCacheFile(["inbox", `${principalUid}.json`], inbox, cacheRoot);
223
+ result.inbox = true;
224
+ try {
225
+ const contacts = await meshJson(token, "/v1/notify/contacts");
226
+ writeMeshCacheFile(["contacts", `${principalUid}.json`], contacts, cacheRoot);
227
+ result.contacts = true;
228
+ }
229
+ catch {
230
+ // Contacts roster is optional for the rail.
231
+ }
232
+ for (const uid of pairDmUidsFromInbox(inbox)) {
233
+ try {
234
+ const thread = await meshJson(token, "/v1/notify/thread", {
235
+ query: { withPersonUid: uid, limit: "50" },
236
+ });
237
+ writeMeshCacheFile(["dms", `${uid}.json`], thread, cacheRoot);
238
+ result.threads += 1;
239
+ }
240
+ catch {
241
+ // one pair must not fail the rest
242
+ }
243
+ }
244
+ if (PRINCIPAL_UID.test(principalUid)) {
245
+ removeSessionFallbackFiles(cacheRoot);
246
+ }
247
+ return result;
248
+ }
249
+ export function entityUidFromJwt(token) {
250
+ const uid = peekIdToken(token)["custom:entityUid"];
251
+ return typeof uid === "string" && PRINCIPAL_UID.test(uid) ? uid : undefined;
252
+ }
253
+ export function principalUidFromTopic(topic) {
254
+ if (typeof topic !== "string")
255
+ return undefined;
256
+ const match = /^hq\/((?:prs|agt)_[0-9A-HJKMNP-TV-Z]{26})(?:\/|$)/.exec(topic);
257
+ return match?.[1];
258
+ }
259
+ /** Extract prs_/agt_ from a realtime credentials payload. Never returns secrets. */
260
+ export function principalUidFromRealtimePayload(data) {
261
+ const rec = asRecord(data);
262
+ if (!rec)
263
+ return undefined;
264
+ const claimed = rec.principalUid;
265
+ if (typeof claimed === "string" && PRINCIPAL_UID.test(claimed))
266
+ return claimed;
267
+ const topics = asRecord(rec.topics);
268
+ return (principalUidFromTopic(topics?.dm) ||
269
+ principalUidFromTopic(rec.topic));
270
+ }
271
+ /**
272
+ * Cache key for directory/inbox/contacts. `custom:entityUid` rides the ID
273
+ * token (access tokens only have email/sub), so prefer the cached idToken.
274
+ */
275
+ export function meshCachePrincipalUid(tokens, fallbackToken) {
276
+ for (const token of [tokens?.idToken, tokens?.accessToken, fallbackToken]) {
277
+ if (!token)
278
+ continue;
279
+ const uid = entityUidFromJwt(token);
280
+ if (uid)
281
+ return uid;
282
+ }
283
+ return "session";
284
+ }
285
+ /**
286
+ * Resolve the cache principal. JWT first; if the human access/id tokens have
287
+ * no entityUid, ask `/v1/realtime/credentials` (same fallback as the pack
288
+ * doctor). Only the uid is kept — credentials are discarded.
289
+ */
290
+ export async function resolveMeshPrincipalUid(token, tokens) {
291
+ const fromJwt = meshCachePrincipalUid(tokens, token);
292
+ if (fromJwt !== "session")
293
+ return fromJwt;
294
+ try {
295
+ const data = await meshJson(token, "/v1/realtime/credentials", {
296
+ method: "POST",
297
+ body: { contractVersion: 2 },
298
+ });
299
+ const uid = principalUidFromRealtimePayload(data);
300
+ if (uid)
301
+ return uid;
302
+ }
303
+ catch {
304
+ // best-effort; session.json is worse than failing doctor
305
+ }
306
+ return "session";
307
+ }
308
+ export function callerLabelFromToken(token) {
309
+ const claims = peekIdToken(token);
310
+ const entityUid = claims["custom:entityUid"];
311
+ if (typeof entityUid === "string" && entityUid)
312
+ return entityUid;
313
+ if (typeof claims.email === "string" && claims.email)
314
+ return claims.email;
315
+ if (typeof claims.sub === "string" && claims.sub)
316
+ return claims.sub;
317
+ return "hq-cli";
318
+ }
319
+ //# sourceMappingURL=api.js.map
@@ -0,0 +1,10 @@
1
+ /**
2
+ * On-disk work-mesh cache. Same layout the desktop/web apps and the pack
3
+ * helper read: ~/.hq/work-mesh/cache/{projects,channels,directory,inbox,contacts,dms}.
4
+ */
5
+ export declare function meshCacheRoot(home?: string, env?: NodeJS.ProcessEnv): string;
6
+ export declare function isSafeCacheSegment(value: string): boolean;
7
+ export declare function writeMeshCacheFile(segments: string[], value: unknown, root?: string): string | null;
8
+ /** Drop leftover session.json once we know the real prs_/agt_ cache key. */
9
+ export declare function removeSessionFallbackFiles(root?: string): void;
10
+ //# sourceMappingURL=cache.d.ts.map
@@ -0,0 +1,42 @@
1
+ /**
2
+ * On-disk work-mesh cache. Same layout the desktop/web apps and the pack
3
+ * helper read: ~/.hq/work-mesh/cache/{projects,channels,directory,inbox,contacts,dms}.
4
+ */
5
+ import * as fs from "node:fs";
6
+ import * as os from "node:os";
7
+ import * as path from "node:path";
8
+ export function meshCacheRoot(home = os.homedir(), env = process.env) {
9
+ const override = env.HQ_WORK_MESH_CACHE?.trim();
10
+ if (override)
11
+ return override;
12
+ return path.join(home, ".hq", "work-mesh", "cache");
13
+ }
14
+ export function isSafeCacheSegment(value) {
15
+ const trimmed = value.trim();
16
+ return Boolean(trimmed &&
17
+ trimmed.length <= 128 &&
18
+ !trimmed.includes("..") &&
19
+ /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(trimmed));
20
+ }
21
+ export function writeMeshCacheFile(segments, value, root = meshCacheRoot()) {
22
+ if (segments.length === 0 || !segments.every(isSafeCacheSegment))
23
+ return null;
24
+ const dest = path.join(root, ...segments);
25
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
26
+ const tmp = `${dest}.tmp`;
27
+ fs.writeFileSync(tmp, `${JSON.stringify(value, null, 2)}\n`);
28
+ fs.renameSync(tmp, dest);
29
+ return dest;
30
+ }
31
+ /** Drop leftover session.json once we know the real prs_/agt_ cache key. */
32
+ export function removeSessionFallbackFiles(root = meshCacheRoot()) {
33
+ for (const dir of ["directory", "inbox", "contacts"]) {
34
+ try {
35
+ fs.unlinkSync(path.join(root, dir, "session.json"));
36
+ }
37
+ catch {
38
+ // missing is fine
39
+ }
40
+ }
41
+ }
42
+ //# sourceMappingURL=cache.js.map
package/dist/main.js CHANGED
@@ -61,6 +61,7 @@ import { registerCoreCommands } from "./commands/core.js";
61
61
  import { registerSearchCommand } from "./commands/search.js";
62
62
  import { registerIndexCommand } from "./commands/index-cmd.js";
63
63
  import { registerDoctorCommand } from "./commands/doctor.js";
64
+ import { registerMeshCommand } from "./commands/mesh.js";
64
65
  import { sanitizeArgv } from "./utils/feedback-diagnostics.js";
65
66
  import { environmentalFsErrorMessage } from "./utils/environmental-error.js";
66
67
  import { networkTransportErrorMessage } from "./utils/network-transport-error.js";
@@ -274,6 +275,9 @@ registerIndexCommand(program);
274
275
  // check registry so later check families (vault, sync, MCP, …) plug in without
275
276
  // engine changes.
276
277
  registerDoctorCommand(program);
278
+ // Work mesh (subcommand group — `hq mesh …`). Native REST + cache. Distinct
279
+ // from `hq doctor` (hook guardrails). Does not start MQTT listen.
280
+ registerMeshCommand(program);
277
281
  program.hook("preAction", async () => {
278
282
  await emitCliSessionStarted();
279
283
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.101.5",
3
+ "version": "5.101.6",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -24,7 +24,8 @@
24
24
  "test:e2e": "vitest run --config vitest.e2e.config.ts",
25
25
  "coverage": "vitest run --coverage --coverage.reporter=text-summary --coverage.reporter=json-summary",
26
26
  "vitest": "vitest",
27
- "clean": "rm -rf dist"
27
+ "clean": "rm -rf dist",
28
+ "prepare": "husky || true"
28
29
  },
29
30
  "dependencies": {
30
31
  "@aws-sdk/client-iot-data-plane": "^3.1096.0",
@@ -52,6 +53,8 @@
52
53
  "@vitest/coverage-v8": "4.1.6",
53
54
  "aws-sdk-client-mock": "^4.1.0",
54
55
  "eslint": "^10.5.0",
56
+ "husky": "^9.1.7",
57
+ "lint-staged": "^15.5.2",
55
58
  "typescript": "^5.7.0",
56
59
  "typescript-eslint": "^8.61.1",
57
60
  "vitest": "^4.1.2"
@@ -77,5 +80,8 @@
77
80
  "onlyBuiltDependencies": [
78
81
  "better-sqlite3"
79
82
  ]
83
+ },
84
+ "lint-staged": {
85
+ "*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}": "eslint --cache --no-error-on-unmatched-pattern"
80
86
  }
81
87
  }