Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,14 @@ repos:
pass_filenames: true
priority: 5

- id: prettier-js
name: Prettier (JavaScript)
entry: npx prettier --write
language: system
files: ^(bin|test)/.*\.js$
pass_filenames: true
priority: 5

# ── Priority 6: auto-fix after formatting ─────────────────────────────────
- repo: local
hooks:
Expand Down
8 changes: 8 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
node_modules/
dist/
coverage/
nemoclaw/node_modules/
nemoclaw/dist/
nemoclaw-blueprint/
docs/_build/
*.md
7 changes: 7 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"semi": true,
"singleQuote": false,
"trailingComma": "all",
"printWidth": 100,
"tabWidth": 2
}
5 changes: 4 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ lint: check
lint-ts:
cd nemoclaw && npm run check

format: format-ts
format: format-ts format-cli

format-cli:
npx prettier --write 'bin/**/*.js' 'test/**/*.js'

format-ts:
cd nemoclaw && npm run lint:fix && npm run format
Expand Down
22 changes: 15 additions & 7 deletions bin/lib/credentials.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,25 +14,29 @@ function resolveHomeDir() {
if (!raw) {
throw new Error(
"Cannot determine safe home directory for credential storage. " +
"Set the HOME environment variable to a user-owned directory."
"Set the HOME environment variable to a user-owned directory.",
);
}
const home = path.resolve(raw);
try {
const real = fs.realpathSync(home);
if (UNSAFE_HOME_PATHS.has(real)) {
throw new Error(
"Cannot store credentials: HOME resolves to '" + real + "' which is world-readable. " +
"Set the HOME environment variable to a user-owned directory."
"Cannot store credentials: HOME resolves to '" +
real +
"' which is world-readable. " +
"Set the HOME environment variable to a user-owned directory.",
);
}
} catch (e) {
if (e.code !== "ENOENT") throw e;
}
if (UNSAFE_HOME_PATHS.has(home)) {
throw new Error(
"Cannot store credentials: HOME resolves to '" + home + "' which is world-readable. " +
"Set the HOME environment variable to a user-owned directory."
"Cannot store credentials: HOME resolves to '" +
home +
"' which is world-readable. " +
"Set the HOME environment variable to a user-owned directory.",
);
}
return home;
Expand All @@ -57,7 +61,9 @@ function loadCredentials() {
if (fs.existsSync(file)) {
return JSON.parse(fs.readFileSync(file, "utf-8"));
}
} catch { /* ignored */ }
} catch {
/* ignored */
}
return {};
}

Expand Down Expand Up @@ -277,7 +283,9 @@ async function ensureGithubToken() {
process.env.GITHUB_TOKEN = token;
return;
}
} catch { /* ignored */ }
} catch {
/* ignored */
}

console.log("");
console.log(" ┌──────────────────────────────────────────────────┐");
Expand Down
16 changes: 12 additions & 4 deletions bin/lib/local-inference.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,8 @@ function validateLocalProvider(provider, runCapture) {
case "ollama-local":
return {
ok: false,
message: "Local Ollama was selected, but nothing is responding on http://localhost:11434.",
message:
"Local Ollama was selected, but nothing is responding on http://localhost:11434.",
};
default:
return { ok: false, message: "The selected local inference provider is unavailable." };
Expand Down Expand Up @@ -101,7 +102,10 @@ function validateLocalProvider(provider, runCapture) {
"Local Ollama is responding on localhost, but containers cannot reach http://host.openshell.internal:11434. Ensure Ollama listens on 0.0.0.0:11434 instead of 127.0.0.1 so sandboxes can reach it.",
};
default:
return { ok: false, message: "The selected local inference provider is unavailable from containers." };
return {
ok: false,
message: "The selected local inference provider is unavailable from containers.",
};
}
}

Expand All @@ -127,7 +131,9 @@ function parseOllamaTags(output) {
}

function getOllamaModelOptions(runCapture) {
const tagsOutput = runCapture("curl -sf http://localhost:11434/api/tags 2>/dev/null", { ignoreError: true });
const tagsOutput = runCapture("curl -sf http://localhost:11434/api/tags 2>/dev/null", {
ignoreError: true,
});
const tagsParsed = parseOllamaTags(tagsOutput);
if (tagsParsed.length > 0) {
return tagsParsed;
Expand Down Expand Up @@ -193,7 +199,9 @@ function validateOllamaModel(model, runCapture) {
message: `Selected Ollama model '${model}' failed the local probe: ${parsed.error.trim()}`,
};
}
} catch { /* ignored */ }
} catch {
/* ignored */
}

