Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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