alnair 0.1.0
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 +15 -0
- package/dist/backend.js +86 -0
- package/dist/bin.js +7 -0
- package/dist/cli.js +58 -0
- package/dist/detect.js +115 -0
- package/dist/hooks/claude-grant.js +45 -0
- package/dist/hooks/cursor-grant.js +77 -0
- package/dist/init.js +172 -0
- package/dist/inspect.js +62 -0
- package/dist/integrations/agents-md.js +41 -0
- package/dist/integrations/bootstrap.js +64 -0
- package/dist/integrations/claude-hooks.js +70 -0
- package/dist/integrations/claude.js +48 -0
- package/dist/integrations/codex.js +38 -0
- package/dist/integrations/cursor.js +77 -0
- package/dist/integrations/hooks.js +73 -0
- package/dist/integrations/mcp-resolve.js +11 -0
- package/dist/integrations/mcp.js +31 -0
- package/dist/integrations/paths.js +54 -0
- package/dist/login.js +11 -0
- package/dist/logs.js +58 -0
- package/dist/mint.js +76 -0
- package/dist/status.js +47 -0
- package/dist/ui.js +64 -0
- package/dist/whoami.js +24 -0
- package/package.json +43 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Install alnair-sdk + alnair-mcp into the user's project so hooks and MCP resolve locally.
|
|
3
|
+
*/
|
|
4
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
5
|
+
import { existsSync } from "node:fs";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { execFile } from "node:child_process";
|
|
8
|
+
import { promisify } from "node:util";
|
|
9
|
+
import { cliPackageVersion } from "./paths.js";
|
|
10
|
+
const execFileAsync = promisify(execFile);
|
|
11
|
+
const ALNAIR_PACKAGES = ["alnair-sdk", "alnair-mcp"];
|
|
12
|
+
export async function ensureProjectDeps(cwd) {
|
|
13
|
+
const version = cliPackageVersion();
|
|
14
|
+
const range = `^${version}`;
|
|
15
|
+
const pkgPath = join(cwd, "package.json");
|
|
16
|
+
let pkg;
|
|
17
|
+
const hadFile = existsSync(pkgPath);
|
|
18
|
+
if (hadFile) {
|
|
19
|
+
try {
|
|
20
|
+
pkg = JSON.parse(await readFile(pkgPath, "utf8"));
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
pkg = defaultPackageJson(cwd);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
pkg = defaultPackageJson(cwd);
|
|
28
|
+
}
|
|
29
|
+
const devDeps = pkg.devDependencies ?? {};
|
|
30
|
+
let changed = !hadFile;
|
|
31
|
+
for (const name of ALNAIR_PACKAGES) {
|
|
32
|
+
if (devDeps[name] !== range) {
|
|
33
|
+
devDeps[name] = range;
|
|
34
|
+
changed = true;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (!changed)
|
|
38
|
+
return "unchanged";
|
|
39
|
+
pkg.devDependencies = devDeps;
|
|
40
|
+
await writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
|
|
41
|
+
try {
|
|
42
|
+
await execFileAsync("npm", ["install", "--no-fund", "--no-audit", "--prefer-offline"], {
|
|
43
|
+
cwd,
|
|
44
|
+
timeout: 120_000,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
/* hooks fall back to npx / monorepo paths */
|
|
49
|
+
}
|
|
50
|
+
return hadFile ? "updated" : "installed";
|
|
51
|
+
}
|
|
52
|
+
function defaultPackageJson(cwd) {
|
|
53
|
+
const name = cwd
|
|
54
|
+
.split(/[/\\]/)
|
|
55
|
+
.filter(Boolean)
|
|
56
|
+
.pop()
|
|
57
|
+
?.replace(/[^a-z0-9-]/gi, "-")
|
|
58
|
+
.toLowerCase() || "project";
|
|
59
|
+
return {
|
|
60
|
+
name,
|
|
61
|
+
private: true,
|
|
62
|
+
version: "0.0.0",
|
|
63
|
+
};
|
|
64
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Install Claude Code settings so grant UX reaches the developer.
|
|
3
|
+
* MCP stderr is captured by Claude Code — systemMessage is the channel.
|
|
4
|
+
*/
|
|
5
|
+
import { chmod, copyFile, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
6
|
+
import { existsSync } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { resolveBundledHook } from "./paths.js";
|
|
9
|
+
const MATCHER = "mcp__alnair__mintCapability";
|
|
10
|
+
function grantHookCommand() {
|
|
11
|
+
return 'node "$CLAUDE_PROJECT_DIR/.claude/hooks/alnair-grant.mjs"';
|
|
12
|
+
}
|
|
13
|
+
async function installClaudeGrantHook(cwd) {
|
|
14
|
+
const hooksDir = join(cwd, ".claude", "hooks");
|
|
15
|
+
await mkdir(hooksDir, { recursive: true });
|
|
16
|
+
const built = resolveBundledHook("claude-grant");
|
|
17
|
+
const dest = join(hooksDir, "alnair-grant.mjs");
|
|
18
|
+
if (existsSync(built)) {
|
|
19
|
+
await copyFile(built, dest);
|
|
20
|
+
await chmod(dest, 0o755);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export async function installClaudeCodeHooks(cwd) {
|
|
24
|
+
await installClaudeGrantHook(cwd);
|
|
25
|
+
const dir = join(cwd, ".claude");
|
|
26
|
+
await mkdir(dir, { recursive: true });
|
|
27
|
+
const path = join(dir, "settings.json");
|
|
28
|
+
let existing = {};
|
|
29
|
+
if (existsSync(path)) {
|
|
30
|
+
try {
|
|
31
|
+
existing = JSON.parse(await readFile(path, "utf8"));
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
existing = {};
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const command = grantHookCommand();
|
|
38
|
+
const hooks = existing.hooks ?? {};
|
|
39
|
+
const post = Array.isArray(hooks.PostToolUse) ? [...hooks.PostToolUse] : [];
|
|
40
|
+
const entry = {
|
|
41
|
+
matcher: MATCHER,
|
|
42
|
+
hooks: [{ type: "command", command }],
|
|
43
|
+
};
|
|
44
|
+
const idx = post.findIndex((h) => h &&
|
|
45
|
+
typeof h === "object" &&
|
|
46
|
+
h.matcher === MATCHER);
|
|
47
|
+
let changed = false;
|
|
48
|
+
if (idx >= 0) {
|
|
49
|
+
const prev = JSON.stringify(post[idx]);
|
|
50
|
+
post[idx] = entry;
|
|
51
|
+
if (JSON.stringify(post[idx]) !== prev)
|
|
52
|
+
changed = true;
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
post.push(entry);
|
|
56
|
+
changed = true;
|
|
57
|
+
}
|
|
58
|
+
if (!changed && existsSync(path))
|
|
59
|
+
return "unchanged";
|
|
60
|
+
const next = {
|
|
61
|
+
...existing,
|
|
62
|
+
hooks: {
|
|
63
|
+
...hooks,
|
|
64
|
+
PostToolUse: post,
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
const hadFile = existsSync(path);
|
|
68
|
+
await writeFile(path, JSON.stringify(next, null, 2) + "\n", "utf8");
|
|
69
|
+
return hadFile ? "updated" : "created";
|
|
70
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Managed CLAUDE.md — Alnair stays invisible.
|
|
3
|
+
* Claude just commits/pushes. Hooks grant access automatically.
|
|
4
|
+
*/
|
|
5
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
6
|
+
import { existsSync } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
export const BEGIN = "<!-- BEGIN ALNAIR -->";
|
|
9
|
+
export const END = "<!-- END ALNAIR -->";
|
|
10
|
+
export const ALNAIR_BLOCK = `${BEGIN}
|
|
11
|
+
## Privileged actions (required)
|
|
12
|
+
|
|
13
|
+
**Before** git commit, git push, deploy, or DB writes, call the Alnair MCP tool
|
|
14
|
+
\`requestAccess\` (or \`mintCapability\`). Do this on your own — the user will
|
|
15
|
+
not remind you. The tool result is the grant notice the user should see:
|
|
16
|
+
|
|
17
|
+
[alnair] Git access granted
|
|
18
|
+
|
|
19
|
+
- \`git commit\` → \`permissions: ["github.git.commit"]\`
|
|
20
|
+
- \`git push\` → \`permissions: ["github.git.push"]\`
|
|
21
|
+
- merge PR → \`permissions: ["github.pull_request.merge"]\`
|
|
22
|
+
- deploy → \`permissions: ["vercel.deploy.preview"]\`
|
|
23
|
+
- database writes → \`permissions: ["supabase.write"]\`
|
|
24
|
+
|
|
25
|
+
\`task\` = the real commit subject / mission. Only request what you need.
|
|
26
|
+
Then proceed with the action. If a git hook blocks you, call requestAccess and retry.
|
|
27
|
+
${END}`;
|
|
28
|
+
export async function installClaudeIntegration(cwd) {
|
|
29
|
+
const path = join(cwd, "CLAUDE.md");
|
|
30
|
+
if (!existsSync(path)) {
|
|
31
|
+
await writeFile(path, ALNAIR_BLOCK + "\n", "utf8");
|
|
32
|
+
return { status: "created" };
|
|
33
|
+
}
|
|
34
|
+
const raw = await readFile(path, "utf8");
|
|
35
|
+
if (raw.includes(BEGIN) && raw.includes(END)) {
|
|
36
|
+
const next = raw.replace(new RegExp(`${escapeRegExp(BEGIN)}[\\s\\S]*?${escapeRegExp(END)}`), ALNAIR_BLOCK);
|
|
37
|
+
if (next === raw)
|
|
38
|
+
return { status: "unchanged" };
|
|
39
|
+
await writeFile(path, next, "utf8");
|
|
40
|
+
return { status: "updated" };
|
|
41
|
+
}
|
|
42
|
+
const sep = raw.endsWith("\n") ? "\n" : "\n\n";
|
|
43
|
+
await writeFile(path, raw + sep + ALNAIR_BLOCK + "\n", "utf8");
|
|
44
|
+
return { status: "updated" };
|
|
45
|
+
}
|
|
46
|
+
function escapeRegExp(s) {
|
|
47
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
48
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex — project MCP via `.codex/config.toml`.
|
|
3
|
+
* Also used when Conductor hosts a Codex session.
|
|
4
|
+
*/
|
|
5
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
6
|
+
import { existsSync } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { resolveAlnairMcpEntry } from "./mcp-resolve.js";
|
|
9
|
+
export async function installCodexIntegration(cwd) {
|
|
10
|
+
const dir = join(cwd, ".codex");
|
|
11
|
+
await mkdir(dir, { recursive: true });
|
|
12
|
+
const path = join(dir, "config.toml");
|
|
13
|
+
const entry = resolveAlnairMcpEntry(cwd);
|
|
14
|
+
const block = [
|
|
15
|
+
"[mcp_servers.alnair]",
|
|
16
|
+
`command = ${tomlStr(entry.command)}`,
|
|
17
|
+
`args = [${entry.args.map(tomlStr).join(", ")}]`,
|
|
18
|
+
"",
|
|
19
|
+
].join("\n");
|
|
20
|
+
if (!existsSync(path)) {
|
|
21
|
+
await writeFile(path, block, "utf8");
|
|
22
|
+
return "created";
|
|
23
|
+
}
|
|
24
|
+
const raw = await readFile(path, "utf8");
|
|
25
|
+
if (raw.includes("[mcp_servers.alnair]")) {
|
|
26
|
+
const next = raw.replace(/\[mcp_servers\.alnair\][\s\S]*?(?=\n\[|\n*$)/, block.trimEnd() + "\n");
|
|
27
|
+
if (next === raw)
|
|
28
|
+
return "unchanged";
|
|
29
|
+
await writeFile(path, next, "utf8");
|
|
30
|
+
return "updated";
|
|
31
|
+
}
|
|
32
|
+
const sep = raw.endsWith("\n") ? "\n" : "\n\n";
|
|
33
|
+
await writeFile(path, raw + sep + block, "utf8");
|
|
34
|
+
return "updated";
|
|
35
|
+
}
|
|
36
|
+
function tomlStr(s) {
|
|
37
|
+
return JSON.stringify(s);
|
|
38
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cursor — MCP is the visible grant path (tool calls show in chat).
|
|
3
|
+
* Git hooks still enforce; MCP is what the developer sees.
|
|
4
|
+
*/
|
|
5
|
+
import { writeFile, mkdir, readFile, copyFile, chmod } from "node:fs/promises";
|
|
6
|
+
import { existsSync } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { resolveAlnairMcpEntry } from "./mcp-resolve.js";
|
|
9
|
+
import { resolveBundledHook } from "./paths.js";
|
|
10
|
+
const BODY = `## Permissions (Alnair)
|
|
11
|
+
|
|
12
|
+
Before **git commit**, **git push**, deploy, or DB writes:
|
|
13
|
+
|
|
14
|
+
1. Call the Alnair MCP tool \`requestAccess\` (or \`mintCapability\`) with the
|
|
15
|
+
real task and permissions.
|
|
16
|
+
2. Wait for the tool result — you and the user will see:
|
|
17
|
+
[alnair] Git access granted
|
|
18
|
+
3. Then run the git/deploy command.
|
|
19
|
+
|
|
20
|
+
Permission map:
|
|
21
|
+
- commit → \`["github.git.commit"]\`
|
|
22
|
+
- push → \`["github.git.push"]\`
|
|
23
|
+
|
|
24
|
+
Never skip the MCP call. Never ask the user whether to request access.
|
|
25
|
+
If Alnair MCP is unavailable, proceed with git (hooks still grant) and tell
|
|
26
|
+
the user to enable the **alnair** MCP server in Cursor Settings → MCP.
|
|
27
|
+
`;
|
|
28
|
+
export async function installCursorIntegration(cwd) {
|
|
29
|
+
const rulesDir = join(cwd, ".cursor", "rules");
|
|
30
|
+
await mkdir(rulesDir, { recursive: true });
|
|
31
|
+
await writeFile(join(rulesDir, "alnair.mdc"), `---
|
|
32
|
+
description: Call Alnair requestAccess MCP before commit/push so grants show in chat
|
|
33
|
+
alwaysApply: true
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
${BODY}
|
|
37
|
+
`, "utf8");
|
|
38
|
+
await installCursorMcp(cwd);
|
|
39
|
+
await installCursorHooks(cwd);
|
|
40
|
+
return "created";
|
|
41
|
+
}
|
|
42
|
+
async function installCursorMcp(cwd) {
|
|
43
|
+
const dir = join(cwd, ".cursor");
|
|
44
|
+
await mkdir(dir, { recursive: true });
|
|
45
|
+
const path = join(dir, "mcp.json");
|
|
46
|
+
const alnairEntry = resolveAlnairMcpEntry(cwd);
|
|
47
|
+
let json = {};
|
|
48
|
+
if (existsSync(path)) {
|
|
49
|
+
try {
|
|
50
|
+
json = JSON.parse(await readFile(path, "utf8"));
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
json = {};
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
json.mcpServers = { ...json.mcpServers, alnair: alnairEntry };
|
|
57
|
+
await writeFile(path, JSON.stringify(json, null, 2) + "\n", "utf8");
|
|
58
|
+
}
|
|
59
|
+
async function installCursorHooks(cwd) {
|
|
60
|
+
const hooksDir = join(cwd, ".cursor", "hooks");
|
|
61
|
+
await mkdir(hooksDir, { recursive: true });
|
|
62
|
+
const built = resolveBundledHook("cursor-grant");
|
|
63
|
+
const dest = join(hooksDir, "alnair-grant.mjs");
|
|
64
|
+
if (existsSync(built)) {
|
|
65
|
+
await copyFile(built, dest);
|
|
66
|
+
}
|
|
67
|
+
await chmod(dest, 0o755).catch(() => undefined);
|
|
68
|
+
await writeFile(join(cwd, ".cursor", "hooks.json"), JSON.stringify({
|
|
69
|
+
version: 1,
|
|
70
|
+
hooks: {
|
|
71
|
+
afterShellExecution: [{ command: "node .cursor/hooks/alnair-grant.mjs" }],
|
|
72
|
+
postToolUse: [
|
|
73
|
+
{ matcher: "Shell", command: "node .cursor/hooks/alnair-grant.mjs" },
|
|
74
|
+
],
|
|
75
|
+
},
|
|
76
|
+
}, null, 2) + "\n", "utf8");
|
|
77
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git hooks — autofill + always echo a plain line Cursor tool UIs can show.
|
|
3
|
+
*/
|
|
4
|
+
import { chmod, mkdir, writeFile } from "node:fs/promises";
|
|
5
|
+
import { existsSync } from "node:fs";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { execFile } from "node:child_process";
|
|
8
|
+
import { promisify } from "node:util";
|
|
9
|
+
const execFileAsync = promisify(execFile);
|
|
10
|
+
const PRE_PUSH = `#!/usr/bin/env bash
|
|
11
|
+
# Alnair — grant push access automatically
|
|
12
|
+
set -euo pipefail
|
|
13
|
+
ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
|
|
14
|
+
cd "$ROOT"
|
|
15
|
+
GATE="$ROOT/node_modules/alnair-sdk/dist/gate.js"
|
|
16
|
+
if [ ! -f "$GATE" ]; then
|
|
17
|
+
GATE="$ROOT/packages/sdk/dist/gate.js"
|
|
18
|
+
fi
|
|
19
|
+
if [ -f "$GATE" ]; then
|
|
20
|
+
# Gate mints; we always echo plain text so Cursor's Shell tool UI shows it
|
|
21
|
+
# (agent chats often hide ANSI / collapse hook stdout from node).
|
|
22
|
+
if node "$GATE" github.git.push; then
|
|
23
|
+
echo "[alnair] GitHub access granted"
|
|
24
|
+
else
|
|
25
|
+
echo "[alnair] Could not grant GitHub access"
|
|
26
|
+
exit 1
|
|
27
|
+
fi
|
|
28
|
+
fi
|
|
29
|
+
`;
|
|
30
|
+
const PRE_COMMIT = `#!/usr/bin/env bash
|
|
31
|
+
# Alnair — grant commit access automatically
|
|
32
|
+
set -euo pipefail
|
|
33
|
+
ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
|
|
34
|
+
cd "$ROOT"
|
|
35
|
+
GATE="$ROOT/node_modules/alnair-sdk/dist/gate.js"
|
|
36
|
+
if [ ! -f "$GATE" ]; then
|
|
37
|
+
GATE="$ROOT/packages/sdk/dist/gate.js"
|
|
38
|
+
fi
|
|
39
|
+
if [ -f "$GATE" ]; then
|
|
40
|
+
if node "$GATE" github.git.commit; then
|
|
41
|
+
echo "[alnair] Git access granted"
|
|
42
|
+
else
|
|
43
|
+
echo "[alnair] Could not grant Git access"
|
|
44
|
+
exit 1
|
|
45
|
+
fi
|
|
46
|
+
fi
|
|
47
|
+
`;
|
|
48
|
+
export async function installGitHooks(cwd) {
|
|
49
|
+
if (!existsSync(join(cwd, ".git")))
|
|
50
|
+
return "skipped";
|
|
51
|
+
const hooksDir = join(cwd, ".git", "hooks");
|
|
52
|
+
await mkdir(hooksDir, { recursive: true });
|
|
53
|
+
const alnairHooks = join(cwd, ".alnair", "hooks");
|
|
54
|
+
await mkdir(alnairHooks, { recursive: true });
|
|
55
|
+
const prePush = join(alnairHooks, "pre-push");
|
|
56
|
+
const preCommit = join(alnairHooks, "pre-commit");
|
|
57
|
+
await writeFile(prePush, PRE_PUSH, "utf8");
|
|
58
|
+
await writeFile(preCommit, PRE_COMMIT, "utf8");
|
|
59
|
+
await chmod(prePush, 0o755);
|
|
60
|
+
await chmod(preCommit, 0o755);
|
|
61
|
+
try {
|
|
62
|
+
await execFileAsync("git", ["config", "core.hooksPath", ".alnair/hooks"], {
|
|
63
|
+
cwd,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
await writeFile(join(hooksDir, "pre-push"), PRE_PUSH, "utf8");
|
|
68
|
+
await writeFile(join(hooksDir, "pre-commit"), PRE_COMMIT, "utf8");
|
|
69
|
+
await chmod(join(hooksDir, "pre-push"), 0o755);
|
|
70
|
+
await chmod(join(hooksDir, "pre-commit"), 0o755);
|
|
71
|
+
}
|
|
72
|
+
return "installed";
|
|
73
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve the local Alnair MCP server command for this monorepo / install.
|
|
3
|
+
*/
|
|
4
|
+
import { resolveMcpServerPath } from "./paths.js";
|
|
5
|
+
export function resolveAlnairMcpEntry(cwd) {
|
|
6
|
+
const local = resolveMcpServerPath(cwd);
|
|
7
|
+
if (local) {
|
|
8
|
+
return { command: "node", args: [local] };
|
|
9
|
+
}
|
|
10
|
+
return { command: "npx", args: ["-y", "alnair-mcp"] };
|
|
11
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code project MCP — `.mcp.json` at repo root.
|
|
3
|
+
*/
|
|
4
|
+
import { writeFile, readFile } from "node:fs/promises";
|
|
5
|
+
import { existsSync } from "node:fs";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { resolveAlnairMcpEntry } from "./mcp-resolve.js";
|
|
8
|
+
export async function installMcpConfig(cwd) {
|
|
9
|
+
const path = join(cwd, ".mcp.json");
|
|
10
|
+
const alnairEntry = resolveAlnairMcpEntry(cwd);
|
|
11
|
+
if (!existsSync(path)) {
|
|
12
|
+
await writeFile(path, JSON.stringify({ mcpServers: { alnair: alnairEntry } }, null, 2) + "\n");
|
|
13
|
+
return "created";
|
|
14
|
+
}
|
|
15
|
+
const raw = await readFile(path, "utf8");
|
|
16
|
+
let json;
|
|
17
|
+
try {
|
|
18
|
+
json = JSON.parse(raw);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
await writeFile(path, JSON.stringify({ mcpServers: { alnair: alnairEntry } }, null, 2) + "\n");
|
|
22
|
+
return "updated";
|
|
23
|
+
}
|
|
24
|
+
json.mcpServers = json.mcpServers ?? {};
|
|
25
|
+
const prev = JSON.stringify(json.mcpServers.alnair);
|
|
26
|
+
json.mcpServers.alnair = alnairEntry;
|
|
27
|
+
if (JSON.stringify(json.mcpServers.alnair) === prev)
|
|
28
|
+
return "unchanged";
|
|
29
|
+
await writeFile(path, JSON.stringify(json, null, 2) + "\n");
|
|
30
|
+
return "updated";
|
|
31
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve Alnair package paths for installed projects (npm) and monorepo dev.
|
|
3
|
+
*/
|
|
4
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
5
|
+
import { dirname, join, resolve } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
export function cliPackageVersion() {
|
|
8
|
+
try {
|
|
9
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const raw = readFileSync(join(here, "../../package.json"), "utf8");
|
|
11
|
+
return JSON.parse(raw).version ?? "0.1.0";
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return "0.1.0";
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/** Absolute path to alnair-sdk gate.js for git hooks, when present. */
|
|
18
|
+
export function resolveGatePath(cwd) {
|
|
19
|
+
for (const candidate of gateCandidates(cwd)) {
|
|
20
|
+
if (existsSync(candidate))
|
|
21
|
+
return candidate;
|
|
22
|
+
}
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
/** Expected gate location after `alnair init` installs devDependencies. */
|
|
26
|
+
export function expectedGatePath(cwd) {
|
|
27
|
+
return join(cwd, "node_modules", "alnair-sdk", "dist", "gate.js");
|
|
28
|
+
}
|
|
29
|
+
function gateCandidates(cwd) {
|
|
30
|
+
return [expectedGatePath(cwd), join(cwd, "packages", "sdk", "dist", "gate.js")];
|
|
31
|
+
}
|
|
32
|
+
/** Built hook script shipped inside the alnair CLI package. */
|
|
33
|
+
export function resolveBundledHook(name) {
|
|
34
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
35
|
+
return join(here, "../hooks", `${name}.js`);
|
|
36
|
+
}
|
|
37
|
+
export function resolveMcpServerPath(cwd) {
|
|
38
|
+
const candidates = [
|
|
39
|
+
join(cwd, "node_modules", "alnair-mcp", "dist", "server.js"),
|
|
40
|
+
join(cwd, "packages", "mcp", "dist", "server.js"),
|
|
41
|
+
];
|
|
42
|
+
try {
|
|
43
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
44
|
+
candidates.unshift(resolve(here, "../../../mcp/dist/server.js"));
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
/* ignore */
|
|
48
|
+
}
|
|
49
|
+
for (const candidate of candidates) {
|
|
50
|
+
if (existsSync(candidate))
|
|
51
|
+
return candidate;
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
package/dist/login.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { blank, copy, diamond } from "./ui.js";
|
|
2
|
+
export async function login() {
|
|
3
|
+
blank();
|
|
4
|
+
diamond("Login");
|
|
5
|
+
blank();
|
|
6
|
+
copy("Alnair Cloud login is coming soon.");
|
|
7
|
+
blank();
|
|
8
|
+
copy("For now, alnair init runs a local control plane.");
|
|
9
|
+
copy("Set ALNAIR_URL when you have a hosted endpoint.");
|
|
10
|
+
blank();
|
|
11
|
+
}
|
package/dist/logs.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { mark } from "alnair-sdk";
|
|
4
|
+
import { blank, copy, diamond, stamp } from "./ui.js";
|
|
5
|
+
import { defaultUrl, isHealthy } from "./backend.js";
|
|
6
|
+
function kindState(kind) {
|
|
7
|
+
const k = kind.toLowerCase();
|
|
8
|
+
if (k.includes("expir") || k.includes("revok"))
|
|
9
|
+
return "expired";
|
|
10
|
+
if (k.includes("use") || k.includes("spent"))
|
|
11
|
+
return "used";
|
|
12
|
+
return "issued";
|
|
13
|
+
}
|
|
14
|
+
export async function logs(opts) {
|
|
15
|
+
blank();
|
|
16
|
+
diamond("Logs");
|
|
17
|
+
blank();
|
|
18
|
+
let url = defaultUrl();
|
|
19
|
+
try {
|
|
20
|
+
const raw = await readFile(join(opts.cwd, ".alnair", "config.json"), "utf8");
|
|
21
|
+
const cfg = JSON.parse(raw);
|
|
22
|
+
if (cfg.url)
|
|
23
|
+
url = cfg.url;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
copy("No local project. Run npx alnair init");
|
|
27
|
+
blank();
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (!(await isHealthy(url))) {
|
|
31
|
+
copy("Control plane is offline.");
|
|
32
|
+
blank();
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const res = await fetch(`${url}/v1/audit`);
|
|
36
|
+
if (!res.ok) {
|
|
37
|
+
copy("Could not load activity.");
|
|
38
|
+
blank();
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const data = (await res.json());
|
|
42
|
+
const events = data.events ?? [];
|
|
43
|
+
if (events.length === 0) {
|
|
44
|
+
copy("No activity yet.");
|
|
45
|
+
blank();
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const recent = [...events].reverse().slice(0, 20);
|
|
49
|
+
for (const e of recent) {
|
|
50
|
+
const time = new Date(e.at).toLocaleTimeString();
|
|
51
|
+
const state = kindState(e.kind);
|
|
52
|
+
const perms = e.perms?.length ? ` · ${e.perms.join(", ")}` : "";
|
|
53
|
+
stamp(state, `${time} ${e.agent} ${e.task || "—"}${perms}`);
|
|
54
|
+
}
|
|
55
|
+
blank();
|
|
56
|
+
copy(`${mark("issued")} issued ${mark("used")} used ${mark("expired")} expired`);
|
|
57
|
+
blank();
|
|
58
|
+
}
|
package/dist/mint.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Power-user / debug mint. Developers should not use this day-to-day —
|
|
3
|
+
* Claude calls alnair-sdk instead. Without args we only show usage.
|
|
4
|
+
*/
|
|
5
|
+
import { AlnairClient } from "alnair-sdk";
|
|
6
|
+
import { ensureControlPlane, defaultUrl } from "./backend.js";
|
|
7
|
+
import { readFile } from "node:fs/promises";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
const USAGE = `
|
|
10
|
+
▣ Alnair
|
|
11
|
+
|
|
12
|
+
Mint a capability for an AI agent.
|
|
13
|
+
|
|
14
|
+
Usage
|
|
15
|
+
|
|
16
|
+
alnair mint --agent claude \\
|
|
17
|
+
--permission github.pull_request.merge \\
|
|
18
|
+
--task "Fix authentication bug" \\
|
|
19
|
+
--ttl 15m
|
|
20
|
+
|
|
21
|
+
Most of the time you shouldn't run this.
|
|
22
|
+
|
|
23
|
+
Install once with alnair init, then let Claude
|
|
24
|
+
call the SDK:
|
|
25
|
+
|
|
26
|
+
await alnair.mint({
|
|
27
|
+
task: "Fix authentication bug",
|
|
28
|
+
permissions: ["github.pull_request.merge"],
|
|
29
|
+
})
|
|
30
|
+
`.trim();
|
|
31
|
+
function flag(args, name) {
|
|
32
|
+
const i = args.indexOf(name);
|
|
33
|
+
if (i < 0 || i + 1 >= args.length)
|
|
34
|
+
return undefined;
|
|
35
|
+
return args[i + 1];
|
|
36
|
+
}
|
|
37
|
+
function flags(args, name) {
|
|
38
|
+
const out = [];
|
|
39
|
+
for (let i = 0; i < args.length; i++) {
|
|
40
|
+
if (args[i] === name && args[i + 1]) {
|
|
41
|
+
out.push(args[i + 1]);
|
|
42
|
+
i++;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
export async function mint(opts) {
|
|
48
|
+
const agent = flag(opts.args, "--agent");
|
|
49
|
+
const task = flag(opts.args, "--task");
|
|
50
|
+
const ttl = flag(opts.args, "--ttl") ?? "15m";
|
|
51
|
+
const permissions = flags(opts.args, "--permission");
|
|
52
|
+
if (!agent || !task || permissions.length === 0) {
|
|
53
|
+
console.log("\n" + USAGE + "\n");
|
|
54
|
+
process.exitCode = 1;
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
let url = defaultUrl();
|
|
58
|
+
try {
|
|
59
|
+
const raw = await readFile(join(opts.cwd, ".alnair", "config.json"), "utf8");
|
|
60
|
+
const cfg = JSON.parse(raw);
|
|
61
|
+
if (cfg.url)
|
|
62
|
+
url = cfg.url;
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
/* default */
|
|
66
|
+
}
|
|
67
|
+
await ensureControlPlane({ cwd: opts.cwd, url });
|
|
68
|
+
const client = new AlnairClient({ url, agent });
|
|
69
|
+
// SDK streams the real notice from these exact fields
|
|
70
|
+
await client.mint({
|
|
71
|
+
agent,
|
|
72
|
+
task,
|
|
73
|
+
ttl,
|
|
74
|
+
permissions,
|
|
75
|
+
});
|
|
76
|
+
}
|
package/dist/status.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { defaultUrl, isHealthy } from "./backend.js";
|
|
4
|
+
import { blank, check, copy, diamond, label, value } from "./ui.js";
|
|
5
|
+
export async function status(opts) {
|
|
6
|
+
blank();
|
|
7
|
+
diamond("Status");
|
|
8
|
+
blank();
|
|
9
|
+
let url = defaultUrl();
|
|
10
|
+
let agent = "—";
|
|
11
|
+
let project = "—";
|
|
12
|
+
let configured = false;
|
|
13
|
+
try {
|
|
14
|
+
const raw = await readFile(join(opts.cwd, ".alnair", "config.json"), "utf8");
|
|
15
|
+
const cfg = JSON.parse(raw);
|
|
16
|
+
configured = true;
|
|
17
|
+
if (cfg.url)
|
|
18
|
+
url = cfg.url;
|
|
19
|
+
if (cfg.agent)
|
|
20
|
+
agent = cfg.agent;
|
|
21
|
+
if (cfg.project)
|
|
22
|
+
project = cfg.project;
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
copy("No local project. Run npx alnair init");
|
|
26
|
+
blank();
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const healthy = await isHealthy(url);
|
|
30
|
+
label("Project");
|
|
31
|
+
value(` ${project}`);
|
|
32
|
+
blank();
|
|
33
|
+
label("Agent");
|
|
34
|
+
value(` ${agent}`);
|
|
35
|
+
blank();
|
|
36
|
+
label("Control plane");
|
|
37
|
+
value(` ${url}`);
|
|
38
|
+
value(` ${healthy ? "online" : "offline"}`);
|
|
39
|
+
blank();
|
|
40
|
+
if (configured && healthy) {
|
|
41
|
+
check("Ready");
|
|
42
|
+
}
|
|
43
|
+
else if (!healthy) {
|
|
44
|
+
copy("Control plane is offline. Run npx alnair init or start the service.");
|
|
45
|
+
}
|
|
46
|
+
blank();
|
|
47
|
+
}
|