@indigoai-us/hq-cli 5.234.2 → 5.235.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.
@@ -1,138 +0,0 @@
1
- import { meshJson, requireToken } from "../mesh/client.js";
2
- import { LanesError } from "./errors.js";
3
- import { mapWorkMeshTrackerError } from "./tracker-mesh.js";
4
- /** Persist a local delivery checkpoint only after the Work Mesh stamp succeeds. */
5
- export async function deliverTrackerMeshQuestionOnce(question, company, client, persist) {
6
- if (!question.mesh_question_id || question.status !== "answered" ||
7
- !question.delivered_at || question.mesh_delivered_at)
8
- return question;
9
- const delivered = await client.delivered(question.mesh_question_id);
10
- if (delivered.questionId !== question.mesh_question_id ||
11
- delivered.companyUid !== company.uid ||
12
- delivered.laneId !== question.lane_id ||
13
- delivered.projectId !== question.project_id ||
14
- !delivered.deliveredAt) {
15
- throw new LanesError("tracker_mesh_response_invalid", "Work Mesh did not stamp the expected question as delivered", {
16
- mesh_question_id: question.mesh_question_id,
17
- question_id: question.question_id,
18
- });
19
- }
20
- return persist(delivered.deliveredAt);
21
- }
22
- const ROOT = "/v1/work-mesh/tracker/questions";
23
- const AUDIENCES = ["owner", "operator", "lane"];
24
- const STATUSES = ["pending", "answered", "cancelled"];
25
- const MAX_PAGES_PER_FILTER = 10;
26
- function invalidResponse(field) {
27
- throw new LanesError("tracker_mesh_response_invalid", `Work Mesh returned an invalid tracker question ${field}`, { field });
28
- }
29
- function parseQuestion(raw, company) {
30
- if (!raw || typeof raw !== "object" || Array.isArray(raw))
31
- return invalidResponse("row");
32
- const row = raw;
33
- if (typeof row.questionId !== "string" || !row.questionId ||
34
- typeof row.threadId !== "string" || !row.threadId ||
35
- row.companyUid !== company.uid ||
36
- typeof row.projectId !== "string" || !row.projectId ||
37
- typeof row.laneId !== "string" || !row.laneId ||
38
- !AUDIENCES.includes(row.audience) ||
39
- !STATUSES.includes(row.status) ||
40
- typeof row.question !== "string" ||
41
- !Array.isArray(row.options) || row.options.some((entry) => typeof entry !== "string") ||
42
- !Array.isArray(row.audienceChain) || row.audienceChain.some((entry) => typeof entry !== "string") ||
43
- !Array.isArray(row.routingHistory) ||
44
- typeof row.askedAt !== "string" || !Number.isFinite(Date.parse(row.askedAt)))
45
- return invalidResponse("row");
46
- if (row.context !== undefined && typeof row.context !== "string")
47
- return invalidResponse("context");
48
- if (row.recommended !== undefined && typeof row.recommended !== "string")
49
- return invalidResponse("recommended");
50
- if (row.choice !== undefined && typeof row.choice !== "string")
51
- return invalidResponse("choice");
52
- if (row.notes !== undefined && typeof row.notes !== "string")
53
- return invalidResponse("notes");
54
- if (row.answeredBy !== undefined && typeof row.answeredBy !== "string")
55
- return invalidResponse("answeredBy");
56
- if (row.answeredAt !== undefined && (typeof row.answeredAt !== "string" || !Number.isFinite(Date.parse(row.answeredAt))))
57
- return invalidResponse("answeredAt");
58
- if (row.deliveredAt !== undefined && (typeof row.deliveredAt !== "string" || !Number.isFinite(Date.parse(row.deliveredAt))))
59
- return invalidResponse("deliveredAt");
60
- if (!row.host || typeof row.host !== "object" || Array.isArray(row.host) || typeof row.host.kind !== "string" || typeof row.host.id !== "string")
61
- return invalidResponse("host");
62
- return row;
63
- }
64
- export function createTrackerMeshQuestionClient(options) {
65
- const { company } = options;
66
- if (!company.slug.trim() || !company.uid.trim()) {
67
- throw new LanesError("tracker_company_unresolved", "A resolved company slug and uid are required for Work Mesh tracker questions", {});
68
- }
69
- const request = options.request ?? (async (endpoint, init) => {
70
- const token = await requireToken({ interactive: false });
71
- return meshJson(token, endpoint, init);
72
- });
73
- const call = async (endpoint, init) => {
74
- try {
75
- const result = await request(endpoint, init);
76
- if (!result || typeof result !== "object" || Array.isArray(result))
77
- return invalidResponse("response");
78
- return result;
79
- }
80
- catch (err) {
81
- throw mapWorkMeshTrackerError(err);
82
- }
83
- };
84
- return {
85
- async create(input) {
86
- const result = await call(ROOT, { method: "POST", body: { companyUid: company.uid, ...input } });
87
- return parseQuestion(result.question, company);
88
- },
89
- async list(requestedStatuses = STATUSES) {
90
- const uniqueStatuses = [...new Set(requestedStatuses)];
91
- if (uniqueStatuses.some((status) => !STATUSES.includes(status))) {
92
- throw new LanesError("tracker_mesh_query_invalid", "Unsupported Work Mesh tracker question status filter", { statuses: uniqueStatuses });
93
- }
94
- const rows = new Map();
95
- for (const audience of AUDIENCES) {
96
- for (const status of uniqueStatuses) {
97
- let cursor;
98
- for (let page = 0; page < MAX_PAGES_PER_FILTER; page += 1) {
99
- const query = { companyUid: company.uid, audience, status, limit: "100", ...(cursor ? { cursor } : {}) };
100
- const result = await call(ROOT, { method: "GET", query });
101
- if (!Array.isArray(result.questions))
102
- return invalidResponse("questions list");
103
- for (const raw of result.questions) {
104
- const row = parseQuestion(raw, company);
105
- if (row.audience !== audience || row.status !== status)
106
- return invalidResponse("filter scope");
107
- rows.set(row.questionId, row);
108
- }
109
- if (result.nextCursor === undefined)
110
- break;
111
- if (typeof result.nextCursor !== "string" || !result.nextCursor || result.nextCursor === cursor)
112
- return invalidResponse("cursor");
113
- if (page === MAX_PAGES_PER_FILTER - 1) {
114
- throw new LanesError("tracker_mesh_response_incomplete", "Work Mesh tracker question list exceeded its bounded page limit", { audience, status, max_pages: MAX_PAGES_PER_FILTER });
115
- }
116
- cursor = result.nextCursor;
117
- }
118
- }
119
- }
120
- return [...rows.values()].sort((a, b) => b.askedAt.localeCompare(a.askedAt) || a.questionId.localeCompare(b.questionId));
121
- },
122
- async answer(questionId, choice, notes) {
123
- const result = await call(`${ROOT}/${encodeURIComponent(questionId)}/answer`, {
124
- method: "POST",
125
- body: { companyUid: company.uid, choice, ...(notes === undefined ? {} : { notes }) },
126
- });
127
- return parseQuestion(result.question, company);
128
- },
129
- async delivered(questionId) {
130
- const result = await call(`${ROOT}/${encodeURIComponent(questionId)}/delivered`, {
131
- method: "POST",
132
- body: { companyUid: company.uid },
133
- });
134
- return parseQuestion(result.question, company);
135
- },
136
- };
137
- }
138
- //# sourceMappingURL=tracker-mesh-questions.js.map
@@ -1,19 +0,0 @@
1
- import { LanesError } from "./errors.js";
2
- import type { LaneCompanyRef } from "./types.js";
3
- import type { TrackerStore } from "./tracker-store.js";
4
- export interface TrackerMeshRequestInit {
5
- method?: string;
6
- body?: Record<string, unknown>;
7
- query?: Record<string, string>;
8
- }
9
- export type TrackerMeshRequest = (path: string, init: TrackerMeshRequestInit) => Promise<unknown>;
10
- export interface WorkMeshTrackerStoreOptions {
11
- company: LaneCompanyRef;
12
- projectId: string;
13
- request?: TrackerMeshRequest;
14
- now?: () => Date;
15
- clientRecordId?: () => string;
16
- }
17
- export declare function mapWorkMeshTrackerError(err: unknown): LanesError;
18
- export declare function createWorkMeshTrackerStore(options: WorkMeshTrackerStoreOptions): TrackerStore;
19
- //# sourceMappingURL=tracker-mesh.d.ts.map
@@ -1,251 +0,0 @@
1
- import { randomUUID } from "node:crypto";
2
- import { meshJson, requireToken } from "../mesh/client.js";
3
- import { LanesError } from "./errors.js";
4
- const ROOT = "/v1/work-mesh/tracker/records";
5
- const MAX_RECORD_PAGES = 20;
6
- const RECORD_FIELDS = {
7
- blocker: ["story_id", "what", "waiting_on", "next_step", "opened_at", "resolved_at", "resolution", "import_key", "source_path", "source_item_id"],
8
- kpi: ["name", "definition", "unit", "target", "direction", "status", "approved_by", "approved_at", "values", "import_key", "source_path", "source_item_id"],
9
- okr: ["objective", "period", "key_results", "status", "approved_by", "approved_at", "import_key", "source_path", "source_item_id"],
10
- directive: ["text", "date", "source_url", "teams", "policy_path", "acknowledgements", "import_key", "source_path", "source_item_id"],
11
- };
12
- function fail(error, message, details = {}) {
13
- throw new LanesError(error, message, details);
14
- }
15
- export function mapWorkMeshTrackerError(err) {
16
- if (err instanceof LanesError)
17
- return err;
18
- const value = err;
19
- const status = typeof value?.status === "number" ? value.status : Number(/^\s*(\d{3})\b/.exec(err instanceof Error ? err.message : "")?.[1]);
20
- const code = typeof value?.code === "string" ? value.code : "";
21
- if (status === 404 && code === "tracker_records_disabled") {
22
- return new LanesError("tracker_records_disabled", "Work Mesh tracker records are disabled for this company", { status, code });
23
- }
24
- if (status === 404)
25
- return new LanesError("tracker_record_not_found", "Work Mesh tracker record was not found", { status, code });
26
- if (status === 409 && (code === "stale_version" || /stale.version/i.test(err instanceof Error ? err.message : ""))) {
27
- return new LanesError("tracker_version_conflict", "Work Mesh tracker record changed before this update", { status, code });
28
- }
29
- if (status === 409)
30
- return new LanesError("tracker_mesh_conflict", "Work Mesh rejected the tracker update because the record state changed", { status, code });
31
- if (status === 401 || status === 403)
32
- return new LanesError("tracker_mesh_auth_failed", "Work Mesh tracker access was denied for this company", { status, code });
33
- if (status === 400 || status === 422)
34
- return new LanesError("tracker_mesh_validation_failed", "Work Mesh rejected the tracker record data", { status, code });
35
- return new LanesError("tracker_mesh_unavailable", `Work Mesh tracker request failed: ${err instanceof Error ? err.message : String(err)}`, { status: Number.isFinite(status) ? status : undefined });
36
- }
37
- function toMesh(value) {
38
- if (Array.isArray(value))
39
- return value.map(toMesh);
40
- if (!value || typeof value !== "object")
41
- return value;
42
- return Object.fromEntries(Object.entries(value).map(([key, item]) => [
43
- key.replace(/_([a-z])/g, (_match, letter) => letter.toUpperCase()),
44
- toMesh(item),
45
- ]));
46
- }
47
- function fromMesh(value) {
48
- if (Array.isArray(value))
49
- return value.map(fromMesh);
50
- if (!value || typeof value !== "object")
51
- return value;
52
- return Object.fromEntries(Object.entries(value).map(([key, item]) => [
53
- key.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`),
54
- fromMesh(item),
55
- ]));
56
- }
57
- function recordFromMeta(raw, company, projectId, expectedKind) {
58
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
59
- return fail("tracker_mesh_response_invalid", "Work Mesh returned a malformed tracker record", {
60
- source: "Work Mesh", kind: expectedKind, company: company.slug, project_id: projectId,
61
- });
62
- }
63
- const meta = raw;
64
- if (meta.companyUid !== company.uid || meta.projectId !== projectId || meta.kind !== expectedKind || typeof meta.threadId !== "string" || !meta.threadId) {
65
- return fail("tracker_mesh_response_invalid", "Work Mesh tracker record identity did not match the requested company, project, and kind", {
66
- source: "Work Mesh",
67
- ...(typeof meta.threadId === "string" && meta.threadId ? { record_id: meta.threadId } : {}),
68
- kind: expectedKind,
69
- company: company.slug,
70
- project_id: projectId,
71
- });
72
- }
73
- const fields = fromMesh(meta);
74
- const createdAt = typeof meta.createdAt === "string" ? meta.createdAt : "";
75
- const updatedAt = typeof meta.lastActivityAt === "string" ? meta.lastActivityAt : createdAt;
76
- const version = meta.version;
77
- if (!Number.isSafeInteger(version) || version < 1 || !Number.isFinite(Date.parse(createdAt)) || !Number.isFinite(Date.parse(updatedAt))) {
78
- return fail("tracker_mesh_response_invalid", "Work Mesh tracker record has invalid version or timestamps", {
79
- source: "Work Mesh", record_id: meta.threadId, kind: expectedKind, company: company.slug, project_id: projectId,
80
- });
81
- }
82
- const record = {
83
- schema_version: "hq-lanes-tracker-record.v1",
84
- kind: expectedKind,
85
- id: meta.threadId,
86
- company: company.slug,
87
- project_id: projectId,
88
- version: version,
89
- created_at: createdAt,
90
- updated_at: updatedAt,
91
- };
92
- for (const field of RECORD_FIELDS[expectedKind]) {
93
- if (Object.hasOwn(fields, field))
94
- record[field] = fields[field];
95
- }
96
- // hq-pro stores KPI and OKR status in the shared threadStatus metadata field.
97
- if ((expectedKind === "kpi" || expectedKind === "okr") && !Object.hasOwn(record, "status") && Object.hasOwn(fields, "thread_status")) {
98
- record.status = fields.thread_status;
99
- }
100
- if (expectedKind === "blocker" && !Object.hasOwn(record, "opened_at"))
101
- record.opened_at = createdAt;
102
- return record;
103
- }
104
- function identityMatches(record, kind, id, company, projectId) {
105
- return record.kind === kind && record.id === id && record.company === company.slug && record.project_id === projectId;
106
- }
107
- function equalValue(left, right) {
108
- return JSON.stringify(left) === JSON.stringify(right);
109
- }
110
- export function createWorkMeshTrackerStore(options) {
111
- const { company, projectId } = options;
112
- if (!company.slug.trim() || !company.uid.trim())
113
- fail("tracker_company_unresolved", "A resolved company slug and uid are required for Work Mesh tracker storage", {});
114
- if (!projectId.trim())
115
- fail("tracker_project_invalid", "A project id is required for Work Mesh tracker storage", {});
116
- const now = options.now ?? (() => new Date());
117
- const request = options.request ?? (async (endpoint, init) => {
118
- const token = await requireToken({ interactive: false });
119
- return meshJson(token, endpoint, init);
120
- });
121
- const requestJson = async (endpoint, init) => {
122
- try {
123
- const result = await request(endpoint, init);
124
- if (!result || typeof result !== "object" || Array.isArray(result)) {
125
- return fail("tracker_mesh_response_invalid", "Work Mesh returned an invalid tracker response", { endpoint });
126
- }
127
- return result;
128
- }
129
- catch (err) {
130
- throw mapWorkMeshTrackerError(err);
131
- }
132
- };
133
- const list = async (kind) => {
134
- const records = [];
135
- let cursor;
136
- for (let page = 0; page < MAX_RECORD_PAGES; page += 1) {
137
- const query = { companyUid: company.uid, projectId, kind, limit: "100", ...(cursor ? { cursor } : {}) };
138
- const result = await requestJson(ROOT, { method: "GET", query });
139
- if (!Array.isArray(result.threads))
140
- return fail("tracker_mesh_response_invalid", "Work Mesh tracker list did not include a threads array", { kind, project_id: projectId });
141
- for (const row of result.threads)
142
- records.push(recordFromMeta(row, company, projectId, kind));
143
- if (result.nextCursor === undefined)
144
- return records.sort((a, b) => b.updated_at.localeCompare(a.updated_at) || a.id.localeCompare(b.id));
145
- if (typeof result.nextCursor !== "string" || !result.nextCursor || result.nextCursor === cursor) {
146
- return fail("tracker_mesh_response_invalid", "Work Mesh tracker list returned an invalid pagination cursor", { kind, project_id: projectId });
147
- }
148
- cursor = result.nextCursor;
149
- }
150
- return fail("tracker_mesh_response_incomplete", "Work Mesh tracker list exceeded its bounded page limit", { kind, project_id: projectId, max_pages: MAX_RECORD_PAGES });
151
- };
152
- const read = async (kind, id) => {
153
- const record = (await list(kind)).find((candidate) => candidate.id === id);
154
- if (record && !identityMatches(record, kind, id, company, projectId)) {
155
- return fail("tracker_company_mismatch", "Work Mesh tracker record does not belong to the selected company and project", { kind, id, project_id: projectId });
156
- }
157
- return record ?? null;
158
- };
159
- return {
160
- read,
161
- list,
162
- async create(input) {
163
- if (input.company !== company.slug || input.project_id !== projectId) {
164
- return fail("tracker_company_mismatch", "Tracker record company and project must match the resolved Work Mesh scope", { company: input.company, project_id: input.project_id });
165
- }
166
- const clientRecordId = input.id ?? (options.clientRecordId ?? randomUUID)();
167
- const { id: _id, company: _company, project_id: _projectId, kind, ...fields } = input;
168
- const result = await requestJson(ROOT, {
169
- method: "POST",
170
- body: { companyUid: company.uid, kind, projectId, clientRecordId, ...toMesh(fields) },
171
- });
172
- if (typeof result.threadId !== "string" || !result.threadId || !Number.isSafeInteger(result.version) || result.version < 1 || typeof result.createdAt !== "string" || !Number.isFinite(Date.parse(result.createdAt))) {
173
- return fail("tracker_mesh_response_invalid", "Work Mesh did not return valid tracker creation metadata", { kind, project_id: projectId });
174
- }
175
- const createdAt = result.createdAt;
176
- return {
177
- ...input,
178
- schema_version: "hq-lanes-tracker-record.v1",
179
- kind,
180
- id: result.threadId,
181
- company: company.slug,
182
- project_id: projectId,
183
- version: result.version,
184
- created_at: createdAt,
185
- updated_at: typeof result.lastActivityAt === "string" ? result.lastActivityAt : createdAt,
186
- };
187
- },
188
- async update(kind, id, expectedVersion, change, metadata) {
189
- const current = await read(kind, id);
190
- if (!current)
191
- return fail("tracker_record_not_found", `Tracker ${kind} record ${id} was not found`, { kind, id, project_id: projectId });
192
- if (current.version !== expectedVersion) {
193
- return fail("tracker_version_conflict", `Tracker record version conflict: expected ${expectedVersion}, found ${current.version}`, { kind, id, expected: expectedVersion, actual: current.version });
194
- }
195
- const changed = change(current);
196
- if (changed.id !== current.id || changed.kind !== current.kind || changed.company !== current.company || changed.project_id !== current.project_id || changed.created_at !== current.created_at) {
197
- return fail("tracker_update_invalid", "Tracker update cannot change record identity", { kind, id });
198
- }
199
- const patch = Object.fromEntries(Object.entries(changed).filter(([key, value]) => !["schema_version", "kind", "id", "company", "project_id", "version", "created_at", "updated_at"].includes(key) && !equalValue(current[key], value)));
200
- if (Object.keys(patch).length === 0)
201
- return current;
202
- const currentValues = Array.isArray(current.values) ? current.values : [];
203
- const changedValues = Array.isArray(changed.values) ? changed.values : [];
204
- const isKpiAppend = kind === "kpi" && changedValues.length === currentValues.length + 1 && currentValues.every((value, index) => equalValue(value, changedValues[index]));
205
- const currentAcks = Array.isArray(current.acknowledgements) ? current.acknowledgements : [];
206
- const changedAcks = Array.isArray(changed.acknowledgements) ? changed.acknowledgements : [];
207
- const isAckAppend = kind === "directive" && changedAcks.length === currentAcks.length + 1 && currentAcks.every((value, index) => equalValue(value, changedAcks[index]));
208
- let version;
209
- let updatedAt = now().toISOString();
210
- if (isKpiAppend) {
211
- const value = changedValues.at(-1);
212
- const response = await requestJson(`${ROOT}/${encodeURIComponent(id)}/events`, {
213
- method: "POST",
214
- body: { companyUid: company.uid, eventKind: "kpi_value", payload: toMesh(value) },
215
- });
216
- version = Number(response.version ?? current.version + 1);
217
- if (typeof response.createdAt === "string")
218
- updatedAt = response.createdAt;
219
- }
220
- else if (isAckAppend) {
221
- const ack = changedAcks.at(-1);
222
- const response = await requestJson(`${ROOT}/${encodeURIComponent(id)}/events`, {
223
- method: "POST",
224
- body: { companyUid: company.uid, eventKind: "ack", payload: toMesh(ack) },
225
- });
226
- version = Number(response.version ?? current.version + 1);
227
- if (typeof response.createdAt === "string")
228
- updatedAt = response.createdAt;
229
- }
230
- else {
231
- const action = kind === "blocker" && current.resolved_at == null && changed.resolved_at != null
232
- ? "resolve"
233
- : ((kind === "kpi" || kind === "okr") && changed.status === "approved" && current.status !== "approved")
234
- ? "approve"
235
- : "update";
236
- const result = await requestJson(`${ROOT}/${encodeURIComponent(id)}`, {
237
- method: "PATCH",
238
- body: { companyUid: company.uid, action, expectedVersion, ...(action === "approve" && metadata?.approvalQuestionId ? { approvalQuestionId: metadata.approvalQuestionId } : {}), ...toMesh(patch) },
239
- });
240
- version = Number(result.version);
241
- if (typeof result.lastActivityAt === "string")
242
- updatedAt = result.lastActivityAt;
243
- }
244
- if (!Number.isSafeInteger(version) || version <= current.version) {
245
- return fail("tracker_mesh_response_invalid", "Work Mesh tracker update returned an invalid version", { kind, id });
246
- }
247
- return { ...changed, schema_version: "hq-lanes-tracker-record.v1", version, created_at: current.created_at, updated_at: updatedAt };
248
- },
249
- };
250
- }
251
- //# sourceMappingURL=tracker-mesh.js.map