return { ok: true };
}
Expand Down
56 changes: 32 additions & 24 deletions bin/lib/nim.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,9 @@ function listModels() {
function detectGpu() {
// Try NVIDIA first — query VRAM
try {
const output = runCapture(
"nvidia-smi --query-gpu=memory.total --format=csv,noheader,nounits",
{ ignoreError: true }
);
const output = runCapture("nvidia-smi --query-gpu=memory.total --format=csv,noheader,nounits", {
ignoreError: true,
});
if (output) {
const lines = output.split("\n").filter((l) => l.trim());
const perGpuMB = lines.map((l) => parseInt(l.trim(), 10)).filter((n) => !isNaN(n));
Expand All @@ -46,21 +45,24 @@ function detectGpu() {
};
}
}
} catch { /* ignored */ }
} catch {
/* ignored */
}

// Fallback: DGX Spark (GB10) — VRAM not queryable due to unified memory architecture
try {
const nameOutput = runCapture(
"nvidia-smi --query-gpu=name --format=csv,noheader,nounits",
{ ignoreError: true }
);
const nameOutput = runCapture("nvidia-smi --query-gpu=name --format=csv,noheader,nounits", {
ignoreError: true,
});
if (nameOutput && nameOutput.includes("GB10")) {
// GB10 has 128GB unified memory shared with Grace CPU — use system RAM
let totalMemoryMB = 0;
try {
const memLine = runCapture("free -m | awk '/Mem:/ {print $2}'", { ignoreError: true });
if (memLine) totalMemoryMB = parseInt(memLine.trim(), 10) || 0;
} catch { /* ignored */ }
} catch {
/* ignored */
}
return {
type: "nvidia",
count: 1,
Expand All @@ -70,15 +72,16 @@ function detectGpu() {
spark: true,
};
}
} catch { /* ignored */ }
} catch {
/* ignored */
}

// macOS: detect Apple Silicon or discrete GPU
if (process.platform === "darwin") {
try {
const spOutput = runCapture(
"system_profiler SPDisplaysDataType 2>/dev/null",
{ ignoreError: true }
);
const spOutput = runCapture("system_profiler SPDisplaysDataType 2>/dev/null", {
ignoreError: true,
});
if (spOutput) {
const chipMatch = spOutput.match(/Chipset Model:\s*(.+)/);
const vramMatch = spOutput.match(/VRAM.*?:\s*(\d+)\s*(MB|GB)/i);
Expand All @@ -96,7 +99,9 @@ function detectGpu() {
try {
const memBytes = runCapture("sysctl -n hw.memsize", { ignoreError: true });
if (memBytes) memoryMB = Math.floor(parseInt(memBytes, 10) / 1024 / 1024);
} catch { /* ignored */ }
} catch {
/* ignored */
}
}

return {
Expand All @@ -110,7 +115,9 @@ function detectGpu() {
};
}
}
} catch { /* ignored */ }
} catch {
/* ignored */
}
}

