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
package/README.md
ADDED
package/dist/backend.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { dirname, join, resolve } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
const DEFAULT_URL = "http://localhost:8080";
|
|
6
|
+
export function defaultUrl() {
|
|
7
|
+
return process.env.ALNAIR_URL?.replace(/\/$/, "") || DEFAULT_URL;
|
|
8
|
+
}
|
|
9
|
+
export async function isHealthy(url) {
|
|
10
|
+
try {
|
|
11
|
+
const res = await fetch(`${url}/healthz`, { signal: AbortSignal.timeout(1500) });
|
|
12
|
+
return res.ok;
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/** Walk up from cwd / package location looking for the Go service module. */
|
|
19
|
+
export function findServiceDir(cwd) {
|
|
20
|
+
if (process.env.ALNAIR_SERVICE_PATH && existsSync(process.env.ALNAIR_SERVICE_PATH)) {
|
|
21
|
+
return process.env.ALNAIR_SERVICE_PATH;
|
|
22
|
+
}
|
|
23
|
+
const candidates = [];
|
|
24
|
+
// From packages/cli/dist → ../../../service
|
|
25
|
+
try {
|
|
26
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
27
|
+
candidates.push(resolve(here, "../../../service"));
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
/* ignore */
|
|
31
|
+
}
|
|
32
|
+
// From project cwd
|
|
33
|
+
let dir = cwd;
|
|
34
|
+
for (let i = 0; i < 6; i++) {
|
|
35
|
+
candidates.push(join(dir, "service"));
|
|
36
|
+
const parent = dirname(dir);
|
|
37
|
+
if (parent === dir)
|
|
38
|
+
break;
|
|
39
|
+
dir = parent;
|
|
40
|
+
}
|
|
41
|
+
for (const c of candidates) {
|
|
42
|
+
if (existsSync(join(c, "go.mod")) && existsSync(join(c, "cmd", "server"))) {
|
|
43
|
+
return c;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
let child = null;
|
|
49
|
+
export async function ensureControlPlane(opts) {
|
|
50
|
+
const url = (opts.url ?? defaultUrl()).replace(/\/$/, "");
|
|
51
|
+
if (await isHealthy(url)) {
|
|
52
|
+
return { url, started: false };
|
|
53
|
+
}
|
|
54
|
+
const serviceDir = findServiceDir(opts.cwd);
|
|
55
|
+
if (!serviceDir) {
|
|
56
|
+
throw new Error(`Alnair control plane is not running at ${url}.\n` +
|
|
57
|
+
` Start it with: cd service && go run ./cmd/server\n` +
|
|
58
|
+
` Or set ALNAIR_URL to a hosted endpoint.`);
|
|
59
|
+
}
|
|
60
|
+
child = spawn("go", ["run", "./cmd/server", "-addr", ":8080", "-seed=false"], {
|
|
61
|
+
cwd: serviceDir,
|
|
62
|
+
stdio: "ignore",
|
|
63
|
+
detached: true,
|
|
64
|
+
env: { ...process.env },
|
|
65
|
+
});
|
|
66
|
+
child.unref();
|
|
67
|
+
const deadline = Date.now() + 20_000;
|
|
68
|
+
while (Date.now() < deadline) {
|
|
69
|
+
if (await isHealthy(url)) {
|
|
70
|
+
return { url, started: true };
|
|
71
|
+
}
|
|
72
|
+
await new Promise((r) => setTimeout(r, 400));
|
|
73
|
+
}
|
|
74
|
+
throw new Error(`Started the control plane but it did not become healthy at ${url} within 20s.`);
|
|
75
|
+
}
|
|
76
|
+
export function stopControlPlane() {
|
|
77
|
+
if (child?.pid) {
|
|
78
|
+
try {
|
|
79
|
+
process.kill(-child.pid, "SIGTERM");
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
/* already gone */
|
|
83
|
+
}
|
|
84
|
+
child = null;
|
|
85
|
+
}
|
|
86
|
+
}
|
package/dist/bin.js
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { mark } from "alnair-sdk";
|
|
2
|
+
import pc from "picocolors";
|
|
3
|
+
import { init } from "./init.js";
|
|
4
|
+
const HELP = `
|
|
5
|
+
${mark("brand")} ${pc.bold("Alnair")}
|
|
6
|
+
|
|
7
|
+
Install once. Then forget it exists.
|
|
8
|
+
Your agents call the SDK — you don't mint by hand.
|
|
9
|
+
|
|
10
|
+
${pc.dim("Usage")}
|
|
11
|
+
|
|
12
|
+
npx alnair init Set up this project
|
|
13
|
+
npx alnair status What's connected
|
|
14
|
+
npx alnair login Connect to Alnair Cloud
|
|
15
|
+
alnair logs Recent capability activity
|
|
16
|
+
alnair inspect Inspect a capability token
|
|
17
|
+
|
|
18
|
+
${pc.dim("https://alnair.ai/docs")}
|
|
19
|
+
`.trim();
|
|
20
|
+
export async function main(args) {
|
|
21
|
+
const cmd = args[0] ?? "help";
|
|
22
|
+
if (cmd === "-h" || cmd === "--help" || cmd === "help") {
|
|
23
|
+
console.log("\n" + HELP + "\n");
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
if (cmd === "init") {
|
|
27
|
+
await init({ cwd: process.cwd() });
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (cmd === "status") {
|
|
31
|
+
const { status } = await import("./status.js");
|
|
32
|
+
await status({ cwd: process.cwd() });
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
if (cmd === "login") {
|
|
36
|
+
const { login } = await import("./login.js");
|
|
37
|
+
await login();
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (cmd === "logs") {
|
|
41
|
+
const { logs } = await import("./logs.js");
|
|
42
|
+
await logs({ cwd: process.cwd() });
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (cmd === "inspect") {
|
|
46
|
+
const { inspect } = await import("./inspect.js");
|
|
47
|
+
await inspect({ cwd: process.cwd(), token: args[1] });
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (cmd === "mint") {
|
|
51
|
+
const { mint } = await import("./mint.js");
|
|
52
|
+
await mint({ cwd: process.cwd(), args: args.slice(1) });
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
console.error(`\n Unknown command: ${cmd}\n`);
|
|
56
|
+
console.log(HELP + "\n");
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
package/dist/detect.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { execFile } from "node:child_process";
|
|
6
|
+
import { promisify } from "node:util";
|
|
7
|
+
const execFileAsync = promisify(execFile);
|
|
8
|
+
async function fileContains(path, needle) {
|
|
9
|
+
try {
|
|
10
|
+
const raw = await readFile(path, "utf8");
|
|
11
|
+
return raw.includes(needle);
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
async function hasGhAuth() {
|
|
18
|
+
try {
|
|
19
|
+
await execFileAsync("gh", ["auth", "status"], { timeout: 3000 });
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
async function gitRemoteIsGitHub(cwd) {
|
|
27
|
+
try {
|
|
28
|
+
const { stdout } = await execFileAsync("git", ["remote", "-v"], {
|
|
29
|
+
cwd,
|
|
30
|
+
timeout: 3000,
|
|
31
|
+
});
|
|
32
|
+
return stdout.includes("github.com");
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function isGitRepo(cwd) {
|
|
39
|
+
return existsSync(join(cwd, ".git"));
|
|
40
|
+
}
|
|
41
|
+
export async function detect(cwd) {
|
|
42
|
+
const home = homedir();
|
|
43
|
+
const claudeCode = existsSync(join(home, ".claude")) ||
|
|
44
|
+
existsSync(join(cwd, "CLAUDE.md")) ||
|
|
45
|
+
existsSync(join(cwd, ".claude")) ||
|
|
46
|
+
Boolean(process.env.CLAUDE_CODE || process.env.CLAUDECODE);
|
|
47
|
+
const cursor = existsSync(join(cwd, ".cursor")) ||
|
|
48
|
+
existsSync(join(home, ".cursor")) ||
|
|
49
|
+
Boolean(process.env.CURSOR_TRACE_ID);
|
|
50
|
+
const codex = existsSync(join(home, ".codex")) ||
|
|
51
|
+
existsSync(join(cwd, ".codex")) ||
|
|
52
|
+
Boolean(process.env.CODEX_HOME) ||
|
|
53
|
+
(await fileContains(join(cwd, "package.json"), "codex"));
|
|
54
|
+
const cline = existsSync(join(home, ".cline")) ||
|
|
55
|
+
existsSync(join(cwd, ".clinerules")) ||
|
|
56
|
+
existsSync(join(home, "Documents", "Cline")) ||
|
|
57
|
+
Boolean(process.env.CLINE);
|
|
58
|
+
const roo = existsSync(join(cwd, ".roo")) ||
|
|
59
|
+
existsSync(join(home, ".roo")) ||
|
|
60
|
+
existsSync(join(cwd, ".roomodes"));
|
|
61
|
+
const conductor = Boolean(process.env.CONDUCTOR || process.env.CONDUCTOR_SESSION) ||
|
|
62
|
+
existsSync(join(home, ".conductor")) ||
|
|
63
|
+
existsSync(join(cwd, ".conductor"));
|
|
64
|
+
const git = isGitRepo(cwd);
|
|
65
|
+
let github = false;
|
|
66
|
+
if (process.env.GITHUB_TOKEN || process.env.GH_TOKEN)
|
|
67
|
+
github = true;
|
|
68
|
+
else if (await hasGhAuth())
|
|
69
|
+
github = true;
|
|
70
|
+
else if (await gitRemoteIsGitHub(cwd))
|
|
71
|
+
github = true;
|
|
72
|
+
const supabase = existsSync(join(cwd, "supabase")) ||
|
|
73
|
+
Boolean(process.env.SUPABASE_URL || process.env.NEXT_PUBLIC_SUPABASE_URL) ||
|
|
74
|
+
(await fileContains(join(cwd, ".env"), "SUPABASE")) ||
|
|
75
|
+
(await fileContains(join(cwd, ".env.local"), "SUPABASE"));
|
|
76
|
+
let agentName = "claude-code";
|
|
77
|
+
let agentRuntime = "Claude Code";
|
|
78
|
+
if (claudeCode) {
|
|
79
|
+
agentName = "claude-code";
|
|
80
|
+
agentRuntime = "Claude Code";
|
|
81
|
+
}
|
|
82
|
+
else if (cursor) {
|
|
83
|
+
agentName = "cursor-agent";
|
|
84
|
+
agentRuntime = "Cursor";
|
|
85
|
+
}
|
|
86
|
+
else if (codex) {
|
|
87
|
+
agentName = "codex";
|
|
88
|
+
agentRuntime = "Codex";
|
|
89
|
+
}
|
|
90
|
+
else if (conductor) {
|
|
91
|
+
agentName = "conductor";
|
|
92
|
+
agentRuntime = "Conductor";
|
|
93
|
+
}
|
|
94
|
+
else if (cline) {
|
|
95
|
+
agentName = "cline";
|
|
96
|
+
agentRuntime = "Cline";
|
|
97
|
+
}
|
|
98
|
+
else if (roo) {
|
|
99
|
+
agentName = "roo";
|
|
100
|
+
agentRuntime = "Roo";
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
claudeCode,
|
|
104
|
+
cursor,
|
|
105
|
+
codex,
|
|
106
|
+
cline,
|
|
107
|
+
roo,
|
|
108
|
+
conductor,
|
|
109
|
+
git,
|
|
110
|
+
github,
|
|
111
|
+
supabase,
|
|
112
|
+
agentName,
|
|
113
|
+
agentRuntime,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code PostToolUse — one-line grant notice for the developer.
|
|
3
|
+
* ALNAIR_QUIET=1 to hide.
|
|
4
|
+
*/
|
|
5
|
+
import { formatMintNotice, shouldNotify } from "alnair-sdk";
|
|
6
|
+
async function readStdin() {
|
|
7
|
+
const chunks = [];
|
|
8
|
+
for await (const chunk of process.stdin) {
|
|
9
|
+
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
10
|
+
}
|
|
11
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
12
|
+
}
|
|
13
|
+
async function main() {
|
|
14
|
+
let input = {};
|
|
15
|
+
try {
|
|
16
|
+
const raw = await readStdin();
|
|
17
|
+
if (raw.trim())
|
|
18
|
+
input = JSON.parse(raw);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
process.exit(0);
|
|
22
|
+
}
|
|
23
|
+
const tool = input.tool_input ?? {};
|
|
24
|
+
const permissions = tool.permissions ?? [];
|
|
25
|
+
const out = {
|
|
26
|
+
suppressOutput: true,
|
|
27
|
+
hookSpecificOutput: {
|
|
28
|
+
hookEventName: "PostToolUse",
|
|
29
|
+
updatedToolOutput: {
|
|
30
|
+
content: [{ type: "text", text: JSON.stringify({ allowed: true }) }],
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
if (shouldNotify() && permissions.length > 0) {
|
|
35
|
+
out.systemMessage = formatMintNotice({
|
|
36
|
+
agent: tool.agent ?? "claude-code",
|
|
37
|
+
task: tool.task,
|
|
38
|
+
permissions,
|
|
39
|
+
ttl: tool.ttl,
|
|
40
|
+
ms: input.duration_ms ?? 0,
|
|
41
|
+
}).trimEnd();
|
|
42
|
+
}
|
|
43
|
+
process.stdout.write(JSON.stringify(out));
|
|
44
|
+
}
|
|
45
|
+
main().catch(() => process.exit(0));
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Cursor afterShellExecution / postToolUse — surface Alnair grants in the chat.
|
|
4
|
+
*/
|
|
5
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
async function readStdin() {
|
|
8
|
+
const chunks = [];
|
|
9
|
+
for await (const c of process.stdin) {
|
|
10
|
+
chunks.push(typeof c === "string" ? Buffer.from(c) : c);
|
|
11
|
+
}
|
|
12
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
13
|
+
}
|
|
14
|
+
function extractAlnairLine(text) {
|
|
15
|
+
if (!text)
|
|
16
|
+
return null;
|
|
17
|
+
const m = text.match(/\[alnair\][^\n]*/);
|
|
18
|
+
return m ? m[0].trim() : null;
|
|
19
|
+
}
|
|
20
|
+
function looksLikeGitGrant(command, output) {
|
|
21
|
+
const cmd = command.toLowerCase();
|
|
22
|
+
const isGit = /\bgit\b/.test(cmd) &&
|
|
23
|
+
(/\bcommit\b/.test(cmd) || /\bpush\b/.test(cmd) || /\bmerge\b/.test(cmd));
|
|
24
|
+
if (!isGit)
|
|
25
|
+
return null;
|
|
26
|
+
return (extractAlnairLine(output) ||
|
|
27
|
+
(/\bpush\b/.test(cmd) ? "[alnair] GitHub access granted" : "[alnair] Git access granted"));
|
|
28
|
+
}
|
|
29
|
+
function sessionHint(cwd) {
|
|
30
|
+
try {
|
|
31
|
+
const p = join(cwd || process.cwd(), ".alnair", "session.json");
|
|
32
|
+
if (!existsSync(p))
|
|
33
|
+
return null;
|
|
34
|
+
const s = JSON.parse(readFileSync(p, "utf8"));
|
|
35
|
+
if (!s.permissions?.length)
|
|
36
|
+
return null;
|
|
37
|
+
if (s.permissions.some((x) => String(x).includes("push"))) {
|
|
38
|
+
return "[alnair] GitHub access granted";
|
|
39
|
+
}
|
|
40
|
+
if (s.permissions.some((x) => String(x).includes("commit"))) {
|
|
41
|
+
return "[alnair] Git access granted";
|
|
42
|
+
}
|
|
43
|
+
return "[alnair] Access granted";
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const raw = await readStdin();
|
|
50
|
+
let input = {};
|
|
51
|
+
try {
|
|
52
|
+
input = raw.trim() ? JSON.parse(raw) : {};
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
process.exit(0);
|
|
56
|
+
}
|
|
57
|
+
const command = input.command || input.tool_input?.command || "";
|
|
58
|
+
let output = input.output || "";
|
|
59
|
+
if (!output && typeof input.tool_output === "string") {
|
|
60
|
+
try {
|
|
61
|
+
const parsed = JSON.parse(input.tool_output);
|
|
62
|
+
output = parsed.stdout || parsed.output || input.tool_output;
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
output = input.tool_output;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const line = looksLikeGitGrant(command, output) ||
|
|
69
|
+
extractAlnairLine(output) ||
|
|
70
|
+
(/\bgit\b/.test(String(command).toLowerCase()) ? sessionHint(input.cwd || process.cwd()) : null);
|
|
71
|
+
if (!line) {
|
|
72
|
+
process.exit(0);
|
|
73
|
+
}
|
|
74
|
+
process.stdout.write(JSON.stringify({
|
|
75
|
+
user_message: line,
|
|
76
|
+
additional_context: `Alnair granted access. Include this exact line in your reply to the user:\n${line}`,
|
|
77
|
+
}));
|
package/dist/init.js
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { mkdir, writeFile, readFile } from "node:fs/promises";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { AlnairClient } from "alnair-sdk";
|
|
5
|
+
import { detect } from "./detect.js";
|
|
6
|
+
import { ensureControlPlane } from "./backend.js";
|
|
7
|
+
import { installClaudeIntegration } from "./integrations/claude.js";
|
|
8
|
+
import { installClaudeCodeHooks } from "./integrations/claude-hooks.js";
|
|
9
|
+
import { installCursorIntegration } from "./integrations/cursor.js";
|
|
10
|
+
import { installCodexIntegration } from "./integrations/codex.js";
|
|
11
|
+
import { installAgentsRuntimeBlock } from "./integrations/agents-md.js";
|
|
12
|
+
import { ensureProjectDeps } from "./integrations/bootstrap.js";
|
|
13
|
+
import { installGitHooks } from "./integrations/hooks.js";
|
|
14
|
+
import { installMcpConfig } from "./integrations/mcp.js";
|
|
15
|
+
import { blank, check, copy, diamond, sleep, workingKeep } from "./ui.js";
|
|
16
|
+
export async function init(opts) {
|
|
17
|
+
const { cwd } = opts;
|
|
18
|
+
blank();
|
|
19
|
+
diamond("Setting up Alnair...");
|
|
20
|
+
blank();
|
|
21
|
+
await sleep(200);
|
|
22
|
+
const found = await detect(cwd);
|
|
23
|
+
let url = process.env.ALNAIR_URL ?? "http://localhost:8080";
|
|
24
|
+
// —— Detections (only what we found) ————————————————————————————————
|
|
25
|
+
if (found.claudeCode)
|
|
26
|
+
check("Claude Code detected");
|
|
27
|
+
if (found.cursor)
|
|
28
|
+
check("Cursor detected");
|
|
29
|
+
if (found.codex)
|
|
30
|
+
check("Codex detected");
|
|
31
|
+
if (found.conductor)
|
|
32
|
+
check("Conductor detected");
|
|
33
|
+
if (found.cline)
|
|
34
|
+
check("Cline detected");
|
|
35
|
+
if (found.roo)
|
|
36
|
+
check("Roo detected");
|
|
37
|
+
if (!found.claudeCode &&
|
|
38
|
+
!found.cursor &&
|
|
39
|
+
!found.codex &&
|
|
40
|
+
!found.conductor &&
|
|
41
|
+
!found.cline &&
|
|
42
|
+
!found.roo) {
|
|
43
|
+
check("Local project ready");
|
|
44
|
+
}
|
|
45
|
+
blank();
|
|
46
|
+
// —— Workspace ——————————————————————————————————————————————————————
|
|
47
|
+
await workingKeep("Creating your workspace...", async () => {
|
|
48
|
+
const plane = await ensureControlPlane({ cwd, url });
|
|
49
|
+
url = plane.url;
|
|
50
|
+
const alnairDir = join(cwd, ".alnair");
|
|
51
|
+
await mkdir(alnairDir, { recursive: true });
|
|
52
|
+
const config = {
|
|
53
|
+
url,
|
|
54
|
+
agent: found.agentName,
|
|
55
|
+
project: inferProjectName(cwd),
|
|
56
|
+
detected: {
|
|
57
|
+
claudeCode: found.claudeCode,
|
|
58
|
+
cursor: found.cursor,
|
|
59
|
+
codex: found.codex,
|
|
60
|
+
cline: found.cline,
|
|
61
|
+
roo: found.roo,
|
|
62
|
+
conductor: found.conductor,
|
|
63
|
+
git: found.git,
|
|
64
|
+
github: found.github,
|
|
65
|
+
},
|
|
66
|
+
createdAt: new Date().toISOString(),
|
|
67
|
+
};
|
|
68
|
+
await writeFile(join(alnairDir, "config.json"), JSON.stringify(config, null, 2) + "\n");
|
|
69
|
+
const gitignore = join(cwd, ".gitignore");
|
|
70
|
+
if (existsSync(gitignore)) {
|
|
71
|
+
const raw = await readFile(gitignore, "utf8");
|
|
72
|
+
if (!raw.includes(".alnair")) {
|
|
73
|
+
await writeFile(gitignore, raw.replace(/\s*$/, "\n") + "\n# Alnair\n.alnair/\n");
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
await writeFile(gitignore, "# Alnair\n.alnair/\n");
|
|
78
|
+
}
|
|
79
|
+
const client = new AlnairClient({ url, agent: found.agentName });
|
|
80
|
+
try {
|
|
81
|
+
await client.register(found.agentName, found.agentRuntime);
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
/* ok */
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
// —— Project dependencies ———————————————————————————————————————————
|
|
88
|
+
try {
|
|
89
|
+
const deps = await ensureProjectDeps(cwd);
|
|
90
|
+
if (deps === "installed")
|
|
91
|
+
check("Installed Alnair packages");
|
|
92
|
+
else if (deps === "updated")
|
|
93
|
+
check("Updated Alnair packages");
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
copy("Skipped Alnair packages (run npm install -D alnair-sdk alnair-mcp)");
|
|
97
|
+
}
|
|
98
|
+
// —— Agent integrations —————————————————————————————————————————————
|
|
99
|
+
// Universal layer: git hooks (any host that runs real git).
|
|
100
|
+
// Per-host: MCP + instruction files so each agent discovers Alnair.
|
|
101
|
+
if (found.claudeCode) {
|
|
102
|
+
const result = await installClaudeIntegration(cwd);
|
|
103
|
+
if (result.status === "created")
|
|
104
|
+
check("Created CLAUDE.md");
|
|
105
|
+
else if (result.status === "updated")
|
|
106
|
+
check("Updated CLAUDE.md");
|
|
107
|
+
else
|
|
108
|
+
check("Installed Claude integration");
|
|
109
|
+
const mcp = await installMcpConfig(cwd);
|
|
110
|
+
if (mcp !== "unchanged")
|
|
111
|
+
check("Installed Claude MCP");
|
|
112
|
+
await installClaudeCodeHooks(cwd);
|
|
113
|
+
}
|
|
114
|
+
if (found.cursor) {
|
|
115
|
+
try {
|
|
116
|
+
await installCursorIntegration(cwd);
|
|
117
|
+
check("Installed Cursor MCP + rules");
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
copy("Skipped Cursor integration (could not write .cursor/)");
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (found.codex || found.conductor) {
|
|
124
|
+
const codex = await installCodexIntegration(cwd);
|
|
125
|
+
if (codex !== "unchanged")
|
|
126
|
+
check("Installed Codex MCP");
|
|
127
|
+
}
|
|
128
|
+
// Codex / Conductor / multi-agent hosts read AGENTS.md
|
|
129
|
+
if (found.codex || found.conductor || found.cline || found.roo || found.cursor) {
|
|
130
|
+
const agents = await installAgentsRuntimeBlock(cwd);
|
|
131
|
+
if (agents === "created")
|
|
132
|
+
check("Created AGENTS.md runtime block");
|
|
133
|
+
else if (agents === "updated")
|
|
134
|
+
check("Updated AGENTS.md runtime block");
|
|
135
|
+
}
|
|
136
|
+
if ((found.codex || found.cline || found.roo) && !found.claudeCode) {
|
|
137
|
+
const result = await installClaudeIntegration(cwd);
|
|
138
|
+
if (result.status === "created")
|
|
139
|
+
check("Created agent instructions");
|
|
140
|
+
else if (result.status === "updated")
|
|
141
|
+
check("Updated agent instructions");
|
|
142
|
+
}
|
|
143
|
+
// —— Git ————————————————————————————————————————————————————————————
|
|
144
|
+
if (found.git) {
|
|
145
|
+
check("Git repository detected");
|
|
146
|
+
try {
|
|
147
|
+
const hooks = await installGitHooks(cwd);
|
|
148
|
+
if (hooks === "installed")
|
|
149
|
+
check("Installed Git hooks");
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
copy("Skipped Git hooks (could not configure)");
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (found.github) {
|
|
156
|
+
check("Connected GitHub");
|
|
157
|
+
}
|
|
158
|
+
blank();
|
|
159
|
+
check("Ready");
|
|
160
|
+
blank();
|
|
161
|
+
blank();
|
|
162
|
+
copy("Access is granted automatically.");
|
|
163
|
+
if (found.cursor) {
|
|
164
|
+
copy("Reload Cursor to pick up Alnair chat hooks.");
|
|
165
|
+
}
|
|
166
|
+
blank();
|
|
167
|
+
copy("https://alnair.ai/docs");
|
|
168
|
+
blank();
|
|
169
|
+
}
|
|
170
|
+
function inferProjectName(cwd) {
|
|
171
|
+
return cwd.split(/[/\\]/).filter(Boolean).pop() || "project";
|
|
172
|
+
}
|
package/dist/inspect.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { blank, copy, diamond, label, nest, stamp, value } from "./ui.js";
|
|
2
|
+
import { defaultUrl, isHealthy } from "./backend.js";
|
|
3
|
+
import { readFile } from "node:fs/promises";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { displayPermission, prettyAgent } from "alnair-sdk";
|
|
6
|
+
export async function inspect(opts) {
|
|
7
|
+
blank();
|
|
8
|
+
diamond("Inspect");
|
|
9
|
+
blank();
|
|
10
|
+
if (!opts.token) {
|
|
11
|
+
copy("Usage");
|
|
12
|
+
blank();
|
|
13
|
+
value(" alnair inspect <token>");
|
|
14
|
+
blank();
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
let url = defaultUrl();
|
|
18
|
+
try {
|
|
19
|
+
const raw = await readFile(join(opts.cwd, ".alnair", "config.json"), "utf8");
|
|
20
|
+
const cfg = JSON.parse(raw);
|
|
21
|
+
if (cfg.url)
|
|
22
|
+
url = cfg.url;
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
/* default */
|
|
26
|
+
}
|
|
27
|
+
if (!(await isHealthy(url))) {
|
|
28
|
+
copy("Control plane is offline.");
|
|
29
|
+
blank();
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const res = await fetch(`${url}/v1/capabilities/verify`, {
|
|
33
|
+
method: "POST",
|
|
34
|
+
headers: { "Content-Type": "application/json" },
|
|
35
|
+
body: JSON.stringify({ token: opts.token }),
|
|
36
|
+
});
|
|
37
|
+
const data = (await res.json());
|
|
38
|
+
if (!res.ok) {
|
|
39
|
+
stamp("expired", data.error ?? "Invalid permission");
|
|
40
|
+
blank();
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const expired = data.not_after != null && new Date(data.not_after).getTime() < Date.now();
|
|
44
|
+
stamp(expired ? "expired" : "issued", expired ? "Expired" : "Active");
|
|
45
|
+
blank();
|
|
46
|
+
label("Agent");
|
|
47
|
+
value(` ${prettyAgent(data.agent ?? "")}`);
|
|
48
|
+
blank();
|
|
49
|
+
label("Task");
|
|
50
|
+
value(` ${data.task || "—"}`);
|
|
51
|
+
blank();
|
|
52
|
+
label("Access");
|
|
53
|
+
for (const p of data.permissions ?? []) {
|
|
54
|
+
const { group, label: actionLabel } = displayPermission(p.includes(":") ? p.replace(":", ".") : p);
|
|
55
|
+
value(` ${group}`);
|
|
56
|
+
nest(actionLabel);
|
|
57
|
+
}
|
|
58
|
+
blank();
|
|
59
|
+
label("Expires");
|
|
60
|
+
value(` ${data.not_after ? new Date(data.not_after).toLocaleString() : "—"}`);
|
|
61
|
+
blank();
|
|
62
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared runtime instructions for Codex / Conductor / any AGENTS.md reader.
|
|
3
|
+
* Appends a managed block — does not clobber existing constitution content.
|
|
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 RUNTIME -->";
|
|
9
|
+
export const END = "<!-- END ALNAIR RUNTIME -->";
|
|
10
|
+
export const RUNTIME_BLOCK = `${BEGIN}
|
|
11
|
+
## Permissions (Alnair)
|
|
12
|
+
|
|
13
|
+
Short-lived access is granted automatically on git commit/push (and similar
|
|
14
|
+
privileged actions). Just do the work.
|
|
15
|
+
|
|
16
|
+
Expect a one-line terminal notice when access is granted:
|
|
17
|
+
▣ GitHub access granted
|
|
18
|
+
|
|
19
|
+
Do not ask the user about permissions. Do not call mint tools unless blocked.
|
|
20
|
+
${END}`;
|
|
21
|
+
export async function installAgentsRuntimeBlock(cwd) {
|
|
22
|
+
const path = join(cwd, "AGENTS.md");
|
|
23
|
+
if (!existsSync(path)) {
|
|
24
|
+
await writeFile(path, RUNTIME_BLOCK + "\n", "utf8");
|
|
25
|
+
return "created";
|
|
26
|
+
}
|
|
27
|
+
const raw = await readFile(path, "utf8");
|
|
28
|
+
if (raw.includes(BEGIN) && raw.includes(END)) {
|
|
29
|
+
const next = raw.replace(new RegExp(`${escapeRegExp(BEGIN)}[\\s\\S]*?${escapeRegExp(END)}`), RUNTIME_BLOCK);
|
|
30
|
+
if (next === raw)
|
|
31
|
+
return "unchanged";
|
|
32
|
+
await writeFile(path, next, "utf8");
|
|
33
|
+
return "updated";
|
|
34
|
+
}
|
|
35
|
+
const sep = raw.endsWith("\n") ? "\n" : "\n\n";
|
|
36
|
+
await writeFile(path, raw + sep + RUNTIME_BLOCK + "\n", "utf8");
|
|
37
|
+
return "updated";
|
|
38
|
+
}
|
|
39
|
+
function escapeRegExp(s) {
|
|
40
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
41
|
+
}
|