@indigoai-us/hq-cloud 6.14.30 → 6.14.32
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/dist/bin/sync-runner.d.ts +1 -1
- package/dist/bin/sync-runner.d.ts.map +1 -1
- package/dist/bin/sync-runner.js +11 -7
- package/dist/bin/sync-runner.js.map +1 -1
- package/dist/bin/sync-runner.test.js +6 -3
- package/dist/bin/sync-runner.test.js.map +1 -1
- package/dist/qmd-reindex.d.ts +6 -3
- package/dist/qmd-reindex.d.ts.map +1 -1
- package/dist/qmd-reindex.js +84 -7
- package/dist/qmd-reindex.js.map +1 -1
- package/dist/qmd-reindex.test.js +171 -5
- package/dist/qmd-reindex.test.js.map +1 -1
- package/dist/telemetry.d.ts +10 -0
- package/dist/telemetry.d.ts.map +1 -1
- package/dist/telemetry.js +174 -58
- package/dist/telemetry.js.map +1 -1
- package/dist/telemetry.test.js +96 -0
- package/dist/telemetry.test.js.map +1 -1
- package/package.json +1 -1
- package/src/bin/sync-runner.test.ts +6 -3
- package/src/bin/sync-runner.ts +13 -5
- package/src/qmd-reindex.test.ts +183 -5
- package/src/qmd-reindex.ts +89 -7
- package/src/telemetry.test.ts +121 -0
- package/src/telemetry.ts +216 -71
package/src/qmd-reindex.ts
CHANGED
|
@@ -15,9 +15,10 @@
|
|
|
15
15
|
* script inside the synced HQ tree (which may be stale).
|
|
16
16
|
*
|
|
17
17
|
* What it does, best-effort and idempotent:
|
|
18
|
-
* 1. Auto-registers
|
|
19
|
-
*
|
|
20
|
-
*
|
|
18
|
+
* 1. Auto-registers populated company knowledge, company project, and
|
|
19
|
+
* personal knowledge dirs that aren't yet qmd collections (kills the
|
|
20
|
+
* manual "map" step). Company knowledge detects path drift when the name
|
|
21
|
+
* exists but points elsewhere; repairs only when
|
|
21
22
|
* `HQ_QMD_REPAIR_PATH_DRIFT=1` (non-breaking default: detect-only).
|
|
22
23
|
* 2. Runs an incremental lexical `qmd update` (fast — qmd skips unchanged
|
|
23
24
|
* files by mtime).
|
|
@@ -395,6 +396,8 @@ export interface ReindexOptions {
|
|
|
395
396
|
readCompanies?: (companiesDir: string) => string[];
|
|
396
397
|
/** Returns true if the knowledge dir has at least one indexable .md file. */
|
|
397
398
|
hasIndexableMarkdown?: (knowledgeDir: string) => boolean;
|
|
399
|
+
/** Returns true if the projects dir has at least one indexable .md or .json file. */
|
|
400
|
+
hasIndexableProjectContent?: (projectsDir: string) => boolean;
|
|
398
401
|
/** Reindex-lock acquirer override for tests. */
|
|
399
402
|
acquireReindexLock?: AcquireReindexLock;
|
|
400
403
|
/** Corrupt-index quarantine override for tests. */
|
|
@@ -474,7 +477,7 @@ export function reindexAfterSync(
|
|
|
474
477
|
const changedPaths = opts.changedPaths;
|
|
475
478
|
const dirtyFromChanges =
|
|
476
479
|
changedPaths === undefined ||
|
|
477
|
-
changedPaths.some(
|
|
480
|
+
changedPaths.some(isQmdIndexableContentPath);
|
|
478
481
|
const registrationMayBeStale =
|
|
479
482
|
opts.forceCollectionRefresh === true ||
|
|
480
483
|
changedPaths === undefined ||
|
|
@@ -528,6 +531,9 @@ export function reindexAfterSync(
|
|
|
528
531
|
result.qmdAvailable = true;
|
|
529
532
|
const existingCollections = list.stdout;
|
|
530
533
|
let repairedThisCycle = false;
|
|
534
|
+
const hasIndexableMarkdown = opts.hasIndexableMarkdown ?? defaultHasIndexableMarkdown;
|
|
535
|
+
const hasIndexableProjectContent =
|
|
536
|
+
opts.hasIndexableProjectContent ?? defaultHasIndexableProjectContent;
|
|
531
537
|
|
|
532
538
|
// 1. Auto-register missing / reconcile drifted company knowledge collections.
|
|
533
539
|
if (registrationMayBeStale) {
|
|
@@ -537,7 +543,7 @@ export function reindexAfterSync(
|
|
|
537
543
|
for (const slug of slugs) {
|
|
538
544
|
const knowledgeDir = path.join(companiesDir, slug, "knowledge");
|
|
539
545
|
if (!existsSync(knowledgeDir)) continue;
|
|
540
|
-
const hasMd =
|
|
546
|
+
const hasMd = hasIndexableMarkdown(knowledgeDir);
|
|
541
547
|
if (!hasMd) continue;
|
|
542
548
|
|
|
543
549
|
const namePresent = existingCollections.includes(`qmd://${slug}/`);
|
|
@@ -601,6 +607,58 @@ export function reindexAfterSync(
|
|
|
601
607
|
result.collectionsRepaired.push(slug);
|
|
602
608
|
repairedThisCycle = true;
|
|
603
609
|
}
|
|
610
|
+
|
|
611
|
+
// 1b. Auto-register company projects collections. Match the shell
|
|
612
|
+
// post-sync convention exactly so prd.json and project docs are searchable.
|
|
613
|
+
for (const slug of slugs) {
|
|
614
|
+
const projectsDir = path.join(companiesDir, slug, "projects");
|
|
615
|
+
if (!existsSync(projectsDir)) continue;
|
|
616
|
+
if (!hasIndexableProjectContent(projectsDir)) continue;
|
|
617
|
+
|
|
618
|
+
const name = `${slug}-projects`;
|
|
619
|
+
if (existingCollections.includes(`qmd://${name}/`)) continue;
|
|
620
|
+
const add = exec([
|
|
621
|
+
"collection",
|
|
622
|
+
"add",
|
|
623
|
+
projectsDir,
|
|
624
|
+
"--name",
|
|
625
|
+
name,
|
|
626
|
+
"--mask",
|
|
627
|
+
"**/*.{md,json}",
|
|
628
|
+
]);
|
|
629
|
+
if (add.status === 0) {
|
|
630
|
+
exec(["context", "add", `qmd://${name}`, `Project PRDs and documentation for ${slug}.`]);
|
|
631
|
+
result.collectionsAdded.push(name);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// 1c. Auto-register the owner overlay's personal knowledge collection.
|
|
636
|
+
const personalKnowledgeDir = path.join(hqRoot, "personal", "knowledge");
|
|
637
|
+
const personalKnowledgeName = "personal-knowledge";
|
|
638
|
+
if (
|
|
639
|
+
existsSync(personalKnowledgeDir) &&
|
|
640
|
+
hasIndexableMarkdown(personalKnowledgeDir) &&
|
|
641
|
+
!existingCollections.includes(`qmd://${personalKnowledgeName}/`)
|
|
642
|
+
) {
|
|
643
|
+
const add = exec([
|
|
644
|
+
"collection",
|
|
645
|
+
"add",
|
|
646
|
+
personalKnowledgeDir,
|
|
647
|
+
"--name",
|
|
648
|
+
personalKnowledgeName,
|
|
649
|
+
"--mask",
|
|
650
|
+
"**/*.md",
|
|
651
|
+
]);
|
|
652
|
+
if (add.status === 0) {
|
|
653
|
+
exec([
|
|
654
|
+
"context",
|
|
655
|
+
"add",
|
|
656
|
+
`qmd://${personalKnowledgeName}`,
|
|
657
|
+
"Personal knowledge base (owner overlay).",
|
|
658
|
+
]);
|
|
659
|
+
result.collectionsAdded.push(personalKnowledgeName);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
604
662
|
}
|
|
605
663
|
|
|
606
664
|
// 2. Incremental lexical reindex.
|
|
@@ -721,9 +779,13 @@ function writeState(statePath: string, state: QmdReindexState): void {
|
|
|
721
779
|
}
|
|
722
780
|
}
|
|
723
781
|
|
|
724
|
-
function
|
|
782
|
+
function isQmdIndexableContentPath(relPath: string): boolean {
|
|
725
783
|
const normalized = relPath.split(path.sep).join("/");
|
|
726
|
-
return
|
|
784
|
+
return (
|
|
785
|
+
/^companies\/[^/]+\/knowledge\/.+\.md$/i.test(normalized) ||
|
|
786
|
+
/^companies\/[^/]+\/projects\/.+\.(?:md|json)$/i.test(normalized) ||
|
|
787
|
+
/^personal\/knowledge\/.+\.md$/i.test(normalized)
|
|
788
|
+
);
|
|
727
789
|
}
|
|
728
790
|
|
|
729
791
|
function defaultReadCompanies(companiesDir: string): string[] {
|
|
@@ -758,3 +820,23 @@ function defaultHasIndexableMarkdown(knowledgeDir: string): boolean {
|
|
|
758
820
|
}
|
|
759
821
|
return false;
|
|
760
822
|
}
|
|
823
|
+
|
|
824
|
+
function defaultHasIndexableProjectContent(projectsDir: string): boolean {
|
|
825
|
+
// Match the shell's `find -type f \( -name '*.md' -o -name '*.json' \)`
|
|
826
|
+
// guard: project documentation and PRD JSON both make the collection useful.
|
|
827
|
+
const stack = [projectsDir];
|
|
828
|
+
while (stack.length) {
|
|
829
|
+
const dir = stack.pop()!;
|
|
830
|
+
let entries: fs.Dirent[];
|
|
831
|
+
try {
|
|
832
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
833
|
+
} catch {
|
|
834
|
+
continue;
|
|
835
|
+
}
|
|
836
|
+
for (const e of entries) {
|
|
837
|
+
if (e.isFile() && (e.name.endsWith(".md") || e.name.endsWith(".json"))) return true;
|
|
838
|
+
if (e.isDirectory()) stack.push(path.join(dir, e.name));
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
return false;
|
|
842
|
+
}
|
package/src/telemetry.test.ts
CHANGED
|
@@ -126,6 +126,8 @@ function makeOpts(env: TestEnv, client: TelemetryClientSurface): CollectTelemetr
|
|
|
126
126
|
installerVersion: "test-version",
|
|
127
127
|
claudeProjectsRoot: env.claudeProjects,
|
|
128
128
|
codexRoot: env.codexRoot,
|
|
129
|
+
maxScanBytesPerSource: Number.MAX_SAFE_INTEGER,
|
|
130
|
+
maxBatchesPerRun: Number.MAX_SAFE_INTEGER,
|
|
129
131
|
cursorPath: env.cursorPath,
|
|
130
132
|
menubarPath: env.menubarPath,
|
|
131
133
|
};
|
|
@@ -813,6 +815,96 @@ describe("collectAndSendTelemetry — Codex rollouts", () => {
|
|
|
813
815
|
expect(cursor.codex_next_rollout).toBe(file);
|
|
814
816
|
});
|
|
815
817
|
|
|
818
|
+
it("bounds a quiet first backfill and resumes until the token event", async () => {
|
|
819
|
+
const file = writeCodexRollout(env, "sessions", "rollout-bounded.jsonl", [
|
|
820
|
+
{ type: "session_meta", payload: { id: "bounded", model: "gpt-5-codex" } },
|
|
821
|
+
...Array.from({ length: 8 }, (_, index) => ({
|
|
822
|
+
type: "response_item",
|
|
823
|
+
payload: { type: "message", content: "x".repeat(140), index },
|
|
824
|
+
})),
|
|
825
|
+
codexToken("2026-07-30T14:00:00Z", "bounded-event", 5, 7),
|
|
826
|
+
]);
|
|
827
|
+
const client = makeClient();
|
|
828
|
+
const opts = {
|
|
829
|
+
...makeOpts(env, client),
|
|
830
|
+
maxScanBytesPerSource: 350,
|
|
831
|
+
};
|
|
832
|
+
|
|
833
|
+
const first = await collectAndSendTelemetry(opts);
|
|
834
|
+
|
|
835
|
+
expect(first.eventsSent).toBe(0);
|
|
836
|
+
const firstOffset = readCursor(env).files[file]?.offset ?? 0;
|
|
837
|
+
expect(firstOffset).toBeGreaterThan(0);
|
|
838
|
+
expect(firstOffset).toBeLessThan(fs.statSync(file).size);
|
|
839
|
+
|
|
840
|
+
for (let run = 0; run < 10 && client.posts.length === 0; run++) {
|
|
841
|
+
await collectAndSendTelemetry(opts);
|
|
842
|
+
}
|
|
843
|
+
expect(client.posts.flatMap((post) => post.events)).toEqual([
|
|
844
|
+
expect.objectContaining({
|
|
845
|
+
sessionId: "bounded",
|
|
846
|
+
uuid: "bounded-event",
|
|
847
|
+
model: "gpt-5-codex",
|
|
848
|
+
}),
|
|
849
|
+
]);
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
it("discards an oversized UTF-8 row without corrupting resume offsets", async () => {
|
|
853
|
+
const file = writeCodexRollout(env, "sessions", "rollout-utf8.jsonl", [
|
|
854
|
+
{ type: "session_meta", payload: { id: "utf8", model: "gpt-5-codex" } },
|
|
855
|
+
{
|
|
856
|
+
type: "response_item",
|
|
857
|
+
payload: { type: "message", content: "😀".repeat(20_000) },
|
|
858
|
+
},
|
|
859
|
+
codexToken("2026-07-30T14:00:01Z", "utf8-event", 2, 3),
|
|
860
|
+
]);
|
|
861
|
+
const client = makeClient();
|
|
862
|
+
const opts = {
|
|
863
|
+
...makeOpts(env, client),
|
|
864
|
+
maxScanBytesPerSource: 64 * 1024,
|
|
865
|
+
};
|
|
866
|
+
|
|
867
|
+
for (let run = 0; run < 5 && client.posts.length === 0; run++) {
|
|
868
|
+
await collectAndSendTelemetry(opts);
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
const events = client.posts.flatMap((post) => post.events);
|
|
872
|
+
expect(events).toEqual([
|
|
873
|
+
expect.objectContaining({ uuid: "utf8-event", sessionId: "utf8" }),
|
|
874
|
+
]);
|
|
875
|
+
expect(readCursor(env).files[file].offset).toBeLessThanOrEqual(
|
|
876
|
+
fs.statSync(file).size,
|
|
877
|
+
);
|
|
878
|
+
});
|
|
879
|
+
|
|
880
|
+
it("caps upload batches and resumes remaining token events", async () => {
|
|
881
|
+
writeCodexRollout(env, "sessions", "rollout-batch-cap.jsonl", [
|
|
882
|
+
{ type: "session_meta", payload: { id: "batch-cap", model: "gpt-5-codex" } },
|
|
883
|
+
...Array.from({ length: 201 }, (_, index) =>
|
|
884
|
+
codexToken(
|
|
885
|
+
"2026-07-30T14:01:" + String(index % 60).padStart(2, "0") + "Z",
|
|
886
|
+
"batch-cap-" + index,
|
|
887
|
+
1,
|
|
888
|
+
1,
|
|
889
|
+
),
|
|
890
|
+
),
|
|
891
|
+
]);
|
|
892
|
+
const client = makeClient();
|
|
893
|
+
const opts = {
|
|
894
|
+
...makeOpts(env, client),
|
|
895
|
+
maxScanBytesPerSource: 1_000_000,
|
|
896
|
+
maxBatchesPerRun: 2,
|
|
897
|
+
};
|
|
898
|
+
|
|
899
|
+
const first = await collectAndSendTelemetry(opts);
|
|
900
|
+
expect(first.batchesSent).toBe(2);
|
|
901
|
+
expect(first.eventsSent).toBe(200);
|
|
902
|
+
|
|
903
|
+
const second = await collectAndSendTelemetry(opts);
|
|
904
|
+
expect(second.eventsSent).toBe(1);
|
|
905
|
+
expect(client.posts.flatMap((post) => post.events)).toHaveLength(201);
|
|
906
|
+
});
|
|
907
|
+
|
|
816
908
|
it("does not advance a Codex rollout cursor when upload fails", async () => {
|
|
817
909
|
const file = writeCodexRollout(env, "sessions", "rollout-retry.jsonl", [
|
|
818
910
|
{ type: "session_meta", payload: { id: "retry", model: "gpt-5-codex" } },
|
|
@@ -886,6 +978,35 @@ describe("collectAndSendTelemetry — companyUid attribution", () => {
|
|
|
886
978
|
expect("companyUid" in client.posts[0].events[0]).toBe(false);
|
|
887
979
|
});
|
|
888
980
|
|
|
981
|
+
it("uses the explicit single-company fallback when cwd is the shared HQ root", async () => {
|
|
982
|
+
const client = makeClient();
|
|
983
|
+
writeJsonl(env, "proj", "s.jsonl", [rowWithCwd("u1", hqRoot)]);
|
|
984
|
+
|
|
985
|
+
await collectAndSendTelemetry({
|
|
986
|
+
...makeOpts(env, client),
|
|
987
|
+
hqRoot,
|
|
988
|
+
fallbackCompany: "indigo",
|
|
989
|
+
});
|
|
990
|
+
|
|
991
|
+
expect(client.posts).toHaveLength(1);
|
|
992
|
+
expect(client.posts[0].events[0].companyUid).toBe(COMPANY_UID);
|
|
993
|
+
});
|
|
994
|
+
|
|
995
|
+
it("prefers the cwd-resolved company over the single-company fallback", async () => {
|
|
996
|
+
const client = makeClient();
|
|
997
|
+
const inRepo = path.join(hqRoot, "repos/private/hq-cloud");
|
|
998
|
+
writeJsonl(env, "proj", "s.jsonl", [rowWithCwd("u1", inRepo)]);
|
|
999
|
+
|
|
1000
|
+
await collectAndSendTelemetry({
|
|
1001
|
+
...makeOpts(env, client),
|
|
1002
|
+
hqRoot,
|
|
1003
|
+
fallbackCompany: "cmp_01OTHER",
|
|
1004
|
+
});
|
|
1005
|
+
|
|
1006
|
+
expect(client.posts).toHaveLength(1);
|
|
1007
|
+
expect(client.posts[0].events[0].companyUid).toBe(COMPANY_UID);
|
|
1008
|
+
});
|
|
1009
|
+
|
|
889
1010
|
it("never sends the reserved 'unattributed' value", async () => {
|
|
890
1011
|
const client = makeClient();
|
|
891
1012
|
writeJsonl(env, "proj", "s.jsonl", [
|