@nurix/nustack 0.23.8 → 0.23.10
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/commands/census.js +48 -0
- package/dist/commands/gate.js +75 -0
- package/dist/commands/telemetry.js +37 -5
- package/dist/index.js +8 -451
- package/dist/lib/agent/claudeHooks.js +39 -19
- package/dist/lib/census/attribute.js +120 -0
- package/dist/lib/census/report.js +108 -0
- package/dist/lib/census/transcripts.js +62 -0
- package/dist/lib/desktop_settings.js +23 -2
- package/dist/lib/filebeat_installer.js +5 -1
- package/dist/lib/gate/bashRead.js +201 -0
- package/dist/lib/gate/estimate.js +90 -0
- package/dist/lib/gate/hook.js +79 -0
- package/dist/lib/gate/outline.js +146 -0
- package/dist/lib/gate/readGate.js +126 -0
- package/dist/lib/gate/settings.js +24 -0
- package/dist/lib/gate/subcommands.js +1 -0
- package/dist/lib/local_store/database.js +42 -1
- package/dist/lib/local_store/index.js +1 -0
- package/dist/lib/local_store/migrations.js +36 -0
- package/dist/lib/local_store/routing.js +137 -0
- package/dist/lib/nustack_client.js +4 -2
- package/dist/lib/session_telemetry.js +108 -40
- package/dist/lib/telemetry/config_renderer.js +4 -4
- package/dist/lib/telemetry/environment.js +2 -0
- package/dist/lib/telemetry/health_report.js +10 -3
- package/dist/lib/telemetry/part_state.js +168 -0
- package/dist/program.js +491 -0
- package/package.json +2 -2
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { attributeTranscript } from "../lib/census/attribute.js";
|
|
2
|
+
import { CensusAccumulator, renderCensusText } from "../lib/census/report.js";
|
|
3
|
+
import { discoverSessions, readLines } from "../lib/census/transcripts.js";
|
|
4
|
+
import { printJsonSuccess } from "../lib/json_output.js";
|
|
5
|
+
class CensusArgumentError extends Error {
|
|
6
|
+
code = "CENSUS_ARGUMENT_INVALID";
|
|
7
|
+
constructor(message) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = "CensusArgumentError";
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
function parseSince(value) {
|
|
13
|
+
if (value === undefined)
|
|
14
|
+
return undefined;
|
|
15
|
+
const instant = /^\d{4}-\d{2}-\d{2}$/.test(value) ? Date.parse(`${value}T00:00:00Z`) : Number.NaN;
|
|
16
|
+
if (Number.isNaN(instant))
|
|
17
|
+
throw new CensusArgumentError(`--since takes a date as YYYY-MM-DD, not "${value}".`);
|
|
18
|
+
return instant;
|
|
19
|
+
}
|
|
20
|
+
function parseMaxSessions(value) {
|
|
21
|
+
if (value === undefined)
|
|
22
|
+
return undefined;
|
|
23
|
+
if (!/^\d+$/.test(value) || Number.parseInt(value, 10) < 1) {
|
|
24
|
+
throw new CensusArgumentError(`--max-sessions takes a positive integer, not "${value}".`);
|
|
25
|
+
}
|
|
26
|
+
return Number.parseInt(value, 10);
|
|
27
|
+
}
|
|
28
|
+
export async function runCensus(options) {
|
|
29
|
+
const sessions = await discoverSessions({
|
|
30
|
+
root: options.root,
|
|
31
|
+
sinceMs: parseSince(options.since),
|
|
32
|
+
maxSessions: parseMaxSessions(options.maxSessions),
|
|
33
|
+
});
|
|
34
|
+
const census = new CensusAccumulator();
|
|
35
|
+
for (const session of sessions) {
|
|
36
|
+
census.addSession();
|
|
37
|
+
census.addTranscript(await attributeTranscript(readLines(session.mainPath), "main"), "main");
|
|
38
|
+
for (const sidechainPath of session.sidechainPaths) {
|
|
39
|
+
census.addTranscript(await attributeTranscript(readLines(sidechainPath), "sidechain"), "sidechain");
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const report = census.finish();
|
|
43
|
+
if (options.json) {
|
|
44
|
+
printJsonSuccess("census", report);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
console.log(renderCensusText(report));
|
|
48
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { hasManagedHook, installManagedHook, removeManagedHook } from "../lib/agent/claudeHooks.js";
|
|
4
|
+
import { readDesktopSettings } from "../lib/desktop_settings.js";
|
|
5
|
+
import { loadOutlineLibrary } from "../lib/gate/outline.js";
|
|
6
|
+
import { resolveThreshold } from "../lib/gate/settings.js";
|
|
7
|
+
import { printJsonSuccess } from "../lib/json_output.js";
|
|
8
|
+
import { readStoreVersion } from "../lib/local_store/database.js";
|
|
9
|
+
export const GATE_HOOK_TIMEOUT_SECONDS = 10;
|
|
10
|
+
export const GATE_HOOKS = [
|
|
11
|
+
{ matcher: "Read", command: "nustack gate read" },
|
|
12
|
+
{ matcher: "Bash", command: "nustack gate bash" },
|
|
13
|
+
];
|
|
14
|
+
const GATE_EVENT = "PreToolUse";
|
|
15
|
+
function claudeSettingsPath() {
|
|
16
|
+
return path.join(os.homedir(), ".claude", "settings.json");
|
|
17
|
+
}
|
|
18
|
+
export async function runGateInstall() {
|
|
19
|
+
let isChanged = false;
|
|
20
|
+
for (const hook of GATE_HOOKS) {
|
|
21
|
+
const { changed } = await installManagedHook(claudeSettingsPath(), GATE_EVENT, hook.matcher, hook.command, {
|
|
22
|
+
timeout: GATE_HOOK_TIMEOUT_SECONDS,
|
|
23
|
+
});
|
|
24
|
+
isChanged ||= changed;
|
|
25
|
+
}
|
|
26
|
+
console.log(isChanged
|
|
27
|
+
? "Installed the large-read gate's Read and Bash hooks."
|
|
28
|
+
: "Installed the large-read gate's Read and Bash hooks (already installed — no change).");
|
|
29
|
+
}
|
|
30
|
+
export async function runGateRemove() {
|
|
31
|
+
let isRemoved = false;
|
|
32
|
+
for (const hook of GATE_HOOKS) {
|
|
33
|
+
const { removed } = await removeManagedHook(claudeSettingsPath(), GATE_EVENT, hook.matcher);
|
|
34
|
+
isRemoved ||= removed;
|
|
35
|
+
}
|
|
36
|
+
console.log(isRemoved
|
|
37
|
+
? "Removed the large-read gate's Read and Bash hooks."
|
|
38
|
+
: "Removed the large-read gate's Read and Bash hooks (already absent — no change).");
|
|
39
|
+
}
|
|
40
|
+
export async function readGateStatus() {
|
|
41
|
+
const settingsPath = claudeSettingsPath();
|
|
42
|
+
const installed = {
|
|
43
|
+
read: await hasManagedHook(settingsPath, GATE_EVENT, "Read"),
|
|
44
|
+
bash: await hasManagedHook(settingsPath, GATE_EVENT, "Bash"),
|
|
45
|
+
};
|
|
46
|
+
const { localReader } = readDesktopSettings().routing;
|
|
47
|
+
const isInstalled = installed.read && installed.bash;
|
|
48
|
+
const isUninstalled = !installed.read && !installed.bash;
|
|
49
|
+
const { thresholdTokens, thresholdSource } = resolveThreshold();
|
|
50
|
+
const library = await loadOutlineLibrary();
|
|
51
|
+
return {
|
|
52
|
+
installed,
|
|
53
|
+
localReader,
|
|
54
|
+
isInSync: localReader ? isInstalled : isUninstalled,
|
|
55
|
+
thresholdTokens,
|
|
56
|
+
thresholdSource,
|
|
57
|
+
outline: library !== null && typeof library.outlineSource === "function" ? "tree-sitter" : "fallback",
|
|
58
|
+
storeVersion: await readStoreVersion(),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
export async function runGateStatus(options) {
|
|
62
|
+
const status = await readGateStatus();
|
|
63
|
+
if (options.json) {
|
|
64
|
+
printJsonSuccess("gate status", status);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const onOff = (isOn) => (isOn ? "installed" : "not installed");
|
|
68
|
+
console.log([
|
|
69
|
+
`Read hook: ${onOff(status.installed.read)} · Bash hook: ${onOff(status.installed.bash)}`,
|
|
70
|
+
`Local Reader switch: ${status.localReader ? "on" : "off"} (${status.isInSync ? "in sync with the hooks" : "out of sync with the hooks — run `nustack gate install` or `nustack gate remove`"})`,
|
|
71
|
+
`Threshold: ${status.thresholdTokens} tokens (from ${status.thresholdSource})`,
|
|
72
|
+
`Outline: ${status.outline}`,
|
|
73
|
+
`Local store version: ${status.storeVersion ?? "no store"}`,
|
|
74
|
+
].join("\n"));
|
|
75
|
+
}
|
|
@@ -6,7 +6,11 @@ import { getBindingByRepositoryRoot, listActiveBindings, setBindingTelemetry, }
|
|
|
6
6
|
import { upsertFilebeatState } from "../lib/local_store/filebeat_state.js";
|
|
7
7
|
import { ensureFilebeatBinary } from "../lib/filebeat_installer.js";
|
|
8
8
|
import { collectTelemetryStatus } from "../lib/telemetry/health_report.js";
|
|
9
|
-
import { ensureBindingTelemetry, setLocalOutput, switchMachineOff } from "../lib/session_telemetry.js";
|
|
9
|
+
import { ensureBindingTelemetry, reconcileAllBindings, setLocalOutput, switchMachineOff } from "../lib/session_telemetry.js";
|
|
10
|
+
import { readPartState, telemetryReconcileLockPath } from "../lib/telemetry/part_state.js";
|
|
11
|
+
import { TELEMETRY_CHILD_VARIABLE } from "../lib/telemetry/environment.js";
|
|
12
|
+
import { isFlagSet } from "../lib/records/environment.js";
|
|
13
|
+
import { acquireDrainLock, releaseDrainLock } from "../lib/records/drain.js";
|
|
10
14
|
import { resolveRepository } from "../lib/git-sync/core/repository.js";
|
|
11
15
|
function resolveTargetBinding(store, options) {
|
|
12
16
|
if (options.device)
|
|
@@ -28,13 +32,37 @@ export async function runTelemetry(action, options) {
|
|
|
28
32
|
const store = await openLocalStore();
|
|
29
33
|
try {
|
|
30
34
|
const verbose = { localVerbose: options.localVerbose === true };
|
|
35
|
+
if (action === "reconcile") {
|
|
36
|
+
const isChild = isFlagSet(TELEMETRY_CHILD_VARIABLE);
|
|
37
|
+
const lockPath = telemetryReconcileLockPath();
|
|
38
|
+
if (isChild && !acquireDrainLock(lockPath))
|
|
39
|
+
return;
|
|
40
|
+
try {
|
|
41
|
+
const result = await reconcileAllBindings({ baseUrl: resolveNuStackUrl(options), store });
|
|
42
|
+
if (options.quiet)
|
|
43
|
+
return;
|
|
44
|
+
if (json) {
|
|
45
|
+
printJsonSuccess("telemetry reconcile", { scope: "device", ...result });
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
p.intro("nustack telemetry reconcile");
|
|
49
|
+
p.log.success(`Reconciled ${result.reconciled} folder${result.reconciled === 1 ? "" : "s"}; parts on ${result.parts.on}, off ${result.parts.off}, unknown ${result.parts.unknown}${result.conflict ? "; two telemetry destinations conflict, so nothing ships" : ""}.`);
|
|
50
|
+
p.outro("Telemetry reconciled.");
|
|
51
|
+
}
|
|
52
|
+
finally {
|
|
53
|
+
if (isChild)
|
|
54
|
+
releaseDrainLock(lockPath);
|
|
55
|
+
}
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
31
58
|
if (action === "status") {
|
|
32
59
|
const status = await collectTelemetryStatus(store, undefined, verbose);
|
|
33
60
|
if (json) {
|
|
34
61
|
printJsonSuccess("telemetry status", status);
|
|
35
62
|
return;
|
|
36
63
|
}
|
|
37
|
-
const shipping = status.folders.filter((f) => f.enabled).length;
|
|
64
|
+
const shipping = status.folders.filter((f) => f.enabled && f.shipping).length;
|
|
65
|
+
const partsOff = status.folders.filter((f) => f.partState === "off").length;
|
|
38
66
|
p.intro("nustack telemetry status");
|
|
39
67
|
p.note([
|
|
40
68
|
`telemetry_enabled: ${status.telemetryEnabled}`,
|
|
@@ -45,6 +73,7 @@ export async function runTelemetry(action, options) {
|
|
|
45
73
|
`install state: ${status.filebeatState}${status.lastErrorCode ? ` (${status.lastErrorCode})` : ""}`,
|
|
46
74
|
`config version: ${status.desiredConfigVersion}`,
|
|
47
75
|
`folders: ${shipping} of ${status.folders.length} shipping`,
|
|
76
|
+
`telemetry part: ${partsOff === 0 ? "on for every folder's project" : `off for ${partsOff} folder${partsOff === 1 ? "" : "s"}`}`,
|
|
48
77
|
`last cloud report: ${status.lastHealthAtMs ? new Date(status.lastHealthAtMs).toISOString() : "never"}`,
|
|
49
78
|
].join("\n"), "Telemetry");
|
|
50
79
|
p.outro("Telemetry status.");
|
|
@@ -99,9 +128,12 @@ export async function runTelemetry(action, options) {
|
|
|
99
128
|
return;
|
|
100
129
|
}
|
|
101
130
|
p.intro(`nustack telemetry ${action}`);
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
131
|
+
const isPartOff = readPartState().projects[target.projectId]?.state === "off";
|
|
132
|
+
p.log.success(enabled && isPartOff
|
|
133
|
+
? `${folderLabel(target)} is switched on, but the tenant's telemetry part is off for this project; this folder ships when it is switched back on.`
|
|
134
|
+
: enabled
|
|
135
|
+
? `${folderLabel(target)} is shipping. Other folders are unchanged.`
|
|
136
|
+
: `${folderLabel(target)} has stopped shipping; nothing it records while off will ever ship. Other folders are unchanged.`);
|
|
105
137
|
p.outro(`Run \`nustack telemetry ${enabled ? "disable" : "enable"}\` here to reverse it.`);
|
|
106
138
|
return;
|
|
107
139
|
}
|