return null;
Expand Down Expand Up @@ -145,7 +152,7 @@ function startNimContainerByName(name, model, port = 8000) {

console.log(` Starting NIM container: ${name}`);
run(
`docker run -d --gpus all -p ${Number(port)}:8000 --name ${qn} --shm-size 16g ${shellQuote(image)}`
`docker run -d --gpus all -p ${Number(port)}:8000 --name ${qn} --shm-size 16g ${shellQuote(image)}`,
);
return name;
}
Expand All @@ -165,7 +172,9 @@ function waitForNimHealth(port = 8000, timeout = 300) {
console.log(" NIM is healthy.");
return true;
}
} catch { /* ignored */ }
} catch {
/* ignored */
}
require("child_process").spawnSync("sleep", [String(intervalSec)]);
}
console.error(` NIM did not become healthy within ${timeout}s.`);
Expand All @@ -192,10 +201,9 @@ function nimStatus(sandboxName, port) {
function nimStatusByName(name, port) {
try {
const qn = shellQuote(name);
const state = runCapture(
`docker inspect --format '{{.State.Status}}' ${qn} 2>/dev/null`,
{ ignoreError: true }
);
const state = runCapture(`docker inspect --format '{{.State.Status}}' ${qn} 2>/dev/null`, {
ignoreError: true,
});
if (!state) return { running: false, container: name };

let healthy = false;
Expand All @@ -210,7 +218,7 @@ function nimStatusByName(name, port) {
}
const health = runCapture(
`curl -sf http://localhost:${resolvedHostPort}/v1/models 2>/dev/null`,
{ ignoreError: true }
{ ignoreError: true },
);
healthy = !!health;
}
Expand Down
28 changes: 19 additions & 9 deletions bin/lib/onboard-session.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,9 @@ function createSession(overrides = {}) {
credentialEnv: overrides.credentialEnv || null,
preferredInferenceApi: overrides.preferredInferenceApi || null,
nimContainer: overrides.nimContainer || null,
policyPresets: Array.isArray(overrides.policyPresets) ? overrides.policyPresets.filter((value) => typeof value === "string") : null,
policyPresets: Array.isArray(overrides.policyPresets)
? overrides.policyPresets.filter((value) => typeof value === "string")
: null,
metadata: {
gatewayName: overrides.metadata?.gatewayName || "nemoclaw",
},
Expand All @@ -72,7 +74,10 @@ function isObject(value) {
function redactSensitiveText(value) {
if (typeof value !== "string") return null;
return value
.replace(/(NVIDIA_API_KEY|OPENAI_API_KEY|ANTHROPIC_API_KEY|GEMINI_API_KEY|COMPATIBLE_API_KEY|COMPATIBLE_ANTHROPIC_API_KEY)=\S+/gi, "$1=<REDACTED>")
.replace(
/(NVIDIA_API_KEY|OPENAI_API_KEY|ANTHROPIC_API_KEY|GEMINI_API_KEY|COMPATIBLE_API_KEY|COMPATIBLE_ANTHROPIC_API_KEY)=\S+/gi,
"$1=<REDACTED>",
)
.replace(/Bearer\s+\S+/gi, "Bearer <REDACTED>")
.replace(/nvapi-[A-Za-z0-9_-]{10,}/g, "<REDACTED>")
.replace(/ghp_[A-Za-z0-9]{20,}/g, "<REDACTED>")
Expand All @@ -84,7 +89,8 @@ function sanitizeFailure(input) {
if (!input) return null;
const step = typeof input.step === "string" ? input.step : null;
const message = redactSensitiveText(input.message);
const recordedAt = typeof input.recordedAt === "string" ? input.recordedAt : new Date().toISOString();
const recordedAt =
typeof input.recordedAt === "string" ? input.recordedAt : new Date().toISOString();
return step || message ? { step, message, recordedAt } : null;
}

Expand Down Expand Up @@ -127,9 +133,12 @@ function normalizeSession(data) {
model: typeof data.model === "string" ? data.model : null,
endpointUrl: typeof data.endpointUrl === "string" ? redactUrl(data.endpointUrl) : null,
credentialEnv: typeof data.credentialEnv === "string" ? data.credentialEnv : null,
preferredInferenceApi: typeof data.preferredInferenceApi === "string" ? data.preferredInferenceApi : null,
preferredInferenceApi:
typeof data.preferredInferenceApi === "string" ? data.preferredInferenceApi : null,
nimContainer: typeof data.nimContainer === "string" ? data.nimContainer : null,
policyPresets: Array.isArray(data.policyPresets) ? data.policyPresets.filter((value) => typeof value === "string") : null,
policyPresets: Array.isArray(data.policyPresets)
? data.policyPresets.filter((value) => typeof value === "string")
: null,
lastStepStarted: typeof data.lastStepStarted === "string" ? data.lastStepStarted : null,
lastCompletedStep: typeof data.lastCompletedStep === "string" ? data.lastCompletedStep : null,
failure: sanitizeFailure(data.failure),
Expand Down Expand Up @@ -172,7 +181,7 @@ function saveSession(session) {
ensureSessionDir();
const tmpFile = path.join(
SESSION_DIR,
`.onboard-session.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`
`.onboard-session.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`,
);
fs.writeFileSync(tmpFile, JSON.stringify(normalized, null, 2), { mode: 0o600 });
fs.renameSync(tmpFile, SESSION_FILE);
Expand Down Expand Up @@ -222,7 +231,7 @@ function acquireOnboardLock(command = null) {
command: typeof command === "string" ? command : null,
},
null,
2
2,
);

for (let attempt = 0; attempt < 2; attempt++) {
Expand Down Expand Up @@ -358,7 +367,8 @@ function filterSafeUpdates(updates) {
if (typeof updates.model === "string") safe.model = updates.model;
if (typeof updates.endpointUrl === "string") safe.endpointUrl = redactUrl(updates.endpointUrl);
if (typeof updates.credentialEnv === "string") safe.credentialEnv = updates.credentialEnv;
if (typeof updates.preferredInferenceApi === "string") safe.preferredInferenceApi = updates.preferredInferenceApi;
if (typeof updates.preferredInferenceApi === "string")
safe.preferredInferenceApi = updates.preferredInferenceApi;
if (typeof updates.nimContainer === "string") safe.nimContainer = updates.nimContainer;
if (Array.isArray(updates.policyPresets)) {
safe.policyPresets = updates.policyPresets.filter((value) => typeof value === "string");
Expand Down Expand Up @@ -401,7 +411,7 @@ function summarizeForDebug(session = loadSession()) {
completedAt: step.completedAt,
error: step.error,
},
])
]),
),
};
}
Expand Down
Loading
Loading