@nurix/nustack 0.10.0-dev.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.
Files changed (131) hide show
  1. package/README.md +77 -0
  2. package/dist/commands/add.js +130 -0
  3. package/dist/commands/agent.js +100 -0
  4. package/dist/commands/bootstrap.js +54 -0
  5. package/dist/commands/describe.js +34 -0
  6. package/dist/commands/device.js +159 -0
  7. package/dist/commands/docs.js +119 -0
  8. package/dist/commands/doctor.js +284 -0
  9. package/dist/commands/graph.js +34 -0
  10. package/dist/commands/handoff.js +108 -0
  11. package/dist/commands/init.js +280 -0
  12. package/dist/commands/lane.js +12 -0
  13. package/dist/commands/list.js +20 -0
  14. package/dist/commands/login.js +134 -0
  15. package/dist/commands/logout.js +44 -0
  16. package/dist/commands/open.js +54 -0
  17. package/dist/commands/organization.js +114 -0
  18. package/dist/commands/review.js +152 -0
  19. package/dist/commands/search.js +57 -0
  20. package/dist/commands/sessions.js +62 -0
  21. package/dist/commands/sync.js +85 -0
  22. package/dist/commands/telemetry.js +112 -0
  23. package/dist/commands/toolkit.js +167 -0
  24. package/dist/commands/validate.js +67 -0
  25. package/dist/commands/workspace-sync.js +645 -0
  26. package/dist/commands/workspace.js +479 -0
  27. package/dist/index.js +357 -0
  28. package/dist/lib/agent/claudeHooks.js +77 -0
  29. package/dist/lib/auth_grant.js +398 -0
  30. package/dist/lib/auth_recovery.js +49 -0
  31. package/dist/lib/browser_opener.js +24 -0
  32. package/dist/lib/device_identity.js +28 -0
  33. package/dist/lib/device_keychain.js +101 -0
  34. package/dist/lib/env_file.js +75 -0
  35. package/dist/lib/etna/manager.js +43 -0
  36. package/dist/lib/etna/profile.js +41 -0
  37. package/dist/lib/etna/reconcile.js +77 -0
  38. package/dist/lib/filebeat_installer.js +246 -0
  39. package/dist/lib/git-sync/core/commit_range.js +37 -0
  40. package/dist/lib/git-sync/core/engine.js +263 -0
  41. package/dist/lib/git-sync/core/git_runner.js +137 -0
  42. package/dist/lib/git-sync/core/operation_state.js +22 -0
  43. package/dist/lib/git-sync/core/path_normalizer.js +98 -0
  44. package/dist/lib/git-sync/core/reconciliation_lock.js +103 -0
  45. package/dist/lib/git-sync/core/repository.js +49 -0
  46. package/dist/lib/git-sync/core/watch_stream.js +86 -0
  47. package/dist/lib/git-sync/core/watcher.js +61 -0
  48. package/dist/lib/git-sync/inbound/applier.js +131 -0
  49. package/dist/lib/git-sync/inbound/apply_commit.js +225 -0
  50. package/dist/lib/git-sync/inbound/approval.js +117 -0
  51. package/dist/lib/git-sync/inbound/content_decision.js +148 -0
  52. package/dist/lib/git-sync/inbound/download.js +223 -0
  53. package/dist/lib/git-sync/inbound/event_sync.js +133 -0
  54. package/dist/lib/git-sync/inbound/index.js +10 -0
  55. package/dist/lib/git-sync/inbound/notification.js +39 -0
  56. package/dist/lib/git-sync/inbound/state_fingerprint.js +32 -0
  57. package/dist/lib/git-sync/inbound/technical_wait.js +39 -0
  58. package/dist/lib/git-sync/inbound/undo.js +198 -0
  59. package/dist/lib/git-sync/outbound/extraction.js +107 -0
  60. package/dist/lib/git-sync/outbound/history_decision.js +112 -0
  61. package/dist/lib/git-sync/outbound/index.js +26 -0
  62. package/dist/lib/git-sync/outbound/manifest_builder.js +58 -0
  63. package/dist/lib/git-sync/outbound/publisher.js +51 -0
  64. package/dist/lib/git-sync/outbound/queue.js +123 -0
  65. package/dist/lib/git-sync/outbound/state.js +12 -0
  66. package/dist/lib/git-sync/outbound/upload_client.js +141 -0
  67. package/dist/lib/git-sync/recovery/bundle.js +56 -0
  68. package/dist/lib/git-sync/recovery/crypto.js +114 -0
  69. package/dist/lib/git-sync/recovery/index.js +3 -0
  70. package/dist/lib/git-sync/recovery/store.js +55 -0
  71. package/dist/lib/git-sync/whitelist/index.js +4 -0
  72. package/dist/lib/git-sync/whitelist/matcher.js +62 -0
  73. package/dist/lib/git-sync/whitelist/store.js +99 -0
  74. package/dist/lib/git-sync/whitelist/types.js +46 -0
  75. package/dist/lib/git-sync/whitelist/validation.js +60 -0
  76. package/dist/lib/graph/answer.js +171 -0
  77. package/dist/lib/graph/freshness.js +29 -0
  78. package/dist/lib/graph/library.js +19 -0
  79. package/dist/lib/guards.js +106 -0
  80. package/dist/lib/interactive.js +12 -0
  81. package/dist/lib/json_output.js +8 -0
  82. package/dist/lib/lanes/claudeAdapter.js +135 -0
  83. package/dist/lib/lanes/frames.js +73 -0
  84. package/dist/lib/lanes/supervisor.js +247 -0
  85. package/dist/lib/lanes/worktree.js +33 -0
  86. package/dist/lib/local_store/accepted_uploads.js +49 -0
  87. package/dist/lib/local_store/bindings.js +163 -0
  88. package/dist/lib/local_store/commit_origins.js +25 -0
  89. package/dist/lib/local_store/database.js +71 -0
  90. package/dist/lib/local_store/device_state.js +72 -0
  91. package/dist/lib/local_store/filebeat_state.js +133 -0
  92. package/dist/lib/local_store/handoff_applications.js +166 -0
  93. package/dist/lib/local_store/history_decisions.js +48 -0
  94. package/dist/lib/local_store/index.js +14 -0
  95. package/dist/lib/local_store/migrations.js +240 -0
  96. package/dist/lib/local_store/paths.js +24 -0
  97. package/dist/lib/local_store/pending_uploads.js +76 -0
  98. package/dist/lib/local_store/recovery_bundles.js +66 -0
  99. package/dist/lib/local_store/sync_vocabulary.js +33 -0
  100. package/dist/lib/local_store/workspace_events_cache.js +54 -0
  101. package/dist/lib/loopback.js +78 -0
  102. package/dist/lib/manifest.js +83 -0
  103. package/dist/lib/nustack_client.js +547 -0
  104. package/dist/lib/nustack_store.js +17 -0
  105. package/dist/lib/onboarding/attach_existing_git.js +65 -0
  106. package/dist/lib/onboarding/binding.js +91 -0
  107. package/dist/lib/onboarding/folder_state.js +99 -0
  108. package/dist/lib/onboarding/git_init.js +10 -0
  109. package/dist/lib/onboarding/handoff_apply.js +68 -0
  110. package/dist/lib/onboarding/init_fresh_folder.js +100 -0
  111. package/dist/lib/onboarding/manifest_writer.js +161 -0
  112. package/dist/lib/onboarding/web_first_attach.js +133 -0
  113. package/dist/lib/package_manager.js +23 -0
  114. package/dist/lib/review/client.js +66 -0
  115. package/dist/lib/schema_validator.js +24 -0
  116. package/dist/lib/secure_file.js +10 -0
  117. package/dist/lib/session_telemetry.js +263 -0
  118. package/dist/lib/sessions/client.js +48 -0
  119. package/dist/lib/sessions/outsideSessions.js +54 -0
  120. package/dist/lib/sse_framing.js +48 -0
  121. package/dist/lib/telemetry/config_renderer.js +175 -0
  122. package/dist/lib/telemetry/health_report.js +173 -0
  123. package/dist/lib/telemetry/provider_paths.js +38 -0
  124. package/dist/lib/telemetry/redaction_policy.js +90 -0
  125. package/dist/lib/telemetry/reload.js +84 -0
  126. package/dist/lib/workspace_context.js +121 -0
  127. package/dist/spec_schema.json +422 -0
  128. package/dist/wire/auth.js +1 -0
  129. package/dist/wire/sync-paths.js +25 -0
  130. package/dist/wire/telemetry.js +1 -0
  131. package/package.json +49 -0
