@indigoai-us/hq-cli 5.345.54 → 5.345.56
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 +31 -0
- package/assets/scaffold/core/scripts/derive-trigger-facts.sh +10 -1
- package/assets/scaffold/core/scripts/jobs-validate.sh +2 -1
- package/assets/scaffold/core/scripts/session-title.sh +18 -15
- package/dist/command-catalog.generated.d.ts +143 -2
- package/dist/command-catalog.generated.js +175 -2
- package/dist/command-registration-plan.d.ts +6 -1
- package/dist/command-registration-plan.js +2 -1
- package/dist/commands/cloud.d.ts +7 -0
- package/dist/commands/cloud.js +92 -16
- package/dist/commands/core-delegate-w1b.js +18 -5
- package/dist/commands/core-derive-trigger-facts-command.js +14 -0
- package/dist/commands/core-derive-trigger-facts-native.d.ts +1 -0
- package/dist/commands/core-derive-trigger-facts-native.js +20 -1
- package/dist/commands/daemon.js +16 -0
- package/dist/commands/doctor.d.ts +1 -0
- package/dist/commands/doctor.js +15 -2
- package/dist/commands/import-scan.d.ts +24 -0
- package/dist/commands/import-scan.js +75 -0
- package/dist/commands/lanes-checks.d.ts +3 -0
- package/dist/commands/lanes-checks.js +34 -0
- package/dist/commands/lanes-session-start.d.ts +17 -0
- package/dist/commands/lanes-session-start.js +195 -0
- package/dist/commands/lanes-shared.d.ts +2 -1
- package/dist/commands/lanes-watch.d.ts +2 -0
- package/dist/commands/lanes-watch.js +2 -1
- package/dist/commands/lanes.d.ts +3 -1
- package/dist/commands/lanes.js +121 -18
- package/dist/commands/members.d.ts +4 -0
- package/dist/commands/members.js +23 -2
- package/dist/lanes-drain-hook.js +6 -2
- package/dist/lib/daemon/conflict-notices.d.ts +4 -0
- package/dist/lib/daemon/conflict-notices.js +80 -0
- package/dist/lib/doctor/checks/sync-health.d.ts +2 -0
- package/dist/lib/doctor/checks/sync-health.js +47 -23
- package/dist/lib/import-scan/events.d.ts +135 -0
- package/dist/lib/import-scan/events.js +71 -0
- package/dist/lib/import-scan/run.d.ts +84 -0
- package/dist/lib/import-scan/run.js +308 -0
- package/dist/lib/import-scan/scan-event.schema.json +127 -0
- package/dist/lib/import-scan/validate.d.ts +31 -0
- package/dist/lib/import-scan/validate.js +111 -0
- package/dist/lib/lanes/dropbox.d.ts +4 -2
- package/dist/lib/lanes/dropbox.js +54 -9
- package/dist/lib/lanes/flag-keys.d.ts +10 -0
- package/dist/lib/lanes/flag-keys.js +10 -0
- package/dist/lib/lanes/hooks-install.d.ts +14 -11
- package/dist/lib/lanes/hooks-install.js +70 -50
- package/dist/lib/lanes/monitor.js +1 -1
- package/dist/lib/lanes/reaper.d.ts +58 -0
- package/dist/lib/lanes/reaper.js +403 -0
- package/dist/lib/lanes/registry-flag.d.ts +2 -0
- package/dist/lib/lanes/registry-flag.js +1 -1
- package/dist/lib/lanes/spawn.d.ts +2 -0
- package/dist/lib/lanes/spawn.js +32 -2
- package/dist/lib/lanes/status-fields.d.ts +12 -0
- package/dist/lib/lanes/status-fields.js +189 -0
- package/dist/lib/lanes/types.js +1 -1
- package/dist/lib/lanes/worker-continuity.d.ts +18 -0
- package/dist/lib/lanes/worker-continuity.js +81 -0
- package/dist/lib/lanes/worker-process.d.ts +4 -0
- package/dist/lib/lanes/worker-process.js +77 -0
- package/dist/main.js +9 -3
- package/dist/utils/cli-telemetry.d.ts +4 -1
- package/dist/utils/cli-telemetry.js +8 -0
- package/dist/utils/jq-windows-compat.d.ts +13 -0
- package/dist/utils/jq-windows-compat.js +48 -0
- package/dist/utils/run-bundled-script.d.ts +3 -0
- package/dist/utils/run-bundled-script.js +9 -80
- package/dist/utils/windows-bash.d.ts +5 -0
- package/dist/utils/windows-bash.js +75 -0
- package/package.json +1 -1
package/dist/lanes-drain-hook.js
CHANGED
|
@@ -17,7 +17,7 @@ function parseHookInput(text) {
|
|
|
17
17
|
throw new Error("invalid_hook_input_shape: expected a JSON object");
|
|
18
18
|
}
|
|
19
19
|
const value = parsed;
|
|
20
|
-
for (const key of ["transcript_path", "session_id", "hook_event_name"]) {
|
|
20
|
+
for (const key of ["transcript_path", "session_id", "hook_event_name", "hookEventName"]) {
|
|
21
21
|
if (value[key] !== undefined && value[key] !== null && typeof value[key] !== "string") {
|
|
22
22
|
throw new Error(`invalid_hook_input_field: ${key} must be a string`);
|
|
23
23
|
}
|
|
@@ -25,7 +25,11 @@ function parseHookInput(text) {
|
|
|
25
25
|
return {
|
|
26
26
|
...(typeof value.transcript_path === "string" ? { transcript_path: value.transcript_path } : {}),
|
|
27
27
|
...(typeof value.session_id === "string" ? { session_id: value.session_id } : {}),
|
|
28
|
-
...(typeof value.hook_event_name === "string"
|
|
28
|
+
...(typeof value.hook_event_name === "string"
|
|
29
|
+
? { hook_event_name: value.hook_event_name }
|
|
30
|
+
: typeof value.hookEventName === "string"
|
|
31
|
+
? { hookEventName: value.hookEventName }
|
|
32
|
+
: {}),
|
|
29
33
|
};
|
|
30
34
|
}
|
|
31
35
|
function laneIdIsSafe(laneId) {
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare function conflictNoticeStateDir(env?: NodeJS.ProcessEnv, home?: string): string;
|
|
2
|
+
/** Acknowledge by appending a tombstone; runner-owned notice data is read-only here. */
|
|
3
|
+
export declare function acknowledgeConflictNotice(id: string, stateDir?: string, acknowledgedAt?: string): void;
|
|
4
|
+
//# sourceMappingURL=conflict-notices.d.ts.map
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as os from "node:os";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
function writeAllSync(fd, text) {
|
|
5
|
+
const bytes = Buffer.from(text);
|
|
6
|
+
let offset = 0;
|
|
7
|
+
while (offset < bytes.length) {
|
|
8
|
+
const written = fs.writeSync(fd, bytes, offset, bytes.length - offset);
|
|
9
|
+
if (written <= 0)
|
|
10
|
+
throw new Error("Conflict acknowledgement append made no progress.");
|
|
11
|
+
offset += written;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export function conflictNoticeStateDir(env = process.env, home = os.homedir()) {
|
|
15
|
+
return env.HQ_STATE_DIR || path.join(home, ".hq");
|
|
16
|
+
}
|
|
17
|
+
/** Acknowledge by appending a tombstone; runner-owned notice data is read-only here. */
|
|
18
|
+
export function acknowledgeConflictNotice(id, stateDir = conflictNoticeStateDir(), acknowledgedAt = new Date().toISOString()) {
|
|
19
|
+
if (!/^[a-f0-9]{64}$/.test(id))
|
|
20
|
+
throw new Error("Invalid conflict notice id.");
|
|
21
|
+
const noticesPath = path.join(stateDir, "conflict-notices.json");
|
|
22
|
+
let notices = [];
|
|
23
|
+
try {
|
|
24
|
+
const file = JSON.parse(fs.readFileSync(noticesPath, "utf8"));
|
|
25
|
+
if (file?.schema === 1 && Array.isArray(file.notices))
|
|
26
|
+
notices = file.notices;
|
|
27
|
+
else
|
|
28
|
+
process.stderr.write("Conflict notice schema is unsupported; treating notices as empty.\n");
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
if (error.code !== "ENOENT") {
|
|
32
|
+
process.stderr.write(`Conflict notice file could not be read; treating notices as empty (${error instanceof Error ? error.name : "UnknownError"}).\n`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (!notices.some((notice) => notice.id === id)) {
|
|
36
|
+
throw new Error("Unknown conflict notice.");
|
|
37
|
+
}
|
|
38
|
+
const ackPath = path.join(stateDir, "conflict-notice-acks.jsonl");
|
|
39
|
+
let existing = "";
|
|
40
|
+
try {
|
|
41
|
+
existing = fs.readFileSync(ackPath, "utf8");
|
|
42
|
+
let malformedLineCount = 0;
|
|
43
|
+
let alreadyAcknowledged = false;
|
|
44
|
+
for (const line of existing.split("\n")) {
|
|
45
|
+
if (!line)
|
|
46
|
+
continue;
|
|
47
|
+
try {
|
|
48
|
+
const record = JSON.parse(line);
|
|
49
|
+
if (!/^[a-f0-9]{64}$/.test(record.id) || typeof record.acknowledgedAt !== "string") {
|
|
50
|
+
throw new Error("Invalid acknowledgement record");
|
|
51
|
+
}
|
|
52
|
+
if (record.id === id)
|
|
53
|
+
alreadyAcknowledged = true;
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
malformedLineCount += 1;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (malformedLineCount > 0) {
|
|
60
|
+
process.stderr.write(`Skipped ${malformedLineCount} malformed conflict acknowledgement line(s).\n`);
|
|
61
|
+
}
|
|
62
|
+
if (alreadyAcknowledged)
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
if (error.code !== "ENOENT")
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
fs.mkdirSync(stateDir, { recursive: true });
|
|
70
|
+
const fd = fs.openSync(ackPath, "a", 0o600);
|
|
71
|
+
try {
|
|
72
|
+
const separator = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
|
|
73
|
+
writeAllSync(fd, `${separator}${JSON.stringify({ id, acknowledgedAt })}\n`);
|
|
74
|
+
fs.fsyncSync(fd);
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
fs.closeSync(fd);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=conflict-notices.js.map
|
|
@@ -59,6 +59,8 @@ export interface SyncManifestUploadStatus {
|
|
|
59
59
|
snapshotId: string | null;
|
|
60
60
|
sequence: number;
|
|
61
61
|
baseUsable: boolean;
|
|
62
|
+
/** Local company slug for company-scoped remediation; absent for personal. */
|
|
63
|
+
companySlug?: string;
|
|
62
64
|
}
|
|
63
65
|
/** Injectable dependencies — defaults are the real collectors. */
|
|
64
66
|
export interface SyncHealthDeps {
|
|
@@ -32,6 +32,7 @@ import * as path from "node:path";
|
|
|
32
32
|
// ../../hq-cloud-manifest.js.
|
|
33
33
|
import * as hqCloud from "@indigoai-us/hq-cloud";
|
|
34
34
|
import { loadManifestExports, MANIFEST_UNAVAILABLE_REASON, } from "../../hq-cloud-manifest.js";
|
|
35
|
+
import { readCompaniesManifestMap } from "../../work-context/repo-remote.js";
|
|
35
36
|
import { CLI_VERSION } from "../../../cli-version.js";
|
|
36
37
|
import { readSyncVersion } from "../../../utils/feedback-versions.js";
|
|
37
38
|
import { readHqVersion } from "../../../utils/pack-contributions.js";
|
|
@@ -52,9 +53,9 @@ function withDefaults(deps, hqRoot) {
|
|
|
52
53
|
return {
|
|
53
54
|
...deps,
|
|
54
55
|
runtime: deps.runtime ?? (() => []),
|
|
55
|
-
manifestStatuses: deps.manifestStatuses ?? defaultManifestStatuses,
|
|
56
|
+
manifestStatuses: deps.manifestStatuses ?? ((journals) => defaultManifestStatuses(hqRoot, journals)),
|
|
56
57
|
manifestAvailable: deps.manifestAvailable ?? (() => loadManifestExports() !== null),
|
|
57
|
-
unresolvedScopes: deps.unresolvedScopes ?? defaultUnresolvedScopes,
|
|
58
|
+
unresolvedScopes: deps.unresolvedScopes ?? ((journals) => defaultUnresolvedScopes(hqRoot, journals)),
|
|
58
59
|
localCompanyExists: deps.localCompanyExists ??
|
|
59
60
|
((slug) => defaultLocalCompanyExists(hqRoot, slug)),
|
|
60
61
|
};
|
|
@@ -78,15 +79,35 @@ function defaultLocalCompanyExists(hqRoot, slug) {
|
|
|
78
79
|
return false;
|
|
79
80
|
}
|
|
80
81
|
}
|
|
81
|
-
/**
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
82
|
+
/** Resolve locally-known company journal slugs to the UID used by snapshots. */
|
|
83
|
+
function resolvedCompanyScopes(hqRoot, journals) {
|
|
84
|
+
const companies = readCompaniesManifestMap(hqRoot);
|
|
85
|
+
const seen = new Set();
|
|
86
|
+
const scopes = [];
|
|
87
|
+
for (const { slug } of journals) {
|
|
88
|
+
if (NON_COMPANY_JOURNAL_SLUGS.has(slug) ||
|
|
89
|
+
!isSafeCompanySlug(slug) ||
|
|
90
|
+
!defaultLocalCompanyExists(hqRoot, slug) ||
|
|
91
|
+
seen.has(slug)) {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
seen.add(slug);
|
|
95
|
+
const uid = companies?.[slug]?.cloud_uid;
|
|
96
|
+
if (typeof uid === "string" && /^cmp_[A-Za-z0-9]{3,128}$/.test(uid.trim())) {
|
|
97
|
+
scopes.push({ kind: "company", companyUid: uid.trim(), slug });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return scopes;
|
|
101
|
+
}
|
|
102
|
+
function defaultUnresolvedScopes(hqRoot, journals) {
|
|
103
|
+
const resolved = new Set(resolvedCompanyScopes(hqRoot, journals).map((scope) => scope.slug));
|
|
104
|
+
return [
|
|
105
|
+
...new Set(journals
|
|
106
|
+
.filter((entry) => !NON_COMPANY_JOURNAL_SLUGS.has(entry.slug) &&
|
|
107
|
+
defaultLocalCompanyExists(hqRoot, entry.slug) &&
|
|
108
|
+
!resolved.has(entry.slug))
|
|
109
|
+
.map((entry) => entry.slug)),
|
|
110
|
+
];
|
|
90
111
|
}
|
|
91
112
|
function defaultVersions(hqRoot) {
|
|
92
113
|
return {
|
|
@@ -119,21 +140,15 @@ const DEFAULT_DEPS = {
|
|
|
119
140
|
// than no check.
|
|
120
141
|
journals: () => hqCloud.listJournals(),
|
|
121
142
|
now: () => new Date(),
|
|
122
|
-
manifestStatuses: defaultManifestStatuses,
|
|
123
143
|
runtime: defaultSyncRuntimeResults,
|
|
124
144
|
};
|
|
125
145
|
/**
|
|
126
146
|
* Map the machine's journal shards onto the manifest scopes hq-cloud keys its
|
|
127
|
-
* snapshot store by, then read each one's last upload.
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
* company. Note the snapshot store keys company scopes by `companyUid`, which
|
|
131
|
-
* the journal listing does not carry — so a company whose uid we cannot name
|
|
132
|
-
* is simply not reported here rather than reported wrongly. That is the honest
|
|
133
|
-
* outcome: this check exists to tell an operator when a manifest STOPPED being
|
|
134
|
-
* uploaded, and inventing a scope key would make it lie in both directions.
|
|
147
|
+
* snapshot store by, then read each one's last upload. Company journal rows
|
|
148
|
+
* carry only slugs, so resolve their UIDs from the local companies manifest;
|
|
149
|
+
* a missing or malformed UID stays explicitly unresolved below.
|
|
135
150
|
*/
|
|
136
|
-
function defaultManifestStatuses(journals) {
|
|
151
|
+
function defaultManifestStatuses(hqRoot, journals) {
|
|
137
152
|
// Both the legacy `personal` shard and the current personal-vault
|
|
138
153
|
// pseudo-slug name the SAME personal tree; a machine mid-migration has both
|
|
139
154
|
// on disk, so collapse them to a single personal scope rather than asking
|
|
@@ -141,6 +156,7 @@ function defaultManifestStatuses(journals) {
|
|
|
141
156
|
const scopes = journals.some((entry) => NON_COMPANY_JOURNAL_SLUGS.has(entry.slug))
|
|
142
157
|
? [{ kind: "personal" }]
|
|
143
158
|
: [];
|
|
159
|
+
scopes.push(...resolvedCompanyScopes(hqRoot, journals));
|
|
144
160
|
if (scopes.length === 0)
|
|
145
161
|
return [];
|
|
146
162
|
// Absent on an older hq-cloud; the NA row is emitted by `manifestResults`
|
|
@@ -148,7 +164,15 @@ function defaultManifestStatuses(journals) {
|
|
|
148
164
|
const exports = loadManifestExports();
|
|
149
165
|
if (!exports)
|
|
150
166
|
return [];
|
|
151
|
-
|
|
167
|
+
const companySlugsByUid = new Map(scopes
|
|
168
|
+
.filter((scope) => scope.kind === "company")
|
|
169
|
+
.map((scope) => [scope.companyUid, scope.slug]));
|
|
170
|
+
const statuses = exports.readManifestUploadStatus(exports.getStateDir(), scopes);
|
|
171
|
+
return statuses.map((status) => {
|
|
172
|
+
const companyUid = /^company:(cmp_[A-Za-z0-9]{3,128})$/.exec(status.scopeKey)?.[1];
|
|
173
|
+
const companySlug = companyUid ? companySlugsByUid.get(companyUid) : undefined;
|
|
174
|
+
return companySlug ? { ...status, companySlug } : status;
|
|
175
|
+
});
|
|
152
176
|
}
|
|
153
177
|
/** The legacy journal slug that named the personal (non-company) tree. */
|
|
154
178
|
const PERSONAL_SCOPE_SLUG = "personal";
|
|
@@ -526,7 +550,7 @@ function manifestResults(deps, journals) {
|
|
|
526
550
|
checkId,
|
|
527
551
|
reasonCode: "stale-threshold",
|
|
528
552
|
message: `Scope '${entry.scopeKey}' last uploaded a sync manifest ${days} day${days === 1 ? "" : "s"} ago (${entry.lastUploadAt}) — the daily audit pass is not running.`,
|
|
529
|
-
remediation:
|
|
553
|
+
remediation: `Run \`hq sync manifest${entry.companySlug ? ` --scope ${entry.companySlug}` : ""}\` and check HQ_SYNC_MANIFEST_DISABLED and the sync runner.`,
|
|
530
554
|
};
|
|
531
555
|
}
|
|
532
556
|
return {
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Streaming context-scan events (contract v1).
|
|
3
|
+
*
|
|
4
|
+
* `hq import scan --json --stream` relays these as JSON Lines, one object per
|
|
5
|
+
* line. They are produced by the HQ import-context scanner
|
|
6
|
+
* (`.claude/skills/import-context/scan.sh --progress-json` in an HQ tree) and
|
|
7
|
+
* validated here before they reach stdout. The HQ desktop first-run setup
|
|
8
|
+
* renders them as a knowledge tree that grows while the scan runs.
|
|
9
|
+
*
|
|
10
|
+
* The canonical contract lives next to the scanner
|
|
11
|
+
* (`.claude/skills/import-context/progress-json.md` in hq-core) and in
|
|
12
|
+
* `docs/import-scan-stream.md` in this repo. The JSON Schema is
|
|
13
|
+
* `scan-event.schema.json` in this directory (shipped next to the compiled
|
|
14
|
+
* module as `dist/lib/import-scan/scan-event.schema.json`).
|
|
15
|
+
*
|
|
16
|
+
* Desktop consumers can import these types from
|
|
17
|
+
* `@indigoai-us/hq-cli/dist/lib/import-scan/events.js` or mirror them.
|
|
18
|
+
*
|
|
19
|
+
* Versioning: within v1, fields, source ids, count keys and messages may be
|
|
20
|
+
* added. Consumers ignore unknown fields and must not parse message text.
|
|
21
|
+
* Removing or retyping a field, or adding a new `type`, `status` or `basis`
|
|
22
|
+
* value, bumps `v`.
|
|
23
|
+
*/
|
|
24
|
+
export declare const SCAN_EVENT_VERSION: 1;
|
|
25
|
+
/** Event types, in the order a stream introduces them. */
|
|
26
|
+
export declare const SCAN_EVENT_TYPES: readonly ["start", "source", "count", "company", "project", "error", "done"];
|
|
27
|
+
export type ScanEventType = (typeof SCAN_EVENT_TYPES)[number];
|
|
28
|
+
export declare const SCAN_SOURCE_STATUSES: readonly ["scanning", "done", "skipped", "error"];
|
|
29
|
+
export type ScanSourceStatus = (typeof SCAN_SOURCE_STATUSES)[number];
|
|
30
|
+
export declare const SCAN_COMPANY_BASES: readonly ["hq-company", "repo-org", "folder"];
|
|
31
|
+
export type ScanCompanyBasis = (typeof SCAN_COMPANY_BASES)[number];
|
|
32
|
+
export declare const SCAN_PROJECT_BASES: readonly ["repo", "claude-code-cwd", "codex-cwd"];
|
|
33
|
+
export type ScanProjectBasis = (typeof SCAN_PROJECT_BASES)[number];
|
|
34
|
+
/**
|
|
35
|
+
* Source ids the v1 scanner emits, in scan order. Other ids may appear in
|
|
36
|
+
* later v1 scanners; show them by their `label`.
|
|
37
|
+
*/
|
|
38
|
+
export declare const KNOWN_SCAN_SOURCE_IDS: readonly ["hq", "repos", "claude-code", "codex", "grok", "claude-ai", "artifacts"];
|
|
39
|
+
export type KnownScanSourceId = (typeof KNOWN_SCAN_SOURCE_IDS)[number];
|
|
40
|
+
/**
|
|
41
|
+
* Source id used on `error` events that hq-cli itself emits (HQ not found,
|
|
42
|
+
* scanner missing or too old, scanner crashed). Never listed in `start`.
|
|
43
|
+
*/
|
|
44
|
+
export declare const SCAN_CLI_ERROR_SOURCE: "scanner";
|
|
45
|
+
/**
|
|
46
|
+
* Stable machine-readable codes on `error` events. hq-cli sets one on every
|
|
47
|
+
* error it emits itself (source "scanner"); scanner-emitted errors may omit it.
|
|
48
|
+
*
|
|
49
|
+
* scanner_outdated the HQ tree's scanner is missing or predates
|
|
50
|
+
* --progress-json; updating HQ fixes it
|
|
51
|
+
* scan_failed any other reason the scan could not run or finish
|
|
52
|
+
* (HQ not found, scanner crashed, cancelled, non-zero exit)
|
|
53
|
+
*
|
|
54
|
+
* Consumers branch on `code`, never on `message`. Treat an error without a
|
|
55
|
+
* code as non-fatal unless the stream ends with `report: null`.
|
|
56
|
+
*/
|
|
57
|
+
export declare const SCAN_ERROR_CODES: readonly ["scanner_outdated", "scan_failed"];
|
|
58
|
+
export type ScanErrorCode = (typeof SCAN_ERROR_CODES)[number];
|
|
59
|
+
/** Project ids: "p_" + 12 lowercase hex (pid() in scan-progress.sh). */
|
|
60
|
+
export declare const SCAN_PROJECT_ID_PATTERN: RegExp;
|
|
61
|
+
export interface ScanSourceInfo {
|
|
62
|
+
id: string;
|
|
63
|
+
label: string;
|
|
64
|
+
}
|
|
65
|
+
interface ScanEventBase {
|
|
66
|
+
v: typeof SCAN_EVENT_VERSION;
|
|
67
|
+
}
|
|
68
|
+
/** First line: the sources present on this machine, in scan order. */
|
|
69
|
+
export interface ScanStartEvent extends ScanEventBase {
|
|
70
|
+
type: "start";
|
|
71
|
+
sources: ScanSourceInfo[];
|
|
72
|
+
}
|
|
73
|
+
/** Source lifecycle: `scanning` once, then one of done/skipped/error. */
|
|
74
|
+
export interface ScanSourceEvent extends ScanEventBase {
|
|
75
|
+
type: "source";
|
|
76
|
+
id: string;
|
|
77
|
+
status: ScanSourceStatus;
|
|
78
|
+
/** Final counts, on `done`. Each value repeats the last `count` event for that key. */
|
|
79
|
+
counts?: Record<string, number>;
|
|
80
|
+
/** Plain-words reason, on `skipped` and `error`. */
|
|
81
|
+
message?: string;
|
|
82
|
+
}
|
|
83
|
+
/** Running count; fires at 1-2-5 milestones and once with the final value. */
|
|
84
|
+
export interface ScanCountEvent extends ScanEventBase {
|
|
85
|
+
type: "count";
|
|
86
|
+
source: string;
|
|
87
|
+
key: string;
|
|
88
|
+
value: number;
|
|
89
|
+
}
|
|
90
|
+
/** A company from the deterministic rule pass. Emitted once per id. */
|
|
91
|
+
export interface ScanCompanyEvent extends ScanEventBase {
|
|
92
|
+
type: "company";
|
|
93
|
+
id: string;
|
|
94
|
+
name: string;
|
|
95
|
+
basis: ScanCompanyBasis;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* A project. Upsert by `id`: an id repeats only when a later rule pass
|
|
99
|
+
* attaches a company to a project that had `company: null`.
|
|
100
|
+
*/
|
|
101
|
+
export interface ScanProjectEvent extends ScanEventBase {
|
|
102
|
+
type: "project";
|
|
103
|
+
/** "p_" + 12 lowercase hex, the same for the same project on any machine. */
|
|
104
|
+
id: string;
|
|
105
|
+
/** Folder or repository basename; never contains "/". */
|
|
106
|
+
name: string;
|
|
107
|
+
company: string | null;
|
|
108
|
+
basis: ScanProjectBasis;
|
|
109
|
+
}
|
|
110
|
+
/** Non-fatal problem, in plain words. */
|
|
111
|
+
export interface ScanErrorEvent extends ScanEventBase {
|
|
112
|
+
type: "error";
|
|
113
|
+
source: string;
|
|
114
|
+
message: string;
|
|
115
|
+
/** Stable code (SCAN_ERROR_CODES). Always set on hq-cli's own errors. */
|
|
116
|
+
code?: ScanErrorCode;
|
|
117
|
+
}
|
|
118
|
+
export interface ScanSummary {
|
|
119
|
+
companies: number;
|
|
120
|
+
projects: number;
|
|
121
|
+
sessions: number;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Last line. `report` is the absolute path of report.json, or `null` when
|
|
125
|
+
* hq-cli could not run the scan (then `summary` is all zeros and the command
|
|
126
|
+
* exits non-zero).
|
|
127
|
+
*/
|
|
128
|
+
export interface ScanDoneEvent extends ScanEventBase {
|
|
129
|
+
type: "done";
|
|
130
|
+
report: string | null;
|
|
131
|
+
summary: ScanSummary;
|
|
132
|
+
}
|
|
133
|
+
export type ScanEvent = ScanStartEvent | ScanSourceEvent | ScanCountEvent | ScanCompanyEvent | ScanProjectEvent | ScanErrorEvent | ScanDoneEvent;
|
|
134
|
+
export {};
|
|
135
|
+
//# sourceMappingURL=events.d.ts.map
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Streaming context-scan events (contract v1).
|
|
3
|
+
*
|
|
4
|
+
* `hq import scan --json --stream` relays these as JSON Lines, one object per
|
|
5
|
+
* line. They are produced by the HQ import-context scanner
|
|
6
|
+
* (`.claude/skills/import-context/scan.sh --progress-json` in an HQ tree) and
|
|
7
|
+
* validated here before they reach stdout. The HQ desktop first-run setup
|
|
8
|
+
* renders them as a knowledge tree that grows while the scan runs.
|
|
9
|
+
*
|
|
10
|
+
* The canonical contract lives next to the scanner
|
|
11
|
+
* (`.claude/skills/import-context/progress-json.md` in hq-core) and in
|
|
12
|
+
* `docs/import-scan-stream.md` in this repo. The JSON Schema is
|
|
13
|
+
* `scan-event.schema.json` in this directory (shipped next to the compiled
|
|
14
|
+
* module as `dist/lib/import-scan/scan-event.schema.json`).
|
|
15
|
+
*
|
|
16
|
+
* Desktop consumers can import these types from
|
|
17
|
+
* `@indigoai-us/hq-cli/dist/lib/import-scan/events.js` or mirror them.
|
|
18
|
+
*
|
|
19
|
+
* Versioning: within v1, fields, source ids, count keys and messages may be
|
|
20
|
+
* added. Consumers ignore unknown fields and must not parse message text.
|
|
21
|
+
* Removing or retyping a field, or adding a new `type`, `status` or `basis`
|
|
22
|
+
* value, bumps `v`.
|
|
23
|
+
*/
|
|
24
|
+
export const SCAN_EVENT_VERSION = 1;
|
|
25
|
+
/** Event types, in the order a stream introduces them. */
|
|
26
|
+
export const SCAN_EVENT_TYPES = [
|
|
27
|
+
"start",
|
|
28
|
+
"source",
|
|
29
|
+
"count",
|
|
30
|
+
"company",
|
|
31
|
+
"project",
|
|
32
|
+
"error",
|
|
33
|
+
"done",
|
|
34
|
+
];
|
|
35
|
+
export const SCAN_SOURCE_STATUSES = ["scanning", "done", "skipped", "error"];
|
|
36
|
+
export const SCAN_COMPANY_BASES = ["hq-company", "repo-org", "folder"];
|
|
37
|
+
export const SCAN_PROJECT_BASES = ["repo", "claude-code-cwd", "codex-cwd"];
|
|
38
|
+
/**
|
|
39
|
+
* Source ids the v1 scanner emits, in scan order. Other ids may appear in
|
|
40
|
+
* later v1 scanners; show them by their `label`.
|
|
41
|
+
*/
|
|
42
|
+
export const KNOWN_SCAN_SOURCE_IDS = [
|
|
43
|
+
"hq",
|
|
44
|
+
"repos",
|
|
45
|
+
"claude-code",
|
|
46
|
+
"codex",
|
|
47
|
+
"grok",
|
|
48
|
+
"claude-ai",
|
|
49
|
+
"artifacts",
|
|
50
|
+
];
|
|
51
|
+
/**
|
|
52
|
+
* Source id used on `error` events that hq-cli itself emits (HQ not found,
|
|
53
|
+
* scanner missing or too old, scanner crashed). Never listed in `start`.
|
|
54
|
+
*/
|
|
55
|
+
export const SCAN_CLI_ERROR_SOURCE = "scanner";
|
|
56
|
+
/**
|
|
57
|
+
* Stable machine-readable codes on `error` events. hq-cli sets one on every
|
|
58
|
+
* error it emits itself (source "scanner"); scanner-emitted errors may omit it.
|
|
59
|
+
*
|
|
60
|
+
* scanner_outdated the HQ tree's scanner is missing or predates
|
|
61
|
+
* --progress-json; updating HQ fixes it
|
|
62
|
+
* scan_failed any other reason the scan could not run or finish
|
|
63
|
+
* (HQ not found, scanner crashed, cancelled, non-zero exit)
|
|
64
|
+
*
|
|
65
|
+
* Consumers branch on `code`, never on `message`. Treat an error without a
|
|
66
|
+
* code as non-fatal unless the stream ends with `report: null`.
|
|
67
|
+
*/
|
|
68
|
+
export const SCAN_ERROR_CODES = ["scanner_outdated", "scan_failed"];
|
|
69
|
+
/** Project ids: "p_" + 12 lowercase hex (pid() in scan-progress.sh). */
|
|
70
|
+
export const SCAN_PROJECT_ID_PATTERN = /^p_[0-9a-f]{12}$/;
|
|
71
|
+
//# sourceMappingURL=events.js.map
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runs the HQ import-context scanner with `--progress-json` and relays its
|
|
3
|
+
* JSON Lines event stream (contract: ./events.ts).
|
|
4
|
+
*
|
|
5
|
+
* The scanner is the HQ tree's own copy at
|
|
6
|
+
* `<hq root>/.claude/skills/import-context/scan.sh`, the same place the
|
|
7
|
+
* /import-context skill runs it from. The scan is local only: the scanner
|
|
8
|
+
* reads the filesystem and writes one report.json; nothing here touches the
|
|
9
|
+
* network.
|
|
10
|
+
*
|
|
11
|
+
* HQ root: `--hq-root` when given, else the root the HQ installer recorded in
|
|
12
|
+
* `~/.hq/menubar.json` (`hqPath`). Nothing else is trusted: no walk up from
|
|
13
|
+
* the current folder and no `$HQ_ROOT`, because this command runs a script
|
|
14
|
+
* from that tree.
|
|
15
|
+
*
|
|
16
|
+
* Output modes:
|
|
17
|
+
* stream every valid event, rebuilt from its v1 fields (invalid lines are
|
|
18
|
+
* dropped with a warning on stderr that never echoes the line)
|
|
19
|
+
* final only the final `done` object
|
|
20
|
+
* human short progress lines and a summary
|
|
21
|
+
*
|
|
22
|
+
* The `done` event is held until the scanner exits. When the scan cannot run
|
|
23
|
+
* or does not finish cleanly (HQ not found, scanner missing or too old,
|
|
24
|
+
* scanner exited non-zero or without `done`, cancelled), an `error` event
|
|
25
|
+
* (source "scanner") comes first, then `done` (zeroed with `report: null`
|
|
26
|
+
* unless the scanner produced one), and the command exits non-zero.
|
|
27
|
+
*
|
|
28
|
+
* Process handling: the scanner runs in its own process group. SIGINT/SIGTERM
|
|
29
|
+
* to hq-cli are forwarded to the whole group and escalated to SIGKILL after
|
|
30
|
+
* `killTimeoutMs`. When the scanner exits, anything still left in its group
|
|
31
|
+
* is terminated so an orphan holding stdout open cannot stall the command.
|
|
32
|
+
*
|
|
33
|
+
* Scanner stderr is not forwarded verbatim: each line is passed through with
|
|
34
|
+
* paths and token-like strings replaced, at most MAX_STDERR_LINES lines, then
|
|
35
|
+
* a count of the rest.
|
|
36
|
+
*/
|
|
37
|
+
export declare const SCANNER_RELATIVE_PATH: string;
|
|
38
|
+
export type ImportScanMode = "stream" | "final" | "human";
|
|
39
|
+
export interface ImportScanOptions {
|
|
40
|
+
mode: ImportScanMode;
|
|
41
|
+
/** Explicit HQ root (--hq-root). */
|
|
42
|
+
hqRoot?: string;
|
|
43
|
+
/** Home directory used to find ~/.hq/menubar.json (tests). */
|
|
44
|
+
home?: string;
|
|
45
|
+
/** Report path. Defaults to <hq>/workspace/imports/<scan id>/report.json. */
|
|
46
|
+
output?: string;
|
|
47
|
+
scopes?: string[];
|
|
48
|
+
claudeExport?: string;
|
|
49
|
+
noDefaultScopes?: boolean;
|
|
50
|
+
/** Shell used to run scan.sh (default "bash"). */
|
|
51
|
+
bash?: string;
|
|
52
|
+
env?: NodeJS.ProcessEnv;
|
|
53
|
+
now?: () => Date;
|
|
54
|
+
/** Grace period between forwarding a signal and SIGKILL (default 5000). */
|
|
55
|
+
killTimeoutMs?: number;
|
|
56
|
+
/** Cancels the scan like SIGTERM (for embedding callers and tests). */
|
|
57
|
+
abortSignal?: AbortSignal;
|
|
58
|
+
}
|
|
59
|
+
export interface ImportScanIo {
|
|
60
|
+
stdout: NodeJS.WritableStream;
|
|
61
|
+
stderr: NodeJS.WritableStream;
|
|
62
|
+
}
|
|
63
|
+
/** UTC scan id in the /import-context skill's format: YYYYMMDDTHHMMSSZ. */
|
|
64
|
+
export declare function scanId(date: Date): string;
|
|
65
|
+
export declare class HqRootError extends Error {
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* --hq-root, else the installer-recorded root in ~/.hq/menubar.json. Throws
|
|
69
|
+
* HqRootError with a plain-words message otherwise.
|
|
70
|
+
*/
|
|
71
|
+
export declare function resolveScanHqRoot(opts: {
|
|
72
|
+
hqRoot?: string;
|
|
73
|
+
home?: string;
|
|
74
|
+
}): string;
|
|
75
|
+
/**
|
|
76
|
+
* Replace paths and token-like strings in one scanner stderr line. A path
|
|
77
|
+
* runs from its first "/" (or a leading ~ / $HOME) to the next quote or the
|
|
78
|
+
* end of the line, because folder names may contain spaces: stopping at
|
|
79
|
+
* whitespace would leak the rest of "/Users/x/My Project/secret".
|
|
80
|
+
*/
|
|
81
|
+
export declare function redactScannerLine(line: string): string;
|
|
82
|
+
/** Run the scan. Resolves to the process exit code. */
|
|
83
|
+
export declare function runImportScan(opts: ImportScanOptions, io: ImportScanIo): Promise<number>;
|
|
84
|
+
//# sourceMappingURL=run.d.ts.map
|