@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.
- package/README.md +77 -0
- package/dist/commands/add.js +130 -0
- package/dist/commands/agent.js +100 -0
- package/dist/commands/bootstrap.js +54 -0
- package/dist/commands/describe.js +34 -0
- package/dist/commands/device.js +159 -0
- package/dist/commands/docs.js +119 -0
- package/dist/commands/doctor.js +284 -0
- package/dist/commands/graph.js +34 -0
- package/dist/commands/handoff.js +108 -0
- package/dist/commands/init.js +280 -0
- package/dist/commands/lane.js +12 -0
- package/dist/commands/list.js +20 -0
- package/dist/commands/login.js +134 -0
- package/dist/commands/logout.js +44 -0
- package/dist/commands/open.js +54 -0
- package/dist/commands/organization.js +114 -0
- package/dist/commands/review.js +152 -0
- package/dist/commands/search.js +57 -0
- package/dist/commands/sessions.js +62 -0
- package/dist/commands/sync.js +85 -0
- package/dist/commands/telemetry.js +112 -0
- package/dist/commands/toolkit.js +167 -0
- package/dist/commands/validate.js +67 -0
- package/dist/commands/workspace-sync.js +645 -0
- package/dist/commands/workspace.js +479 -0
- package/dist/index.js +357 -0
- package/dist/lib/agent/claudeHooks.js +77 -0
- package/dist/lib/auth_grant.js +398 -0
- package/dist/lib/auth_recovery.js +49 -0
- package/dist/lib/browser_opener.js +24 -0
- package/dist/lib/device_identity.js +28 -0
- package/dist/lib/device_keychain.js +101 -0
- package/dist/lib/env_file.js +75 -0
- package/dist/lib/etna/manager.js +43 -0
- package/dist/lib/etna/profile.js +41 -0
- package/dist/lib/etna/reconcile.js +77 -0
- package/dist/lib/filebeat_installer.js +246 -0
- package/dist/lib/git-sync/core/commit_range.js +37 -0
- package/dist/lib/git-sync/core/engine.js +263 -0
- package/dist/lib/git-sync/core/git_runner.js +137 -0
- package/dist/lib/git-sync/core/operation_state.js +22 -0
- package/dist/lib/git-sync/core/path_normalizer.js +98 -0
- package/dist/lib/git-sync/core/reconciliation_lock.js +103 -0
- package/dist/lib/git-sync/core/repository.js +49 -0
- package/dist/lib/git-sync/core/watch_stream.js +86 -0
- package/dist/lib/git-sync/core/watcher.js +61 -0
- package/dist/lib/git-sync/inbound/applier.js +131 -0
- package/dist/lib/git-sync/inbound/apply_commit.js +225 -0
- package/dist/lib/git-sync/inbound/approval.js +117 -0
- package/dist/lib/git-sync/inbound/content_decision.js +148 -0
- package/dist/lib/git-sync/inbound/download.js +223 -0
- package/dist/lib/git-sync/inbound/event_sync.js +133 -0
- package/dist/lib/git-sync/inbound/index.js +10 -0
- package/dist/lib/git-sync/inbound/notification.js +39 -0
- package/dist/lib/git-sync/inbound/state_fingerprint.js +32 -0
- package/dist/lib/git-sync/inbound/technical_wait.js +39 -0
- package/dist/lib/git-sync/inbound/undo.js +198 -0
- package/dist/lib/git-sync/outbound/extraction.js +107 -0
- package/dist/lib/git-sync/outbound/history_decision.js +112 -0
- package/dist/lib/git-sync/outbound/index.js +26 -0
- package/dist/lib/git-sync/outbound/manifest_builder.js +58 -0
- package/dist/lib/git-sync/outbound/publisher.js +51 -0
- package/dist/lib/git-sync/outbound/queue.js +123 -0
- package/dist/lib/git-sync/outbound/state.js +12 -0
- package/dist/lib/git-sync/outbound/upload_client.js +141 -0
- package/dist/lib/git-sync/recovery/bundle.js +56 -0
- package/dist/lib/git-sync/recovery/crypto.js +114 -0
- package/dist/lib/git-sync/recovery/index.js +3 -0
- package/dist/lib/git-sync/recovery/store.js +55 -0
- package/dist/lib/git-sync/whitelist/index.js +4 -0
- package/dist/lib/git-sync/whitelist/matcher.js +62 -0
- package/dist/lib/git-sync/whitelist/store.js +99 -0
- package/dist/lib/git-sync/whitelist/types.js +46 -0
- package/dist/lib/git-sync/whitelist/validation.js +60 -0
- package/dist/lib/graph/answer.js +171 -0
- package/dist/lib/graph/freshness.js +29 -0
- package/dist/lib/graph/library.js +19 -0
- package/dist/lib/guards.js +106 -0
- package/dist/lib/interactive.js +12 -0
- package/dist/lib/json_output.js +8 -0
- package/dist/lib/lanes/claudeAdapter.js +135 -0
- package/dist/lib/lanes/frames.js +73 -0
- package/dist/lib/lanes/supervisor.js +247 -0
- package/dist/lib/lanes/worktree.js +33 -0
- package/dist/lib/local_store/accepted_uploads.js +49 -0
- package/dist/lib/local_store/bindings.js +163 -0
- package/dist/lib/local_store/commit_origins.js +25 -0
- package/dist/lib/local_store/database.js +71 -0
- package/dist/lib/local_store/device_state.js +72 -0
- package/dist/lib/local_store/filebeat_state.js +133 -0
- package/dist/lib/local_store/handoff_applications.js +166 -0
- package/dist/lib/local_store/history_decisions.js +48 -0
- package/dist/lib/local_store/index.js +14 -0
- package/dist/lib/local_store/migrations.js +240 -0
- package/dist/lib/local_store/paths.js +24 -0
- package/dist/lib/local_store/pending_uploads.js +76 -0
- package/dist/lib/local_store/recovery_bundles.js +66 -0
- package/dist/lib/local_store/sync_vocabulary.js +33 -0
- package/dist/lib/local_store/workspace_events_cache.js +54 -0
- package/dist/lib/loopback.js +78 -0
- package/dist/lib/manifest.js +83 -0
- package/dist/lib/nustack_client.js +547 -0
- package/dist/lib/nustack_store.js +17 -0
- package/dist/lib/onboarding/attach_existing_git.js +65 -0
- package/dist/lib/onboarding/binding.js +91 -0
- package/dist/lib/onboarding/folder_state.js +99 -0
- package/dist/lib/onboarding/git_init.js +10 -0
- package/dist/lib/onboarding/handoff_apply.js +68 -0
- package/dist/lib/onboarding/init_fresh_folder.js +100 -0
- package/dist/lib/onboarding/manifest_writer.js +161 -0
- package/dist/lib/onboarding/web_first_attach.js +133 -0
- package/dist/lib/package_manager.js +23 -0
- package/dist/lib/review/client.js +66 -0
- package/dist/lib/schema_validator.js +24 -0
- package/dist/lib/secure_file.js +10 -0
- package/dist/lib/session_telemetry.js +263 -0
- package/dist/lib/sessions/client.js +48 -0
- package/dist/lib/sessions/outsideSessions.js +54 -0
- package/dist/lib/sse_framing.js +48 -0
- package/dist/lib/telemetry/config_renderer.js +175 -0
- package/dist/lib/telemetry/health_report.js +173 -0
- package/dist/lib/telemetry/provider_paths.js +38 -0
- package/dist/lib/telemetry/redaction_policy.js +90 -0
- package/dist/lib/telemetry/reload.js +84 -0
- package/dist/lib/workspace_context.js +121 -0
- package/dist/spec_schema.json +422 -0
- package/dist/wire/auth.js +1 -0
- package/dist/wire/sync-paths.js +25 -0
- package/dist/wire/telemetry.js +1 -0
- package/package.json +49 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export const SYNC_PROTOCOL_VERSION = "nustack.dev/v2";
|
|
2
|
+
function isStringArray(value) {
|
|
3
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
4
|
+
}
|
|
5
|
+
function isLimits(value) {
|
|
6
|
+
if (!value || typeof value !== "object")
|
|
7
|
+
return false;
|
|
8
|
+
const limits = value;
|
|
9
|
+
return (typeof limits.defaultMaxBytes === "number" &&
|
|
10
|
+
typeof limits.blobMaxBytes === "number" &&
|
|
11
|
+
typeof limits.maxOperations === "number");
|
|
12
|
+
}
|
|
13
|
+
function isEntry(value) {
|
|
14
|
+
if (!value || typeof value !== "object")
|
|
15
|
+
return false;
|
|
16
|
+
const entry = value;
|
|
17
|
+
if (typeof entry.pattern !== "string")
|
|
18
|
+
return false;
|
|
19
|
+
if (entry.kind !== "exact" && entry.kind !== "glob")
|
|
20
|
+
return false;
|
|
21
|
+
if (entry.direction !== "bidirectional" && entry.direction !== "cloud_to_local" && entry.direction !== "local_to_cloud") {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
if (typeof entry.maxBytes !== "number")
|
|
25
|
+
return false;
|
|
26
|
+
if (!isStringArray(entry.mediaTypes))
|
|
27
|
+
return false;
|
|
28
|
+
if (entry.allowsSupplemental !== undefined && typeof entry.allowsSupplemental !== "boolean")
|
|
29
|
+
return false;
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
export function isWhitelistManifest(value) {
|
|
33
|
+
if (!value || typeof value !== "object")
|
|
34
|
+
return false;
|
|
35
|
+
const manifest = value;
|
|
36
|
+
return (manifest.schemaVersion === 1 &&
|
|
37
|
+
typeof manifest.manifestId === "string" &&
|
|
38
|
+
typeof manifest.version === "number" &&
|
|
39
|
+
typeof manifest.workspaceId === "string" &&
|
|
40
|
+
typeof manifest.protocolVersion === "string" &&
|
|
41
|
+
Array.isArray(manifest.entries) &&
|
|
42
|
+
manifest.entries.every(isEntry) &&
|
|
43
|
+
isStringArray(manifest.protectedPatterns) &&
|
|
44
|
+
isLimits(manifest.limits) &&
|
|
45
|
+
typeof manifest.manifestSha256 === "string");
|
|
46
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
const SYMLINK_MODE = "120000";
|
|
2
|
+
const EXECUTABLE_MODE = "100755";
|
|
3
|
+
const NUL = String.fromCharCode(0);
|
|
4
|
+
const MEDIA_TYPE_BY_EXTENSION = {
|
|
5
|
+
md: "text/markdown",
|
|
6
|
+
json: "application/json",
|
|
7
|
+
yaml: "text/yaml",
|
|
8
|
+
yml: "text/yaml",
|
|
9
|
+
};
|
|
10
|
+
export function mediaTypeForPath(path) {
|
|
11
|
+
const dot = path.lastIndexOf(".");
|
|
12
|
+
if (dot < 0)
|
|
13
|
+
return null;
|
|
14
|
+
const ext = path.slice(dot + 1).toLowerCase();
|
|
15
|
+
return MEDIA_TYPE_BY_EXTENSION[ext] ?? null;
|
|
16
|
+
}
|
|
17
|
+
export function frontmatterWorkspaceId(text) {
|
|
18
|
+
if (!text.startsWith("---"))
|
|
19
|
+
return null;
|
|
20
|
+
const firstNewline = text.indexOf("\n");
|
|
21
|
+
if (firstNewline < 0)
|
|
22
|
+
return null;
|
|
23
|
+
if (text.slice(0, firstNewline).trim() !== "---")
|
|
24
|
+
return null;
|
|
25
|
+
const rest = text.slice(firstNewline + 1);
|
|
26
|
+
const closeIndex = rest.search(/\n---\s*(\n|$)/);
|
|
27
|
+
const block = closeIndex < 0 ? rest : rest.slice(0, closeIndex);
|
|
28
|
+
for (const line of block.split("\n")) {
|
|
29
|
+
const match = /^\s*(?:workspace_id|workspaceId|app_id|appId)\s*:\s*(.+?)\s*$/.exec(line);
|
|
30
|
+
if (match)
|
|
31
|
+
return match[1].replace(/^["']|["']$/g, "");
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
export function validateOperationContent(input) {
|
|
36
|
+
if (input.mode === SYMLINK_MODE)
|
|
37
|
+
return { ok: false, code: "symlink_denied" };
|
|
38
|
+
if (input.mode === EXECUTABLE_MODE)
|
|
39
|
+
return { ok: false, code: "executable_denied" };
|
|
40
|
+
if (input.byteSize > input.entry.maxBytes)
|
|
41
|
+
return { ok: false, code: "content_too_large" };
|
|
42
|
+
const mediaType = mediaTypeForPath(input.path);
|
|
43
|
+
if (mediaType === null || !input.entry.mediaTypes.includes(mediaType)) {
|
|
44
|
+
return { ok: false, code: "media_type_not_allowed" };
|
|
45
|
+
}
|
|
46
|
+
let text;
|
|
47
|
+
try {
|
|
48
|
+
text = new TextDecoder("utf-8", { fatal: true }).decode(input.bytes);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return { ok: false, code: "invalid_encoding" };
|
|
52
|
+
}
|
|
53
|
+
if (text.includes(NUL))
|
|
54
|
+
return { ok: false, code: "invalid_encoding" };
|
|
55
|
+
const declaredWorkspace = frontmatterWorkspaceId(text);
|
|
56
|
+
if (declaredWorkspace !== null && declaredWorkspace !== input.workspaceId) {
|
|
57
|
+
return { ok: false, code: "cross_workspace_marker" };
|
|
58
|
+
}
|
|
59
|
+
return { ok: true };
|
|
60
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { printJsonSuccess } from "../json_output.js";
|
|
5
|
+
import { loadCodegraph } from "./library.js";
|
|
6
|
+
import { refreshIfStale } from "./freshness.js";
|
|
7
|
+
export class GraphNoRepository extends Error {
|
|
8
|
+
code = "GRAPH_NO_REPOSITORY";
|
|
9
|
+
constructor(where) {
|
|
10
|
+
super(`not a git repository: ${where} — pass --repo <path|id> or run inside a checkout`);
|
|
11
|
+
this.name = "GraphNoRepository";
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export function resolveRoot(library, repo, cwd) {
|
|
15
|
+
if (repo !== undefined) {
|
|
16
|
+
const known = library.findProject(repo);
|
|
17
|
+
if (known !== undefined)
|
|
18
|
+
return known.path;
|
|
19
|
+
const path = resolve(cwd, repo);
|
|
20
|
+
if (!existsSync(path))
|
|
21
|
+
throw new GraphNoRepository(repo);
|
|
22
|
+
return gitRoot(path);
|
|
23
|
+
}
|
|
24
|
+
return gitRoot(cwd);
|
|
25
|
+
}
|
|
26
|
+
function gitRoot(from) {
|
|
27
|
+
try {
|
|
28
|
+
return execFileSync("git", ["rev-parse", "--show-toplevel"], { cwd: from, stdio: ["ignore", "pipe", "ignore"] })
|
|
29
|
+
.toString("utf8")
|
|
30
|
+
.trim();
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
throw new GraphNoRepository(from);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export class GraphUnavailable extends Error {
|
|
37
|
+
code = "GRAPH_UNAVAILABLE";
|
|
38
|
+
constructor(reason) {
|
|
39
|
+
super(reason);
|
|
40
|
+
this.name = "GraphUnavailable";
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const progress = (line) => {
|
|
44
|
+
process.stderr.write(`${line}\n`);
|
|
45
|
+
};
|
|
46
|
+
export async function answerGraph(fn, argv, options, cwd) {
|
|
47
|
+
const library = await loadCodegraph();
|
|
48
|
+
const root = resolveRoot(library, options.repo, cwd);
|
|
49
|
+
const known = library.findFn(fn);
|
|
50
|
+
if (known === undefined) {
|
|
51
|
+
throw new GraphUnavailable(`unknown code-graph function '${fn}' — run \`nustack graph functions\` for the list`);
|
|
52
|
+
}
|
|
53
|
+
await refreshIfStale(library, root, { noRefresh: options.refresh === false, onProgress: progress });
|
|
54
|
+
const db = library.open(root);
|
|
55
|
+
const args = library.parseArgs(known, argv);
|
|
56
|
+
const budget = options.budget !== undefined ? Number(options.budget) : args["limit"] !== undefined ? 0 : 2000;
|
|
57
|
+
const answer = library.dispatch({ root, db, budget }, { fn: known.name, args });
|
|
58
|
+
if (answer.unavailable !== undefined)
|
|
59
|
+
throw new GraphUnavailable(answer.unavailable);
|
|
60
|
+
if (options.json === true) {
|
|
61
|
+
printJsonSuccess(`graph ${known.name}`, answer);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
console.log(library.renderText(answer));
|
|
65
|
+
}
|
|
66
|
+
export async function indexGraph(options, cwd) {
|
|
67
|
+
const library = await loadCodegraph();
|
|
68
|
+
const root = resolveRoot(library, options.repo, cwd);
|
|
69
|
+
const result = await library.runIndex(root, {
|
|
70
|
+
maxCommits: options.commits === undefined ? undefined : Number(options.commits),
|
|
71
|
+
onProgress: options.json === true ? undefined : progress,
|
|
72
|
+
});
|
|
73
|
+
if (options.json === true) {
|
|
74
|
+
printJsonSuccess("graph index", {
|
|
75
|
+
root: result.root,
|
|
76
|
+
projectId: result.projectId,
|
|
77
|
+
store: result.store,
|
|
78
|
+
files: result.files,
|
|
79
|
+
commits: result.commits,
|
|
80
|
+
symbols: result.symbols,
|
|
81
|
+
edges: result.edges,
|
|
82
|
+
extractor: result.extractor,
|
|
83
|
+
head: result.head,
|
|
84
|
+
elapsedMs: result.elapsedMs,
|
|
85
|
+
});
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
console.log([
|
|
89
|
+
`indexed ${result.root}`,
|
|
90
|
+
` project id ${result.projectId}`,
|
|
91
|
+
` store ${result.store}`,
|
|
92
|
+
` files ${result.files}`,
|
|
93
|
+
` commits ${result.commits} (${result.changes} file changes)`,
|
|
94
|
+
` symbols ${result.symbols} across ${result.symbolFiles} files`,
|
|
95
|
+
` call edges ${result.edges} (inferred from syntax, not resolved bindings)`,
|
|
96
|
+
` extractor ${result.extractor} (declarations: ${result.declarationConfidence})`,
|
|
97
|
+
` elapsed ${(result.elapsedMs / 1000).toFixed(1)}s`,
|
|
98
|
+
"",
|
|
99
|
+
"next: nustack graph overview",
|
|
100
|
+
].join("\n"));
|
|
101
|
+
}
|
|
102
|
+
export async function statusGraph(options, cwd) {
|
|
103
|
+
const library = await loadCodegraph();
|
|
104
|
+
const root = resolveRoot(library, options.repo, cwd);
|
|
105
|
+
const record = library.findProject(root);
|
|
106
|
+
const health = record === undefined ? undefined : library.projectHealth(record, library.databaseFile(root));
|
|
107
|
+
const data = {
|
|
108
|
+
root,
|
|
109
|
+
projectId: library.projectId(root),
|
|
110
|
+
indexed: record !== undefined,
|
|
111
|
+
store: library.storeDir(root),
|
|
112
|
+
home: library.codegraphHome(),
|
|
113
|
+
head: library.headOf(root),
|
|
114
|
+
indexedHead: record?.lastHead ?? null,
|
|
115
|
+
indexedAt: record?.indexedAt ?? null,
|
|
116
|
+
files: record?.files ?? 0,
|
|
117
|
+
symbols: record?.symbols ?? 0,
|
|
118
|
+
commits: record?.commits ?? 0,
|
|
119
|
+
extractor: record?.extractor ?? null,
|
|
120
|
+
isStale: health?.isStale ?? false,
|
|
121
|
+
isReadable: health?.isReadable ?? false,
|
|
122
|
+
unreadableReason: health?.unreadableReason ?? "never indexed",
|
|
123
|
+
schemaVersion: health?.schemaVersion ?? 0,
|
|
124
|
+
databaseBytes: health?.databaseBytes ?? 0,
|
|
125
|
+
};
|
|
126
|
+
if (options.json === true) {
|
|
127
|
+
printJsonSuccess("graph status", data);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (!data.indexed) {
|
|
131
|
+
console.log(`${root}\n not indexed — run: nustack graph index`);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const age = data.indexedAt === null ? "never" : `${((Date.now() / 1000 - data.indexedAt) / 3600).toFixed(1)}h ago`;
|
|
135
|
+
console.log([
|
|
136
|
+
`${root}`,
|
|
137
|
+
` project id ${data.projectId}`,
|
|
138
|
+
` store ${data.store} (${(data.databaseBytes / 1e6).toFixed(1)} MB, schema v${data.schemaVersion})`,
|
|
139
|
+
` indexed ${age} — ${data.commits} commits, ${data.files} files, ${data.symbols} symbols`,
|
|
140
|
+
` head ${data.indexedHead ?? "unknown"}${data.isStale ? ` [STALE — checkout is at ${data.head ?? "unknown"}]` : " [current]"}`,
|
|
141
|
+
` readable ${data.isReadable ? "yes" : `no — ${data.unreadableReason}`}`,
|
|
142
|
+
"",
|
|
143
|
+
data.isStale ? "the next query refreshes it; --no-refresh answers from this index" : "next: nustack graph overview",
|
|
144
|
+
].join("\n"));
|
|
145
|
+
}
|
|
146
|
+
export async function listGraphFunctions(options) {
|
|
147
|
+
const library = await loadCodegraph();
|
|
148
|
+
if (options.json === true) {
|
|
149
|
+
printJsonSuccess("graph functions", {
|
|
150
|
+
functions: library.FUNCTIONS.map((fn) => ({
|
|
151
|
+
name: fn.name,
|
|
152
|
+
summary: fn.summary,
|
|
153
|
+
cost: fn.cost,
|
|
154
|
+
args: Object.entries(fn.args).map(([key, spec]) => ({
|
|
155
|
+
name: key,
|
|
156
|
+
kind: spec.kind,
|
|
157
|
+
required: spec.required === true,
|
|
158
|
+
positional: spec.positional ?? null,
|
|
159
|
+
describe: spec.describe,
|
|
160
|
+
})),
|
|
161
|
+
})),
|
|
162
|
+
});
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
const lines = ["the code graph's query functions — `nustack graph <fn> [args]`", ""];
|
|
166
|
+
for (const fn of library.FUNCTIONS) {
|
|
167
|
+
lines.push(`${fn.name.padEnd(18)}${fn.summary} [${fn.cost}]`);
|
|
168
|
+
}
|
|
169
|
+
lines.push("", "index build or rebuild this repository's graph", "status what the store holds, and whether it is behind");
|
|
170
|
+
console.log(lines.join("\n"));
|
|
171
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export function inspect(library, root) {
|
|
2
|
+
const record = library.findProject(root);
|
|
3
|
+
if (record === undefined) {
|
|
4
|
+
return { record: undefined, isStale: false, isReadable: false, unreadableReason: "never indexed" };
|
|
5
|
+
}
|
|
6
|
+
const health = library.projectHealth(record, library.databaseFile(root));
|
|
7
|
+
return {
|
|
8
|
+
record,
|
|
9
|
+
isStale: health.isStale,
|
|
10
|
+
isReadable: health.isReadable,
|
|
11
|
+
unreadableReason: health.unreadableReason,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export class GraphNotIndexed extends Error {
|
|
15
|
+
code = "GRAPH_NOT_INDEXED";
|
|
16
|
+
constructor(root) {
|
|
17
|
+
super(`no code graph for ${root} yet — run: nustack graph index`);
|
|
18
|
+
this.name = "GraphNotIndexed";
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export async function refreshIfStale(library, root, options = {}) {
|
|
22
|
+
const verdict = inspect(library, root);
|
|
23
|
+
if (verdict.record === undefined)
|
|
24
|
+
throw new GraphNotIndexed(root);
|
|
25
|
+
if (!verdict.isStale || options.noRefresh === true)
|
|
26
|
+
return { refreshed: false, verdict };
|
|
27
|
+
await library.runIndex(root, { onProgress: options.onProgress });
|
|
28
|
+
return { refreshed: true, verdict: inspect(library, root) };
|
|
29
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export class GraphLibraryMissing extends Error {
|
|
2
|
+
code = "GRAPH_LIBRARY_MISSING";
|
|
3
|
+
constructor() {
|
|
4
|
+
super("the code graph library is not installed with this nustack — install it beside the CLI (`npm i -g @nurix/codegraph`) or run nustack from a checkout of the monorepo.");
|
|
5
|
+
this.name = "GraphLibraryMissing";
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
let cached = null;
|
|
9
|
+
export async function loadCodegraph() {
|
|
10
|
+
if (cached !== null)
|
|
11
|
+
return cached;
|
|
12
|
+
try {
|
|
13
|
+
cached = (await import("@nurix/codegraph"));
|
|
14
|
+
return cached;
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
throw new GraphLibraryMissing();
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
const LOOPBACK_HOSTS = new Set(["localhost", "127.0.0.1", "[::1]", "::1"]);
|
|
2
|
+
function assertNoControlChars(label, value) {
|
|
3
|
+
for (const character of value) {
|
|
4
|
+
const code = character.codePointAt(0) ?? 0;
|
|
5
|
+
if (code < 0x20 || code === 0x7f) {
|
|
6
|
+
throw new Error(`${label} contains control characters — refusing to use it`);
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
function parseSecureUrl(label, raw) {
|
|
11
|
+
assertNoControlChars(label, raw);
|
|
12
|
+
let url;
|
|
13
|
+
try {
|
|
14
|
+
url = new URL(raw);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
throw new Error(`${label} is not a valid URL: ${raw}`);
|
|
18
|
+
}
|
|
19
|
+
const isLoopback = LOOPBACK_HOSTS.has(url.hostname);
|
|
20
|
+
if (url.protocol !== "https:" && !(url.protocol === "http:" && isLoopback)) {
|
|
21
|
+
throw new Error(`${label} must use https (got ${url.protocol}//${url.host}) — refusing to send credentials over an unencrypted or non-web transport`);
|
|
22
|
+
}
|
|
23
|
+
if (url.username !== "" || url.password !== "") {
|
|
24
|
+
throw new Error(`${label} must not embed credentials`);
|
|
25
|
+
}
|
|
26
|
+
return url;
|
|
27
|
+
}
|
|
28
|
+
export function assertSafeNuStackUrl(raw) {
|
|
29
|
+
const url = parseSecureUrl("The NuStack URL", raw);
|
|
30
|
+
return url.toString().replace(/\/+$/, "");
|
|
31
|
+
}
|
|
32
|
+
const PACKAGE_NAME_PATTERN = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/;
|
|
33
|
+
export function assertSafePackageName(name) {
|
|
34
|
+
assertNoControlChars("The package name", name);
|
|
35
|
+
if (name.length > 214 || !PACKAGE_NAME_PATTERN.test(name)) {
|
|
36
|
+
throw new Error(`The catalogue offered an unsafe package name (${name}) — expected a plain npm name like @scope/pkg, not a URL, path, or flag`);
|
|
37
|
+
}
|
|
38
|
+
return name;
|
|
39
|
+
}
|
|
40
|
+
export function assertSafeEnvKey(key) {
|
|
41
|
+
if (!/^[A-Z][A-Z0-9_]*$/.test(key)) {
|
|
42
|
+
throw new Error(`Unsafe env key from the server: ${key}`);
|
|
43
|
+
}
|
|
44
|
+
return key;
|
|
45
|
+
}
|
|
46
|
+
export function assertSafeEnvValue(key, value) {
|
|
47
|
+
assertNoControlChars(`The value for ${key}`, value);
|
|
48
|
+
return value;
|
|
49
|
+
}
|
|
50
|
+
export function assertSafeIndexName(index) {
|
|
51
|
+
if (!/^[a-z0-9][a-z0-9._-]{0,254}$/.test(index)) {
|
|
52
|
+
throw new Error(`The broker offered an unsafe index name: ${index}`);
|
|
53
|
+
}
|
|
54
|
+
return index;
|
|
55
|
+
}
|
|
56
|
+
export function assertSafeElkUrl(raw) {
|
|
57
|
+
const url = parseSecureUrl("The ELK URL", raw);
|
|
58
|
+
const port = url.port || (url.protocol === "https:" ? "443" : "80");
|
|
59
|
+
return `${url.protocol}//${url.hostname}:${port}${url.pathname}${url.search}`;
|
|
60
|
+
}
|
|
61
|
+
export function assertSafeApiKey(key) {
|
|
62
|
+
if (!/^[A-Za-z0-9+/=_:-]{1,1024}$/.test(key)) {
|
|
63
|
+
throw new Error("The broker offered an ingest key with unexpected characters");
|
|
64
|
+
}
|
|
65
|
+
if (key.includes(":"))
|
|
66
|
+
return key;
|
|
67
|
+
const decoded = decodeApiKeyPair(key);
|
|
68
|
+
return decoded ?? key;
|
|
69
|
+
}
|
|
70
|
+
function decodeApiKeyPair(key) {
|
|
71
|
+
if (key.length % 4 !== 0)
|
|
72
|
+
return null;
|
|
73
|
+
try {
|
|
74
|
+
const decoded = Buffer.from(key, "base64").toString("utf8");
|
|
75
|
+
if (!/^[A-Za-z0-9_-]+:[A-Za-z0-9_-]+$/.test(decoded))
|
|
76
|
+
return null;
|
|
77
|
+
return Buffer.from(decoded, "utf8").toString("base64") === key ? decoded : null;
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
export function assertSafeEndpointUrl(raw) {
|
|
84
|
+
return parseSecureUrl("The service endpoint", raw).toString();
|
|
85
|
+
}
|
|
86
|
+
export function assertSafeOpaqueId(label, raw) {
|
|
87
|
+
assertNoControlChars(label, raw);
|
|
88
|
+
if (raw.length === 0 || raw.length > 256 || raw.includes("/")) {
|
|
89
|
+
throw new Error(`${label} is not a valid identifier (${raw})`);
|
|
90
|
+
}
|
|
91
|
+
return raw;
|
|
92
|
+
}
|
|
93
|
+
export function assertSafeBrowserUrl(raw) {
|
|
94
|
+
assertNoControlChars("The approval URL", raw);
|
|
95
|
+
let url;
|
|
96
|
+
try {
|
|
97
|
+
url = new URL(raw);
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
throw new Error(`The approval URL is not a valid URL: ${raw}`);
|
|
101
|
+
}
|
|
102
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") {
|
|
103
|
+
throw new Error(`Refusing to open a non-web URL: ${raw}`);
|
|
104
|
+
}
|
|
105
|
+
return url.toString();
|
|
106
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import * as p from "@clack/prompts";
|
|
2
|
+
export function isInteractive() {
|
|
3
|
+
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
4
|
+
}
|
|
5
|
+
export async function confirmOrFallback(message, fallback) {
|
|
6
|
+
if (!isInteractive()) {
|
|
7
|
+
p.log.info(`${message} → ${fallback ? "yes" : "no"} (non-interactive default)`);
|
|
8
|
+
return fallback;
|
|
9
|
+
}
|
|
10
|
+
const answer = await p.confirm({ message });
|
|
11
|
+
return p.isCancel(answer) ? false : answer;
|
|
12
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export const NUSTACK_JSON_VERSION = 1;
|
|
2
|
+
export function printJsonSuccess(command, data) {
|
|
3
|
+
console.log(JSON.stringify({ nustackJsonVersion: NUSTACK_JSON_VERSION, command, ok: true, data }));
|
|
4
|
+
}
|
|
5
|
+
export function printJsonError(command, error) {
|
|
6
|
+
console.log(JSON.stringify({ nustackJsonVersion: NUSTACK_JSON_VERSION, command, ok: false, error }));
|
|
7
|
+
process.exitCode = 1;
|
|
8
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { createInterface } from "node:readline";
|
|
3
|
+
export function claudeLaneArgs(permissionMode, resumeSessionId) {
|
|
4
|
+
const args = [
|
|
5
|
+
"-p",
|
|
6
|
+
"--input-format",
|
|
7
|
+
"stream-json",
|
|
8
|
+
"--output-format",
|
|
9
|
+
"stream-json",
|
|
10
|
+
"--verbose",
|
|
11
|
+
"--include-partial-messages",
|
|
12
|
+
"--permission-prompt-tool",
|
|
13
|
+
"stdio",
|
|
14
|
+
];
|
|
15
|
+
if (permissionMode)
|
|
16
|
+
args.push("--permission-mode", permissionMode);
|
|
17
|
+
if (resumeSessionId)
|
|
18
|
+
args.push("--resume", resumeSessionId);
|
|
19
|
+
return args;
|
|
20
|
+
}
|
|
21
|
+
const DENY_MESSAGE = "Denied by the user in NuStack desktop.";
|
|
22
|
+
const STOP_KILL_GRACE_MS = 3_000;
|
|
23
|
+
export class ClaudeLane {
|
|
24
|
+
child;
|
|
25
|
+
pendingApprovals = new Map();
|
|
26
|
+
interruptSeq = 0;
|
|
27
|
+
sessionIdSeen = false;
|
|
28
|
+
killTimer = null;
|
|
29
|
+
options;
|
|
30
|
+
constructor(options) {
|
|
31
|
+
this.options = options;
|
|
32
|
+
const spawnImpl = options.spawnImpl ?? spawn;
|
|
33
|
+
this.child = spawnImpl(options.claudeBin ?? "claude", claudeLaneArgs(options.permissionMode, options.resumeSessionId), {
|
|
34
|
+
cwd: options.cwd,
|
|
35
|
+
env: options.env ?? process.env,
|
|
36
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
37
|
+
});
|
|
38
|
+
this.child.on("error", (error) => {
|
|
39
|
+
const code = error.code === "ENOENT" ? "CLAUDE_NOT_FOUND" : "ADAPTER_FAILED";
|
|
40
|
+
options.onProtocolError(code, error.message);
|
|
41
|
+
});
|
|
42
|
+
this.child.on("exit", (code) => {
|
|
43
|
+
if (this.killTimer)
|
|
44
|
+
clearTimeout(this.killTimer);
|
|
45
|
+
this.killTimer = null;
|
|
46
|
+
options.onExit(code);
|
|
47
|
+
});
|
|
48
|
+
this.child.stderr.on("data", () => {
|
|
49
|
+
});
|
|
50
|
+
const lines = createInterface({ input: this.child.stdout });
|
|
51
|
+
lines.on("line", (line) => this.handleLine(line));
|
|
52
|
+
}
|
|
53
|
+
write(payload) {
|
|
54
|
+
if (this.child.stdin.writable)
|
|
55
|
+
this.child.stdin.write(JSON.stringify(payload) + "\n");
|
|
56
|
+
}
|
|
57
|
+
handleLine(line) {
|
|
58
|
+
let frame;
|
|
59
|
+
try {
|
|
60
|
+
const parsed = JSON.parse(line);
|
|
61
|
+
if (typeof parsed !== "object" || parsed === null)
|
|
62
|
+
throw new Error("not an object");
|
|
63
|
+
frame = parsed;
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
this.options.onProtocolError("ADAPTER_FAILED", `unparseable provider line: ${line.slice(0, 120)}`);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (frame.type === "control_request") {
|
|
70
|
+
this.handleControlRequest(frame);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (!this.sessionIdSeen && frame.type === "system" && frame.subtype === "init" && typeof frame.session_id === "string") {
|
|
74
|
+
this.sessionIdSeen = true;
|
|
75
|
+
this.options.onSessionId?.(frame.session_id);
|
|
76
|
+
}
|
|
77
|
+
this.options.onEvent(frame);
|
|
78
|
+
}
|
|
79
|
+
handleControlRequest(frame) {
|
|
80
|
+
const requestId = typeof frame.request_id === "string" ? frame.request_id : null;
|
|
81
|
+
const request = (typeof frame.request === "object" && frame.request !== null ? frame.request : {});
|
|
82
|
+
const subtype = typeof request.subtype === "string" ? request.subtype : null;
|
|
83
|
+
if (requestId === null) {
|
|
84
|
+
this.options.onProtocolError("ADAPTER_FAILED", "control_request without request_id — cannot answer; ignored");
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (subtype !== "can_use_tool") {
|
|
88
|
+
this.write({ type: "control_response", response: { subtype: "error", request_id: requestId, error: `unsupported control request: ${String(subtype)}` } });
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const toolName = typeof request.tool_name === "string" ? request.tool_name : null;
|
|
92
|
+
if (toolName === null) {
|
|
93
|
+
this.write({
|
|
94
|
+
type: "control_response",
|
|
95
|
+
response: { subtype: "success", request_id: requestId, response: { behavior: "deny", message: DENY_MESSAGE } },
|
|
96
|
+
});
|
|
97
|
+
this.options.onProtocolError("ADAPTER_FAILED", "can_use_tool without tool_name — denied fail-closed");
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
this.pendingApprovals.set(requestId, { toolInput: request.input });
|
|
101
|
+
this.options.onApprovalRequest({
|
|
102
|
+
requestId,
|
|
103
|
+
toolName,
|
|
104
|
+
displayName: typeof request.display_name === "string" ? request.display_name : null,
|
|
105
|
+
toolInput: request.input,
|
|
106
|
+
description: typeof request.description === "string" ? request.description : null,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
sendTurn(text) {
|
|
110
|
+
this.write({ type: "user", message: { role: "user", content: [{ type: "text", text }] } });
|
|
111
|
+
}
|
|
112
|
+
respondApproval(requestId, verdict) {
|
|
113
|
+
const pending = this.pendingApprovals.get(requestId);
|
|
114
|
+
if (!pending) {
|
|
115
|
+
this.options.onProtocolError("APPROVAL_UNROUTABLE", `no pending approval ${requestId} — verdict dropped`);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
this.pendingApprovals.delete(requestId);
|
|
119
|
+
const response = verdict === "allow"
|
|
120
|
+
? { behavior: "allow", updatedInput: pending.toolInput }
|
|
121
|
+
: { behavior: "deny", message: DENY_MESSAGE };
|
|
122
|
+
this.write({ type: "control_response", response: { subtype: "success", request_id: requestId, response } });
|
|
123
|
+
}
|
|
124
|
+
interrupt() {
|
|
125
|
+
this.interruptSeq += 1;
|
|
126
|
+
this.write({ type: "control_request", request_id: `nustack-interrupt-${this.interruptSeq}`, request: { subtype: "interrupt" } });
|
|
127
|
+
}
|
|
128
|
+
stop() {
|
|
129
|
+
this.child.stdin.end();
|
|
130
|
+
this.killTimer = setTimeout(() => {
|
|
131
|
+
this.child.kill("SIGKILL");
|
|
132
|
+
}, STOP_KILL_GRACE_MS);
|
|
133
|
+
this.killTimer.unref?.();
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
export const FRAME_VERSION = 1;
|
|
2
|
+
const INBOUND_TYPES = new Set([
|
|
3
|
+
"lane.open",
|
|
4
|
+
"lane.turn",
|
|
5
|
+
"lane.approval",
|
|
6
|
+
"lane.interrupt",
|
|
7
|
+
"lane.close",
|
|
8
|
+
"engine.shutdown",
|
|
9
|
+
]);
|
|
10
|
+
export function parseInboundFrame(line) {
|
|
11
|
+
let raw;
|
|
12
|
+
try {
|
|
13
|
+
raw = JSON.parse(line);
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return { ok: false, error: { code: "BAD_FRAME", name: null, message: "not JSON" } };
|
|
17
|
+
}
|
|
18
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
19
|
+
return { ok: false, error: { code: "BAD_FRAME", name: null, message: "not an object" } };
|
|
20
|
+
}
|
|
21
|
+
const frame = raw;
|
|
22
|
+
const type = typeof frame.type === "string" ? frame.type : null;
|
|
23
|
+
if (type === null)
|
|
24
|
+
return { ok: false, error: { code: "BAD_FRAME", name: null, message: "missing type" } };
|
|
25
|
+
if (!INBOUND_TYPES.has(type)) {
|
|
26
|
+
return { ok: false, error: { code: "UNKNOWN_FRAME", name: type, message: `unknown frame type: ${type}` } };
|
|
27
|
+
}
|
|
28
|
+
if (frame.v !== FRAME_VERSION) {
|
|
29
|
+
return { ok: false, error: { code: "BAD_FRAME", name: type, message: `unsupported frame version: ${String(frame.v)}` } };
|
|
30
|
+
}
|
|
31
|
+
const requireString = (key) => (typeof frame[key] === "string" && frame[key] !== "" ? frame[key] : null);
|
|
32
|
+
switch (type) {
|
|
33
|
+
case "engine.shutdown":
|
|
34
|
+
return { ok: true, frame: { v: FRAME_VERSION, type } };
|
|
35
|
+
case "lane.open": {
|
|
36
|
+
const laneId = requireString("laneId");
|
|
37
|
+
const projectPath = requireString("projectPath");
|
|
38
|
+
if (!laneId || !projectPath)
|
|
39
|
+
return { ok: false, error: { code: "BAD_FRAME", name: type, message: "lane.open requires laneId and projectPath" } };
|
|
40
|
+
const permissionMode = typeof frame.permissionMode === "string" ? frame.permissionMode : undefined;
|
|
41
|
+
const isolated = frame.isolated === true ? true : undefined;
|
|
42
|
+
const resumeSessionId = typeof frame.resumeSessionId === "string" && frame.resumeSessionId !== "" ? frame.resumeSessionId : undefined;
|
|
43
|
+
return { ok: true, frame: { v: FRAME_VERSION, type, laneId, projectPath, permissionMode, isolated, resumeSessionId } };
|
|
44
|
+
}
|
|
45
|
+
case "lane.turn": {
|
|
46
|
+
const laneId = requireString("laneId");
|
|
47
|
+
const text = typeof frame.text === "string" ? frame.text : null;
|
|
48
|
+
if (!laneId || text === null)
|
|
49
|
+
return { ok: false, error: { code: "BAD_FRAME", name: type, message: "lane.turn requires laneId and text" } };
|
|
50
|
+
return { ok: true, frame: { v: FRAME_VERSION, type, laneId, text } };
|
|
51
|
+
}
|
|
52
|
+
case "lane.approval": {
|
|
53
|
+
const laneId = requireString("laneId");
|
|
54
|
+
const requestId = requireString("requestId");
|
|
55
|
+
const verdict = frame.verdict === "allow" || frame.verdict === "deny" ? frame.verdict : null;
|
|
56
|
+
if (!laneId || !requestId || !verdict)
|
|
57
|
+
return { ok: false, error: { code: "BAD_FRAME", name: type, message: "lane.approval requires laneId, requestId, verdict" } };
|
|
58
|
+
return { ok: true, frame: { v: FRAME_VERSION, type, laneId, requestId, verdict } };
|
|
59
|
+
}
|
|
60
|
+
case "lane.interrupt":
|
|
61
|
+
case "lane.close": {
|
|
62
|
+
const laneId = requireString("laneId");
|
|
63
|
+
if (!laneId)
|
|
64
|
+
return { ok: false, error: { code: "BAD_FRAME", name: type, message: `${type} requires laneId` } };
|
|
65
|
+
return { ok: true, frame: { v: FRAME_VERSION, type, laneId } };
|
|
66
|
+
}
|
|
67
|
+
default:
|
|
68
|
+
return { ok: false, error: { code: "UNKNOWN_FRAME", name: type, message: `unknown frame type: ${type}` } };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
export function serializeFrame(frame) {
|
|
72
|
+
return JSON.stringify(frame) + "\n";
|
|
73
|
+
}
|