@@ -0,0 +1,175 @@
1
+ import { createHash } from "node:crypto";
2
+ import { assertSafeApiKey, assertSafeElkUrl, assertSafeIndexName } from "../guards.js";
3
+ import { claudeProjectDir, codexSessionsDir } from "./provider_paths.js";
4
+ import { REDACTION_POLICY_VERSION, SLASH_COMMAND_CLOSE, SLASH_COMMAND_OPEN, providerDropFields, } from "./redaction_policy.js";
5
+ export const TELEMETRY_CONFIG_VERSION = 2;
6
+ export const ENVELOPE_SCHEMA_VERSION = 2;
7
+ export function renderTemplate(template, vars) {
8
+ return template.replace(/\{\{\s*([a-zA-Z0-9_]+)\s*\}\}/g, (_match, name) => {
9
+ const value = vars[name];
10
+ if (value === undefined)
11
+ throw new Error(`template variable not provided: ${name}`);
12
+ assertNoControlChars(name, value);
13
+ return value;
14
+ });
15
+ }
16
+ function assertNoControlChars(label, value) {
17
+ for (const character of value) {
18
+ const code = character.codePointAt(0) ?? 0;
19
+ if (code < 0x20 || code === 0x7f) {
20
+ throw new Error(`template variable ${label} contains control characters`);
21
+ }
22
+ }
23
+ }
24
+ export function yamlScalar(value) {
25
+ assertNoControlChars("scalar", value);
26
+ return JSON.stringify(value);
27
+ }
28
+ export function claudeFilestreamId(repoRoot) {
29
+ const slug = repoRoot.replace(/[^a-zA-Z0-9]/g, "-").replace(/^-+/, "").toLowerCase();
30
+ return `claude-${slug}`;
31
+ }
32
+ export function desiredConfigHash(bytes) {
33
+ return createHash("sha256").update(bytes).digest("hex");
34
+ }
35
+ const CENTRAL_TEMPLATE = `# Managed by the nustack CLI — do not edit by hand.
36
+ filebeat.config.inputs:
37
+ enabled: true
38
+ path: \${path.config}/inputs.d/*.yml
39
+ reload.enabled: true
40
+ reload.period: 30s
41
+
42
+ # Set @timestamp to the session/prompt time (each line's own \`timestamp\` field).
43
+ processors:
44
+ - timestamp:
45
+ field: timestamp
46
+ layouts:
47
+ - '2006-01-02T15:04:05.999999999Z07:00'
48
+ - '2006-01-02T15:04:05Z07:00'
49
+ ignore_missing: true
50
+ ignore_failure: true
51
+
52
+ queue.mem:
53
+ events: 4096
54
+ flush.min_events: 512
55
+ flush.timeout: 5s
56
+
57
+ output.elasticsearch:
58
+ hosts: [{{elk_url}}]
59
+ api_key: {{api_key}}
60
+ index: {{index}}
61
+ ssl.verification_mode: full
62
+
63
+ setup.ilm.enabled: false
64
+ setup.template.name: {{index}}
65
+ setup.template.pattern: {{index_pattern}}
66
+
67
+ logging.level: warning
68
+ `;
69
+ export function renderCentralConfig(opts) {
70
+ const elkUrl = assertSafeElkUrl(opts.elkUrl);
71
+ const apiKey = assertSafeApiKey(opts.apiKey);
72
+ const index = assertSafeIndexName(opts.index);
73
+ return renderTemplate(CENTRAL_TEMPLATE, {
74
+ elk_url: yamlScalar(elkUrl),
75
+ api_key: yamlScalar(apiKey),
76
+ index: yamlScalar(index),
77
+ index_pattern: yamlScalar(`${index}-*`),
78
+ });
79
+ }
80
+ const BINDING_TEMPLATE = `# Managed by the nustack CLI — binding: {{binding_id_raw}}
81
+ - type: filestream
82
+ id: {{filestream_id}}
83
+ paths:
84
+ - {{claude_glob}}
85
+ parsers:
86
+ - ndjson:
87
+ target: ""
88
+ overwrite_keys: true
89
+ add_error_key: true
90
+ fields_under_root: true
91
+ fields:
92
+ nustack:
93
+ schema_version: {{schema_version}}
94
+ organization_id: {{organization_id}}
95
+ workspace_id: {{workspace_id}}
96
+ binding_id: {{binding_id}}
97
+ device_id: {{device_id}}
98
+ user_id: {{user_id}}
99
+ provider: {{provider}}
100
+ redaction_policy_version: {{redaction_policy_version}}
101
+ redaction_applied: true
102
+ processors:
103
+ - dissect:
104
+ tokenizer: "%{}{{slash_open}}%{slashCommand}{{slash_close}}%{}"
105
+ field: "message.content"
106
+ target_prefix: ""
107
+ ignore_failure: true
108
+ - drop_fields:
109
+ fields: [{{drop_fields}}]
110
+ ignore_missing: true
111
+ `;
112
+ export function renderBindingInput(binding) {
113
+ return renderTemplate(BINDING_TEMPLATE, {
114
+ binding_id_raw: binding.bindingId,
115
+ filestream_id: claudeFilestreamId(binding.repoRoot),
116
+ claude_glob: yamlScalar(`${claudeProjectDir(binding.repoRoot)}/**/*.jsonl`),
117
+ schema_version: String(ENVELOPE_SCHEMA_VERSION),
118
+ organization_id: yamlScalar(binding.organizationId),
119
+ workspace_id: yamlScalar(binding.workspaceId),
120
+ binding_id: yamlScalar(binding.bindingId),
121
+ device_id: yamlScalar(binding.deviceRef),
122
+ user_id: yamlScalar(binding.userId),
123
+ provider: yamlScalar("claude_code"),
124
+ redaction_policy_version: String(REDACTION_POLICY_VERSION),
125
+ slash_open: SLASH_COMMAND_OPEN,
126
+ slash_close: SLASH_COMMAND_CLOSE,
127
+ drop_fields: providerDropFields("claude_code").join(", "),
128
+ });
129
+ }
130
+ export function renderCodexSharedInput(bindings) {
131
+ if (bindings.length === 0) {
132
+ throw new Error("renderCodexSharedInput requires at least one binding");
133
+ }
134
+ const sorted = [...bindings].sort((a, b) => (a.bindingId < b.bindingId ? -1 : a.bindingId > b.bindingId ? 1 : 0));
135
+ const device = sorted[0];
136
+ const conditionals = sorted
137
+ .map((binding) => ` - if:
138
+ equals:
139
+ cwd: ${yamlScalar(binding.repoRoot)}
140
+ then:
141
+ - add_fields:
142
+ target: nustack
143
+ fields:
144
+ organization_id: ${yamlScalar(binding.organizationId)}
145
+ workspace_id: ${yamlScalar(binding.workspaceId)}
146
+ binding_id: ${yamlScalar(binding.bindingId)}`)
147
+ .join("\n");
148
+ const dropList = ["cwd", ...providerDropFields("codex")].join(", ");
149
+ return `# Managed by the nustack CLI — codex shared input (all bindings)
150
+ - type: filestream
151
+ id: codex-shared
152
+ paths:
153
+ - ${yamlScalar(`${codexSessionsDir()}/**/*.jsonl`)}
154
+ parsers:
155
+ - ndjson:
156
+ target: ""
157
+ overwrite_keys: true
158
+ add_error_key: true
159
+ fields_under_root: true
160
+ processors:
161
+ - add_fields:
162
+ target: nustack
163
+ fields:
164
+ schema_version: ${ENVELOPE_SCHEMA_VERSION}
165
+ device_id: ${yamlScalar(device.deviceRef)}
166
+ user_id: ${yamlScalar(device.userId)}
167
+ provider: ${yamlScalar("codex")}
168
+ redaction_policy_version: ${REDACTION_POLICY_VERSION}
169
+ redaction_applied: true
170
+ ${conditionals}
171
+ - drop_fields:
172
+ fields: [${dropList}]
173
+ ignore_missing: true
174
+ `;
175
+ }
@@ -0,0 +1,173 @@
1
+ import { existsSync } from "node:fs";
2
+ import { readFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { listActiveBindings } from "../local_store/bindings.js";
5
+ import { readDeviceState } from "../local_store/device_state.js";
6
+ import { listActivityBindings, readFilebeatState, upsertFilebeatState, } from "../local_store/filebeat_state.js";
7
+ import { desiredFilebeatVersion, filebeatBinPath, filebeatDir, inputsDir, observedFilebeatVersion, probeServiceRegistered, } from "../filebeat_installer.js";
8
+ import { REDACTION_POLICY_VERSION, } from "./redaction_policy.js";
9
+ import { TELEMETRY_CONFIG_VERSION, desiredConfigHash, renderBindingInput, } from "./config_renderer.js";
10
+ import { discoverProviderBindings } from "./provider_paths.js";
11
+ import { readObservedConfigBytes } from "./reload.js";
12
+ export function defaultProbes() {
13
+ return {
14
+ binaryPresent: () => existsSync(filebeatBinPath()),
15
+ binaryPath: () => (existsSync(filebeatBinPath()) ? filebeatBinPath() : null),
16
+ desiredVersion: () => desiredFilebeatVersion(),
17
+ observedVersion: () => observedFilebeatVersion(),
18
+ serviceRegistered: () => probeServiceRegistered(),
19
+ observedConfigBytes: () => readObservedConfigBytes(filebeatDir()),
20
+ bindingInputHash: async (bindingId) => {
21
+ const file = path.join(inputsDir(), `${bindingId}.yml`);
22
+ return existsSync(file) ? desiredConfigHash(await readFile(file, "utf8")) : null;
23
+ },
24
+ };
25
+ }
26
+ export async function collectTelemetryStatus(store, probes = defaultProbes(), options = {}) {
27
+ const fbState = readFilebeatState(store);
28
+ const device = readDeviceState(store);
29
+ const telemetryEnabled = fbState ? fbState.telemetryEnabled : true;
30
+ const binaryPresent = probes.binaryPresent();
31
+ const serviceRegistered = probes.serviceRegistered();
32
+ const deviceRef = device?.deviceRef ?? "";
33
+ const userId = device?.activeUserId ?? "";
34
+ const folders = [];
35
+ const activity = [];
36
+ for (const binding of listActiveBindings(store)) {
37
+ const rendered = renderBindingInput({
38
+ bindingId: binding.id,
39
+ repoRoot: binding.repositoryRoot,
40
+ organizationId: binding.organizationId,
41
+ workspaceId: binding.workspaceId,
42
+ deviceRef,
43
+ userId,
44
+ });
45
+ const desired = desiredConfigHash(rendered);
46
+ const observed = await probes.bindingInputHash(binding.id);
47
+ folders.push({
48
+ bindingId: binding.id,
49
+ label: binding.repositoryRoot.split("/").filter(Boolean).pop() ?? binding.id,
50
+ repositoryRoot: options.localVerbose === true ? binding.repositoryRoot : null,
51
+ enabled: binding.telemetryEnabled,
52
+ pathFingerprint: binding.repositoryFingerprint,
53
+ desiredConfigVersion: TELEMETRY_CONFIG_VERSION,
54
+ desiredConfigHash: desired,
55
+ observedConfigHash: observed,
56
+ redactionPolicyVersion: REDACTION_POLICY_VERSION,
57
+ state: folderState({
58
+ telemetryEnabled,
59
+ folderEnabled: binding.telemetryEnabled,
60
+ binaryPresent,
61
+ desired,
62
+ observed,
63
+ }),
64
+ });
65
+ const rows = listActivityBindings(store, binding.id);
66
+ for (const discovered of discoverProviderBindings(binding.repositoryRoot)) {
67
+ const row = rows.find((r) => r.provider === discovered.provider);
68
+ const baseState = row ? row.state : discovered.state;
69
+ activity.push({
70
+ bindingId: binding.id,
71
+ provider: discovered.provider,
72
+ providerProjectKey: discovered.providerProjectKey,
73
+ pathFingerprint: discovered.pathFingerprint,
74
+ discoveryMethod: discovered.discoveryMethod,
75
+ state: !telemetryEnabled || !binding.telemetryEnabled ? "disabled" : baseState,
76
+ lastObservedAtMs: row?.lastObservedAtMs ?? null,
77
+ });
78
+ }
79
+ }
80
+ const observedConfigHash = binaryPresent
81
+ ? desiredConfigHash(await probes.observedConfigBytes())
82
+ : null;
83
+ return {
84
+ telemetryEnabled,
85
+ optedOutByEnv: process.env.NUSTACK_TELEMETRY === "off",
86
+ binaryPresent,
87
+ binaryPath: probes.binaryPath(),
88
+ desiredVersion: probes.desiredVersion(),
89
+ observedVersion: probes.observedVersion(),
90
+ serviceRegistered,
91
+ desiredConfigVersion: TELEMETRY_CONFIG_VERSION,
92
+ observedConfigHash,
93
+ filebeatState: installState({
94
+ binaryPresent,
95
+ serviceRegistered,
96
+ lastErrorCode: fbState?.lastErrorCode ?? null,
97
+ }),
98
+ lastErrorCode: fbState?.lastErrorCode ?? null,
99
+ lastHealthAtMs: fbState?.lastHealthAtMs ?? null,
100
+ folders,
101
+ activity,
102
+ };
103
+ }
104
+ function folderState(args) {
105
+ if (!args.telemetryEnabled || !args.folderEnabled)
106
+ return "disabled";
107
+ if (args.observed === null)
108
+ return "pending";
109
+ if (args.observed === args.desired && args.binaryPresent)
110
+ return "healthy";
111
+ return "degraded";
112
+ }
113
+ function installState(args) {
114
+ if (!args.binaryPresent)
115
+ return "not_installed";
116
+ if (args.lastErrorCode)
117
+ return "degraded";
118
+ return args.serviceRegistered ? "healthy" : "degraded";
119
+ }
120
+ export function buildTelemetryHealthReport(status) {
121
+ return {
122
+ filebeat: {
123
+ desiredVersion: status.desiredVersion,
124
+ observedVersion: status.observedVersion,
125
+ desiredConfigVersion: status.desiredConfigVersion,
126
+ observedConfigHash: status.observedConfigHash,
127
+ state: status.filebeatState,
128
+ errorCode: status.lastErrorCode,
129
+ },
130
+ folders: status.folders.map((folder) => ({
131
+ bindingId: folder.bindingId,
132
+ enabled: folder.enabled,
133
+ pathFingerprint: folder.pathFingerprint,
134
+ desiredConfigVersion: folder.desiredConfigVersion,
135
+ desiredConfigHash: folder.desiredConfigHash,
136
+ observedConfigHash: folder.observedConfigHash,
137
+ redactionPolicyVersion: folder.redactionPolicyVersion,
138
+ state: folder.state,
139
+ })),
140
+ activity: status.activity.map((entry) => ({
141
+ bindingId: entry.bindingId,
142
+ provider: entry.provider,
143
+ providerProjectKey: entry.providerProjectKey,
144
+ pathFingerprint: entry.pathFingerprint,
145
+ discoveryMethod: entry.discoveryMethod,
146
+ state: entry.state,
147
+ lastObservedAt: entry.lastObservedAtMs === null ? null : new Date(entry.lastObservedAtMs).toISOString(),
148
+ })),
149
+ };
150
+ }
151
+ const HEALTH_REPORT_TIMEOUT_MS = 10_000;
152
+ export async function reportTelemetryHealth(opts) {
153
+ try {
154
+ const deviceRef = opts.deviceRef ?? readDeviceState(opts.store)?.deviceRef ?? null;
155
+ if (!deviceRef || !opts.accessToken)
156
+ return;
157
+ const report = opts.report ??
158
+ buildTelemetryHealthReport(await collectTelemetryStatus(opts.store, opts.probes));
159
+ const fetchImpl = opts.fetchImpl ?? fetch;
160
+ await fetchImpl(`${opts.baseUrl}/api/v2/devices/${encodeURIComponent(deviceRef)}/telemetry-health`, {
161
+ method: "PUT",
162
+ headers: {
163
+ Authorization: `Bearer ${opts.accessToken}`,
164
+ "Content-Type": "application/json",
165
+ },
166
+ body: JSON.stringify(report),
167
+ signal: AbortSignal.timeout(HEALTH_REPORT_TIMEOUT_MS),
168
+ });
169
+ upsertFilebeatState(opts.store, { lastHealthAtMs: Date.now() });
170
+ }
171
+ catch {
172
+ }
173
+ }
@@ -0,0 +1,38 @@
1
+ import { createHash } from "node:crypto";
2
+ import { existsSync } from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ export function encodeClaudeProjectKey(repoRoot) {
6
+ return repoRoot.replace(/[^a-zA-Z0-9]/g, "-");
7
+ }
8
+ export function claudeProjectDir(repoRoot) {
9
+ return path.join(os.homedir(), ".claude", "projects", encodeClaudeProjectKey(repoRoot));
10
+ }
11
+ export function codexSessionsDir() {
12
+ return path.join(os.homedir(), ".codex", "sessions");
13
+ }
14
+ function sha256Hex(value) {
15
+ return createHash("sha256").update(value).digest("hex");
16
+ }
17
+ export function discoverProviderBindings(repoRoot) {
18
+ const claudeDir = claudeProjectDir(repoRoot);
19
+ const codexDir = codexSessionsDir();
20
+ return [
21
+ {
22
+ provider: "claude_code",
23
+ providerProjectKey: encodeClaudeProjectKey(repoRoot),
24
+ absolutePath: claudeDir,
25
+ pathFingerprint: sha256Hex(claudeDir),
26
+ discoveryMethod: "claude_projects_dir",
27
+ state: existsSync(claudeDir) ? "active" : "missing",
28
+ },
29
+ {
30
+ provider: "codex",
31
+ providerProjectKey: sha256Hex(repoRoot),
32
+ absolutePath: codexDir,
33
+ pathFingerprint: sha256Hex(`codex:${repoRoot}`),
34
+ discoveryMethod: "codex_sessions_cwd",
35
+ state: existsSync(codexDir) ? "active" : "missing",
36
+ },
37
+ ];
38
+ }
@@ -0,0 +1,90 @@
1
+ export const REDACTION_POLICY_VERSION = 1;
2
+ const DROP_FIELDS = {
3
+ claude_code: ["message.content", "message.thinking", "toolUseResult", "summary", "attachment"],
4
+ codex: [
5
+ "payload.content",
6
+ "payload.arguments",
7
+ "payload.output",
8
+ "payload.instructions",
9
+ "payload.environment",
10
+ ],
11
+ };
12
+ const KEEP_FIELDS = {
13
+ claude_code: [
14
+ "sessionId",
15
+ "type",
16
+ "subtype",
17
+ "timestamp",
18
+ "uuid",
19
+ "parentUuid",
20
+ "gitBranch",
21
+ "userType",
22
+ "version",
23
+ "requestId",
24
+ "isSidechain",
25
+ "entrypoint",
26
+ "promptSource",
27
+ "mode",
28
+ "permissionMode",
29
+ "durationMs",
30
+ "messageCount",
31
+ "slashCommand",
32
+ "skillTrigger",
33
+ "message.role",
34
+ "message.model",
35
+ "message.stop_reason",
36
+ "message.usage",
37
+ ],
38
+ codex: ["id", "timestamp", "type", "payload.type", "payload.model", "payload.usage", "cwd"],
39
+ };
40
+ export function providerDropFields(provider) {
41
+ return DROP_FIELDS[provider];
42
+ }
43
+ export function providerKeepFields(provider) {
44
+ return KEEP_FIELDS[provider];
45
+ }
46
+ export const SLASH_COMMAND_OPEN = "<command-name>";
47
+ export const SLASH_COMMAND_CLOSE = "</command-name>";
48
+ export function extractSlashCommand(content) {
49
+ if (typeof content !== "string")
50
+ return null;
51
+ const open = content.indexOf(SLASH_COMMAND_OPEN);
52
+ if (open === -1)
53
+ return null;
54
+ const start = open + SLASH_COMMAND_OPEN.length;
55
+ const close = content.indexOf(SLASH_COMMAND_CLOSE, start);
56
+ if (close === -1)
57
+ return null;
58
+ return content.slice(start, close);
59
+ }
60
+ function deleteDottedPath(target, path) {
61
+ const segments = path.split(".");
62
+ let node = target;
63
+ for (let i = 0; i < segments.length - 1; i++) {
64
+ const next = node[segments[i]];
65
+ if (next === null || typeof next !== "object" || Array.isArray(next))
66
+ return;
67
+ node = next;
68
+ }
69
+ delete node[segments[segments.length - 1]];
70
+ }
71
+ export function applyRedaction(event, provider) {
72
+ const redacted = structuredClone(event);
73
+ if (provider === "claude_code") {
74
+ const message = redacted.message;
75
+ if (message !== null && typeof message === "object" && !Array.isArray(message)) {
76
+ const command = extractSlashCommand(message.content);
77
+ if (command !== null)
78
+ redacted.slashCommand = command;
79
+ }
80
+ }
81
+ for (const field of providerDropFields(provider)) {
82
+ deleteDottedPath(redacted, field);
83
+ }
84
+ redacted.nustack = {
85
+ ...(typeof redacted.nustack === "object" && redacted.nustack !== null ? redacted.nustack : {}),
86
+ redaction_policy_version: REDACTION_POLICY_VERSION,
87
+ redaction_applied: true,
88
+ };
89
+ return redacted;
90
+ }
@@ -0,0 +1,84 @@
1
+ import { existsSync } from "node:fs";
2
+ import { copyFile, readdir, readFile, rm } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { mkdirSecure, writeFileSecure } from "../secure_file.js";
5
+ const MAIN_CONFIG = "filebeat.yml";
6
+ const INPUTS_DIR = "inputs.d";
7
+ async function liveConfigFiles(configDir) {
8
+ const files = [];
9
+ if (existsSync(path.join(configDir, MAIN_CONFIG)))
10
+ files.push(MAIN_CONFIG);
11
+ const inputsDir = path.join(configDir, INPUTS_DIR);
12
+ if (existsSync(inputsDir)) {
13
+ for (const name of await readdir(inputsDir)) {
14
+ if (name.endsWith(".yml"))
15
+ files.push(`${INPUTS_DIR}/${name}`);
16
+ }
17
+ }
18
+ return files;
19
+ }
20
+ async function copyRelative(fromRoot, toRoot, relativePath) {
21
+ const dest = path.join(toRoot, relativePath);
22
+ await mkdirSecure(path.dirname(dest));
23
+ await copyFile(path.join(fromRoot, relativePath), dest);
24
+ }
25
+ async function mirrorSet(configDir, targetDir) {
26
+ await rm(targetDir, { recursive: true, force: true });
27
+ await mkdirSecure(targetDir);
28
+ for (const relativePath of await liveConfigFiles(configDir)) {
29
+ await copyRelative(configDir, targetDir, relativePath);
30
+ }
31
+ }
32
+ async function writeRendered(root, file) {
33
+ const dest = path.join(root, file.relativePath);
34
+ await mkdirSecure(path.dirname(dest));
35
+ await writeFileSecure(dest, file.contents);
36
+ }
37
+ async function restoreKnownGood(configDir, knownGoodDir, files) {
38
+ for (const file of files) {
39
+ const snapshot = path.join(knownGoodDir, file.relativePath);
40
+ const live = path.join(configDir, file.relativePath);
41
+ if (existsSync(snapshot)) {
42
+ await copyFile(snapshot, live);
43
+ }
44
+ else {
45
+ await rm(live, { force: true });
46
+ }
47
+ }
48
+ }
49
+ export async function applyConfigChange(opts) {
50
+ const { configDir, testConfig, restartService } = opts.context;
51
+ const stagedDir = path.join(configDir, "staged");
52
+ const knownGoodDir = path.join(configDir, "known_good");
53
+ await mirrorSet(configDir, stagedDir);
54
+ for (const file of opts.files)
55
+ await writeRendered(stagedDir, file);
56
+ const validation = testConfig({ configDir: stagedDir, mainConfigPath: path.join(stagedDir, MAIN_CONFIG) });
57
+ if (!validation.ok) {
58
+ await rm(stagedDir, { recursive: true, force: true });
59
+ return { ok: false, errorCode: "config_invalid", rolledBack: false };
60
+ }
61
+ await mirrorSet(configDir, knownGoodDir);
62
+ for (const file of opts.files)
63
+ await writeRendered(configDir, file);
64
+ await rm(stagedDir, { recursive: true, force: true });
65
+ if (opts.scope === "central") {
66
+ const restart = await restartService();
67
+ if (!restart.ok) {
68
+ await restoreKnownGood(configDir, knownGoodDir, opts.files);
69
+ return { ok: false, errorCode: "restart_failed", rolledBack: true };
70
+ }
71
+ }
72
+ return { ok: true };
73
+ }
74
+ export async function removeInputFile(configDir, relativePath) {
75
+ await rm(path.join(configDir, relativePath), { force: true });
76
+ }
77
+ export async function readObservedConfigBytes(configDir) {
78
+ const parts = [];
79
+ for (const relativePath of [MAIN_CONFIG, `${INPUTS_DIR}/codex-shared.yml`]) {
80
+ const file = path.join(configDir, relativePath);
81
+ parts.push(existsSync(file) ? await readFile(file, "utf8") : "");
82
+ }
83
+ return parts.join("\n");
84
+ }
@@ -0,0 +1,121 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { existsSync } from "node:fs";
4
+ import { readManagedValues } from "./env_file.js";
5
+ import { manifestPath } from "./manifest.js";
6
+ import { assertSafeOpaqueId } from "./guards.js";
7
+ const NO_WORKSPACE_MESSAGE = "No Workspace id — run inside a bound repository (`.nustack/workspace.json`), pass `--workspace <id>`, add an `nustackWorkspace.workspaceId` to nustack.service.json, or set NUSTACK_WORKSPACE_ID in the managed .env block.";
8
+ export function workspaceJsonPath(cwd) {
9
+ return path.join(cwd, ".nustack", "workspace.json");
10
+ }
11
+ export function legacyAppJsonPath(cwd) {
12
+ return path.join(cwd, ".nustack", "app.json");
13
+ }
14
+ export async function readBindingMarker(cwd) {
15
+ const v2Path = workspaceJsonPath(cwd);
16
+ if (existsSync(v2Path)) {
17
+ try {
18
+ const marker = JSON.parse(await readFile(v2Path, "utf8"));
19
+ if (marker.schemaVersion === 2) {
20
+ const workspaceId = trimmed(marker.workspaceId);
21
+ if (workspaceId) {
22
+ return { workspaceId, organizationId: trimmed(marker.organizationId), version: 2 };
23
+ }
24
+ }
25
+ }
26
+ catch {
27
+ }
28
+ }
29
+ const v1Path = legacyAppJsonPath(cwd);
30
+ if (!existsSync(v1Path))
31
+ return null;
32
+ try {
33
+ const marker = JSON.parse(await readFile(v1Path, "utf8"));
34
+ if (marker.schemaVersion !== undefined && marker.schemaVersion !== 1)
35
+ return null;
36
+ const workspaceId = trimmed(marker.appId);
37
+ if (!workspaceId)
38
+ return null;
39
+ return { workspaceId, organizationId: trimmed(marker.workspaceId), version: 1 };
40
+ }
41
+ catch {
42
+ return null;
43
+ }
44
+ }
45
+ function trimmed(value) {
46
+ return typeof value === "string" && value.trim() !== "" ? value.trim() : null;
47
+ }
48
+ export async function readWorkspaceJsonWorkspaceId(cwd) {
49
+ return (await readBindingMarker(cwd))?.workspaceId ?? null;
50
+ }
51
+ async function readManifestWorkspaceId(cwd) {
52
+ const filePath = manifestPath(cwd);
53
+ if (!existsSync(filePath))
54
+ return null;
55
+ try {
56
+ const manifest = JSON.parse(await readFile(filePath, "utf8"));
57
+ return trimmed(manifest.nustackWorkspace?.workspaceId) ?? trimmed(manifest.nustackApp?.appId);
58
+ }
59
+ catch {
60
+ return null;
61
+ }
62
+ }
63
+ async function readEnvWorkspaceId(cwd) {
64
+ const managed = await readManagedValues(path.join(cwd, ".env"));
65
+ const workspaceId = managed.NUSTACK_WORKSPACE_ID?.trim();
66
+ return workspaceId ? workspaceId : null;
67
+ }
68
+ export async function resolveWorkspaceId(cwd, options) {
69
+ const flag = options.workspace?.trim();
70
+ const workspaceJsonId = await readWorkspaceJsonWorkspaceId(cwd);
71
+ const candidate = workspaceJsonId
72
+ ? { value: workspaceJsonId, source: "workspace_json" }
73
+ : flag
74
+ ? { value: flag, source: "flag" }
75
+ : (await readManifestWorkspaceId(cwd).then((id) => (id ? { value: id, source: "manifest" } : null))) ??
76
+ (await readEnvWorkspaceId(cwd).then((id) => (id ? { value: id, source: "env" } : null)));
77
+ if (!candidate)
78
+ return { ok: false, message: NO_WORKSPACE_MESSAGE };
79
+ try {
80
+ return { ok: true, workspaceId: assertSafeOpaqueId("The Workspace id", candidate.value), source: candidate.source };
81
+ }
82
+ catch (error) {
83
+ return { ok: false, message: error instanceof Error ? error.message : String(error) };
84
+ }
85
+ }
86
+ const NO_ORGANIZATION_MESSAGE = "No Organization context — pass `--organization <id-or-slug>`, run `nustack organization use <id>`, or create one with `nustack organization create <name>`.";
87
+ export function resolveOrganizationId(options, activeOrganizationId, memberships) {
88
+ const flag = options.organization?.trim();
89
+ if (flag) {
90
+ if (memberships && memberships.length > 0) {
91
+ const match = memberships.find((m) => m.id === flag || m.slug === flag);
92
+ if (!match)
93
+ return { ok: false, message: `No Organization "${flag}" among your memberships.` };
94
+ return finalizeOrganization(match.id, "flag");
95
+ }
96
+ return finalizeOrganization(flag, "flag");
97
+ }
98
+ if (activeOrganizationId) {
99
+ if (!memberships || memberships.length === 0)
100
+ return finalizeOrganization(activeOrganizationId, "active");
101
+ if (memberships.some((m) => m.id === activeOrganizationId))
102
+ return finalizeOrganization(activeOrganizationId, "active");
103
+ if (memberships.length === 1)
104
+ return finalizeOrganization(memberships[0].id, "single_membership");
105
+ return {
106
+ ok: false,
107
+ message: "Your active Organization is no longer among your memberships — run `nustack organization use <id-or-slug>` to pick a current one.",
108
+ };
109
+ }
110
+ if (memberships && memberships.length === 1)
111
+ return finalizeOrganization(memberships[0].id, "single_membership");
112
+ return { ok: false, message: NO_ORGANIZATION_MESSAGE };
113
+ }
114
+ function finalizeOrganization(value, source) {
115
+ try {
116
+ return { ok: true, organizationId: assertSafeOpaqueId("The Organization id", value), source };
117
+ }
118
+ catch (error) {
119
+ return { ok: false, message: error instanceof Error ? error.message : String(error) };
120
+ }
121
+ }