alnair-sdk 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/agent.d.ts +6 -0
- package/dist/agent.js +40 -0
- package/dist/gate.d.ts +1 -0
- package/dist/gate.js +115 -0
- package/dist/index.d.ts +80 -0
- package/dist/index.js +233 -0
- package/dist/mark.d.ts +22 -0
- package/dist/mark.js +50 -0
- package/dist/notify.d.ts +30 -0
- package/dist/notify.js +118 -0
- package/package.json +41 -0
package/README.md
ADDED
package/dist/agent.d.ts
ADDED
package/dist/agent.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detect which agent host is running this process.
|
|
3
|
+
* Used at mint time so Cursor/Codex/Claude grants attribute correctly
|
|
4
|
+
* (init-time config alone is wrong when multiple hosts share a repo).
|
|
5
|
+
*/
|
|
6
|
+
export function detectRunningAgent() {
|
|
7
|
+
const env = process.env;
|
|
8
|
+
if (env.ALNAIR_AGENT?.trim())
|
|
9
|
+
return env.ALNAIR_AGENT.trim();
|
|
10
|
+
// Cursor
|
|
11
|
+
if (env.CURSOR_TRACE_ID ||
|
|
12
|
+
env.CURSOR_AGENT ||
|
|
13
|
+
env.CURSOR_SESSION_ID ||
|
|
14
|
+
env.COMPOSER_SESSION ||
|
|
15
|
+
(env.TERM_PROGRAM === "vscode" && env.VSCODE_PID && env.CURSOR_AGENT !== undefined)) {
|
|
16
|
+
return "cursor-agent";
|
|
17
|
+
}
|
|
18
|
+
// Heuristic: Cursor injects these often
|
|
19
|
+
if (env.VSCODE_GIT_ASKPASS_MAIN?.includes("cursor") || env.CURSOR_EXTENSION_HOST) {
|
|
20
|
+
return "cursor-agent";
|
|
21
|
+
}
|
|
22
|
+
// Claude Code
|
|
23
|
+
if (env.CLAUDECODE || env.CLAUDE_CODE || env.CLAUDE_CODE_ENTRYPOINT) {
|
|
24
|
+
return "claude-code";
|
|
25
|
+
}
|
|
26
|
+
// Codex
|
|
27
|
+
if (env.CODEX_HOME || env.CODEX_SANDBOX || env.CODEX_CI || env.OPENAI_CODEX) {
|
|
28
|
+
return "codex";
|
|
29
|
+
}
|
|
30
|
+
// Conductor (hosts Claude/Cursor/Codex — prefer nested host signals first)
|
|
31
|
+
if (env.CONDUCTOR || env.CONDUCTOR_SESSION || env.CONDUCTOR_WORKSPACE) {
|
|
32
|
+
return "conductor";
|
|
33
|
+
}
|
|
34
|
+
// Cline / Roo
|
|
35
|
+
if (env.CLINE || env.CLINE_DIR)
|
|
36
|
+
return "cline";
|
|
37
|
+
if (env.ROO || env.ROO_DIR)
|
|
38
|
+
return "roo";
|
|
39
|
+
return "";
|
|
40
|
+
}
|
package/dist/gate.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/gate.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gate used by git hooks.
|
|
3
|
+
*
|
|
4
|
+
* Bash hooks echo `[alnair] … access granted` to stdout (Cursor tool capture).
|
|
5
|
+
* Gate still mints; set ALNAIR_HOOK_ECHO=0 to have gate print instead.
|
|
6
|
+
*/
|
|
7
|
+
import { readFile } from "node:fs/promises";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { execFile } from "node:child_process";
|
|
10
|
+
import { promisify } from "node:util";
|
|
11
|
+
const execFileAsync = promisify(execFile);
|
|
12
|
+
function covers(granted, want) {
|
|
13
|
+
const w = want.includes(":")
|
|
14
|
+
? want
|
|
15
|
+
: want.replace(/^([^.]+)\./, "$1:");
|
|
16
|
+
const alt = want.includes(".")
|
|
17
|
+
? `${want.split(".")[0]}:${want.split(".").slice(1).join(".")}`
|
|
18
|
+
: want;
|
|
19
|
+
for (const p of granted) {
|
|
20
|
+
if (p === w || p === alt || p === want)
|
|
21
|
+
return true;
|
|
22
|
+
if (p.endsWith(".*")) {
|
|
23
|
+
const prefix = p.slice(0, -1);
|
|
24
|
+
if (w.startsWith(prefix) || alt.startsWith(prefix))
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
async function sessionCovers(want) {
|
|
31
|
+
try {
|
|
32
|
+
const raw = await readFile(join(process.cwd(), ".alnair", "session.json"), "utf8");
|
|
33
|
+
const session = JSON.parse(raw);
|
|
34
|
+
if (!session.expiresAt || new Date(session.expiresAt).getTime() < Date.now()) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
return covers(session.permissions ?? [], want);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
async function inferTask(want) {
|
|
44
|
+
try {
|
|
45
|
+
if (want.includes("commit")) {
|
|
46
|
+
const raw = await readFile(join(process.cwd(), ".git", "COMMIT_EDITMSG"), "utf8");
|
|
47
|
+
const subject = raw.split("\n").find((l) => l.trim() && !l.startsWith("#"));
|
|
48
|
+
if (subject?.trim())
|
|
49
|
+
return subject.trim();
|
|
50
|
+
}
|
|
51
|
+
const { stdout } = await execFileAsync("git", ["log", "-1", "--pretty=%s"], {
|
|
52
|
+
cwd: process.cwd(),
|
|
53
|
+
});
|
|
54
|
+
if (stdout.trim())
|
|
55
|
+
return stdout.trim();
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
/* fall through */
|
|
59
|
+
}
|
|
60
|
+
if (want.includes("push"))
|
|
61
|
+
return "git push";
|
|
62
|
+
if (want.includes("commit"))
|
|
63
|
+
return "git commit";
|
|
64
|
+
return want;
|
|
65
|
+
}
|
|
66
|
+
async function announce(want, agent, ms) {
|
|
67
|
+
// Default: bash hook echoes the line. Gate prints only when asked.
|
|
68
|
+
if (process.env.ALNAIR_HOOK_ECHO === "0") {
|
|
69
|
+
const { streamMintNotice } = await import("./notify.js");
|
|
70
|
+
streamMintNotice({
|
|
71
|
+
agent: agent || "agent",
|
|
72
|
+
permissions: [want],
|
|
73
|
+
ms,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async function autoGrant(want) {
|
|
78
|
+
const started = Date.now();
|
|
79
|
+
try {
|
|
80
|
+
const { AlnairClient, detectRunningAgent } = await import("./index.js");
|
|
81
|
+
const runtime = detectRunningAgent();
|
|
82
|
+
const client = await AlnairClient.fromProject(runtime ? { agent: runtime } : {});
|
|
83
|
+
const task = process.env.ALNAIR_TASK?.trim() || (await inferTask(want));
|
|
84
|
+
await client.mint({
|
|
85
|
+
task,
|
|
86
|
+
permissions: [want],
|
|
87
|
+
ttl: process.env.ALNAIR_TTL ?? "15m",
|
|
88
|
+
agent: runtime || undefined,
|
|
89
|
+
notify: false,
|
|
90
|
+
});
|
|
91
|
+
return { ok: true, agent: runtime || "agent", ms: Date.now() - started };
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return { ok: false, agent: "", ms: 0 };
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
async function main() {
|
|
98
|
+
const want = process.argv[2];
|
|
99
|
+
if (!want) {
|
|
100
|
+
process.exit(2);
|
|
101
|
+
}
|
|
102
|
+
const { detectRunningAgent } = await import("./agent.js");
|
|
103
|
+
const agent = detectRunningAgent() || "agent";
|
|
104
|
+
if (await sessionCovers(want)) {
|
|
105
|
+
await announce(want, agent, 1);
|
|
106
|
+
process.exit(0);
|
|
107
|
+
}
|
|
108
|
+
const granted = await autoGrant(want);
|
|
109
|
+
if (granted.ok) {
|
|
110
|
+
await announce(want, granted.agent || agent, granted.ms);
|
|
111
|
+
process.exit(0);
|
|
112
|
+
}
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
115
|
+
main();
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* alnair-sdk — mint and verify short-lived agent capabilities.
|
|
3
|
+
*
|
|
4
|
+
* Claude / Cursor call this. Developers run `npx alnair init` once, then forget it.
|
|
5
|
+
*
|
|
6
|
+
* const token = await alnair.mint({
|
|
7
|
+
* task: "Fix authentication bug",
|
|
8
|
+
* permissions: ["github.pull_request.merge"],
|
|
9
|
+
* ttl: "15m",
|
|
10
|
+
* })
|
|
11
|
+
*/
|
|
12
|
+
export type MintRequest = {
|
|
13
|
+
/** What the agent is doing — required for a real mint. */
|
|
14
|
+
task?: string;
|
|
15
|
+
ttl?: string;
|
|
16
|
+
agent?: string;
|
|
17
|
+
/** Preferred: ["github.pull_request.merge"] */
|
|
18
|
+
permissions?: string[];
|
|
19
|
+
/** Aliases — also accepted */
|
|
20
|
+
github?: string[];
|
|
21
|
+
supabase?: string[];
|
|
22
|
+
postgres?: string[];
|
|
23
|
+
vercel?: string[];
|
|
24
|
+
audience?: string[];
|
|
25
|
+
/** Set false to silence the one-line notice. Default: on (ALNAIR_QUIET=1 to hide). */
|
|
26
|
+
notify?: boolean;
|
|
27
|
+
};
|
|
28
|
+
export type AlnairConfig = {
|
|
29
|
+
url?: string;
|
|
30
|
+
agent?: string;
|
|
31
|
+
};
|
|
32
|
+
export type Capability = {
|
|
33
|
+
can: (permission: string) => boolean;
|
|
34
|
+
agent: () => string;
|
|
35
|
+
agentId: () => string;
|
|
36
|
+
task: () => string;
|
|
37
|
+
expiresAt: () => Date;
|
|
38
|
+
permissions: () => string[];
|
|
39
|
+
tokenId: () => string;
|
|
40
|
+
};
|
|
41
|
+
/** Normalize to wire form github:pull_request.merge for the control plane. */
|
|
42
|
+
export declare function flatten(req: MintRequest): string[];
|
|
43
|
+
export declare class AlnairClient {
|
|
44
|
+
#private;
|
|
45
|
+
constructor(config?: AlnairConfig);
|
|
46
|
+
static fromProject(overrides?: AlnairConfig): Promise<AlnairClient>;
|
|
47
|
+
get url(): string;
|
|
48
|
+
register(name: string, runtime?: string): Promise<{
|
|
49
|
+
id: string;
|
|
50
|
+
name: string;
|
|
51
|
+
}>;
|
|
52
|
+
mint(req: MintRequest): Promise<string>;
|
|
53
|
+
mintCapability(req: MintRequest): Promise<{
|
|
54
|
+
token: string;
|
|
55
|
+
capability: Capability;
|
|
56
|
+
}>;
|
|
57
|
+
verify(token: string, opts?: {
|
|
58
|
+
audience?: string;
|
|
59
|
+
}): Promise<Capability>;
|
|
60
|
+
}
|
|
61
|
+
export declare const alnair: {
|
|
62
|
+
mint: (req: MintRequest) => Promise<string>;
|
|
63
|
+
verify: (token: string, opts?: {
|
|
64
|
+
audience?: string;
|
|
65
|
+
}) => Promise<Capability>;
|
|
66
|
+
mintCapability: (req: MintRequest) => Promise<{
|
|
67
|
+
token: string;
|
|
68
|
+
capability: Capability;
|
|
69
|
+
}>;
|
|
70
|
+
register: (name: string, runtime?: string) => Promise<{
|
|
71
|
+
id: string;
|
|
72
|
+
name: string;
|
|
73
|
+
}>;
|
|
74
|
+
configure: (config: AlnairConfig) => void;
|
|
75
|
+
};
|
|
76
|
+
export { prettyAgent, prettyAction, prettyProvider, parsePermission, displayPermission, formatMintNotice, shouldNotify, streamMintNotice, } from "./notify.js";
|
|
77
|
+
export { mark, markGlyph, MARK } from "./mark.js";
|
|
78
|
+
export type { MarkState } from "./mark.js";
|
|
79
|
+
export { detectRunningAgent } from "./agent.js";
|
|
80
|
+
export default alnair;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* alnair-sdk — mint and verify short-lived agent capabilities.
|
|
3
|
+
*
|
|
4
|
+
* Claude / Cursor call this. Developers run `npx alnair init` once, then forget it.
|
|
5
|
+
*
|
|
6
|
+
* const token = await alnair.mint({
|
|
7
|
+
* task: "Fix authentication bug",
|
|
8
|
+
* permissions: ["github.pull_request.merge"],
|
|
9
|
+
* ttl: "15m",
|
|
10
|
+
* })
|
|
11
|
+
*/
|
|
12
|
+
import { streamMintNotice, parsePermission, shouldNotify } from "./notify.js";
|
|
13
|
+
import { detectRunningAgent } from "./agent.js";
|
|
14
|
+
async function loadConfigFile() {
|
|
15
|
+
try {
|
|
16
|
+
const { readFile } = await import("node:fs/promises");
|
|
17
|
+
const { join } = await import("node:path");
|
|
18
|
+
const raw = await readFile(join(process.cwd(), ".alnair", "config.json"), "utf8");
|
|
19
|
+
return JSON.parse(raw);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return {};
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/** Normalize to wire form github:pull_request.merge for the control plane. */
|
|
26
|
+
export function flatten(req) {
|
|
27
|
+
const out = [];
|
|
28
|
+
for (const p of req.permissions ?? []) {
|
|
29
|
+
const trimmed = p.trim();
|
|
30
|
+
if (!trimmed)
|
|
31
|
+
continue;
|
|
32
|
+
if (trimmed.includes(":")) {
|
|
33
|
+
out.push(trimmed);
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
// github.pull_request.merge → github:pull_request.merge
|
|
37
|
+
const { provider, action } = parsePermission(trimmed);
|
|
38
|
+
if (provider && action)
|
|
39
|
+
out.push(`${provider}:${action}`);
|
|
40
|
+
else
|
|
41
|
+
out.push(trimmed);
|
|
42
|
+
}
|
|
43
|
+
const add = (provider, actions) => {
|
|
44
|
+
for (const a of actions ?? []) {
|
|
45
|
+
const trimmed = a.trim();
|
|
46
|
+
if (!trimmed)
|
|
47
|
+
continue;
|
|
48
|
+
out.push(trimmed.includes(":") ? trimmed : `${provider}:${trimmed}`);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
add("github", req.github);
|
|
52
|
+
add("supabase", req.supabase);
|
|
53
|
+
add("postgres", req.postgres);
|
|
54
|
+
add("vercel", req.vercel);
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
function canMatch(granted, want) {
|
|
58
|
+
let w = want.trim();
|
|
59
|
+
if (!w)
|
|
60
|
+
return false;
|
|
61
|
+
// Allow asking with dots or colons
|
|
62
|
+
if (!w.includes(":") && w.includes(".")) {
|
|
63
|
+
const { provider, action } = parsePermission(w);
|
|
64
|
+
w = `${provider}:${action}`;
|
|
65
|
+
}
|
|
66
|
+
for (const p of granted) {
|
|
67
|
+
if (p === w)
|
|
68
|
+
return true;
|
|
69
|
+
if (p.endsWith(":*") && w.startsWith(p.slice(0, -1)))
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
function capabilityFrom(result) {
|
|
75
|
+
const perms = result.permissions ?? [];
|
|
76
|
+
return {
|
|
77
|
+
can: (permission) => canMatch(perms, permission),
|
|
78
|
+
agent: () => result.agent,
|
|
79
|
+
agentId: () => result.agent_id,
|
|
80
|
+
task: () => result.task ?? "",
|
|
81
|
+
expiresAt: () => new Date(result.not_after),
|
|
82
|
+
permissions: () => [...perms],
|
|
83
|
+
tokenId: () => result.token_id,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
export class AlnairClient {
|
|
87
|
+
#url;
|
|
88
|
+
#agent;
|
|
89
|
+
constructor(config = {}) {
|
|
90
|
+
this.#url = (config.url ?? "http://localhost:8080").replace(/\/$/, "");
|
|
91
|
+
this.#agent = config.agent ?? "claude-code";
|
|
92
|
+
}
|
|
93
|
+
static async fromProject(overrides = {}) {
|
|
94
|
+
const file = await loadConfigFile();
|
|
95
|
+
const runtime = detectRunningAgent();
|
|
96
|
+
return new AlnairClient({
|
|
97
|
+
url: overrides.url ?? file.url ?? process.env.ALNAIR_URL ?? "http://localhost:8080",
|
|
98
|
+
agent: overrides.agent ?? (runtime || file.agent || "claude-code"),
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
get url() {
|
|
102
|
+
return this.#url;
|
|
103
|
+
}
|
|
104
|
+
async register(name, runtime = "cli") {
|
|
105
|
+
return this.#post("/v1/agents", { name, runtime });
|
|
106
|
+
}
|
|
107
|
+
async mint(req) {
|
|
108
|
+
const perms = flatten(req);
|
|
109
|
+
if (perms.length === 0) {
|
|
110
|
+
throw new Error('Add at least one permission, e.g. { permissions: ["github.pull_request.merge"] }');
|
|
111
|
+
}
|
|
112
|
+
if (!req.task?.trim()) {
|
|
113
|
+
throw new Error('Add a task so the grant is meaningful, e.g. { task: "Fix authentication bug" }');
|
|
114
|
+
}
|
|
115
|
+
const agent = req.agent ?? this.#agent;
|
|
116
|
+
const body = {
|
|
117
|
+
task: req.task,
|
|
118
|
+
agent,
|
|
119
|
+
ttl: req.ttl ?? "15m",
|
|
120
|
+
audience: req.audience,
|
|
121
|
+
// Control plane expects provider arrays — split wire perms back
|
|
122
|
+
...toProviderBody(perms),
|
|
123
|
+
};
|
|
124
|
+
const started = Date.now();
|
|
125
|
+
const res = await this.#post("/v1/capabilities/mint", body);
|
|
126
|
+
if (req.notify !== false && shouldNotify(req.notify)) {
|
|
127
|
+
streamMintNotice({
|
|
128
|
+
agent: res.agent || agent,
|
|
129
|
+
task: res.task || req.task,
|
|
130
|
+
permissions: res.permissions?.length ? res.permissions : perms,
|
|
131
|
+
ttl: req.ttl ?? "15m",
|
|
132
|
+
ms: Date.now() - started,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
// Session file for git hooks / gates
|
|
136
|
+
await writeSession({
|
|
137
|
+
permissions: res.permissions?.length ? res.permissions : perms,
|
|
138
|
+
expiresAt: res.not_after,
|
|
139
|
+
task: res.task || req.task,
|
|
140
|
+
agent: res.agent || agent,
|
|
141
|
+
tokenId: res.token_id,
|
|
142
|
+
});
|
|
143
|
+
return res.token;
|
|
144
|
+
}
|
|
145
|
+
async mintCapability(req) {
|
|
146
|
+
const token = await this.mint(req);
|
|
147
|
+
const capability = await this.verify(token);
|
|
148
|
+
return { token, capability };
|
|
149
|
+
}
|
|
150
|
+
async verify(token, opts) {
|
|
151
|
+
const res = await this.#post("/v1/capabilities/verify", {
|
|
152
|
+
token,
|
|
153
|
+
audience: opts?.audience,
|
|
154
|
+
});
|
|
155
|
+
return capabilityFrom({
|
|
156
|
+
...res,
|
|
157
|
+
token_id: res.token_id ?? "",
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
async #post(path, body) {
|
|
161
|
+
let res;
|
|
162
|
+
try {
|
|
163
|
+
res = await fetch(`${this.#url}${path}`, {
|
|
164
|
+
method: "POST",
|
|
165
|
+
headers: { "Content-Type": "application/json" },
|
|
166
|
+
body: JSON.stringify(body),
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
throw new Error(`Cannot reach Alnair at ${this.#url}. Run \`npx alnair init\` first.`);
|
|
171
|
+
}
|
|
172
|
+
const text = await res.text();
|
|
173
|
+
let data = null;
|
|
174
|
+
try {
|
|
175
|
+
data = text ? JSON.parse(text) : null;
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
data = { error: text };
|
|
179
|
+
}
|
|
180
|
+
if (!res.ok) {
|
|
181
|
+
const msg = data && typeof data === "object" && data !== null && "error" in data
|
|
182
|
+
? String(data.error)
|
|
183
|
+
: res.statusText;
|
|
184
|
+
throw new Error(msg);
|
|
185
|
+
}
|
|
186
|
+
return data;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
async function writeSession(session) {
|
|
190
|
+
try {
|
|
191
|
+
const { mkdir, writeFile } = await import("node:fs/promises");
|
|
192
|
+
const { join } = await import("node:path");
|
|
193
|
+
const dir = join(process.cwd(), ".alnair");
|
|
194
|
+
await mkdir(dir, { recursive: true });
|
|
195
|
+
await writeFile(join(dir, "session.json"), JSON.stringify(session, null, 2) + "\n");
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
/* non-fatal */
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
function toProviderBody(perms) {
|
|
202
|
+
const body = {};
|
|
203
|
+
for (const p of perms) {
|
|
204
|
+
const i = p.indexOf(":");
|
|
205
|
+
if (i < 0)
|
|
206
|
+
continue;
|
|
207
|
+
const prov = p.slice(0, i);
|
|
208
|
+
const act = p.slice(i + 1);
|
|
209
|
+
if (!body[prov])
|
|
210
|
+
body[prov] = [];
|
|
211
|
+
body[prov].push(act);
|
|
212
|
+
}
|
|
213
|
+
return body;
|
|
214
|
+
}
|
|
215
|
+
let _default = null;
|
|
216
|
+
async function getDefault() {
|
|
217
|
+
if (!_default)
|
|
218
|
+
_default = await AlnairClient.fromProject();
|
|
219
|
+
return _default;
|
|
220
|
+
}
|
|
221
|
+
export const alnair = {
|
|
222
|
+
mint: async (req) => (await getDefault()).mint(req),
|
|
223
|
+
verify: async (token, opts) => (await getDefault()).verify(token, opts),
|
|
224
|
+
mintCapability: async (req) => (await getDefault()).mintCapability(req),
|
|
225
|
+
register: async (name, runtime) => (await getDefault()).register(name, runtime),
|
|
226
|
+
configure: (config) => {
|
|
227
|
+
_default = new AlnairClient(config);
|
|
228
|
+
},
|
|
229
|
+
};
|
|
230
|
+
export { prettyAgent, prettyAction, prettyProvider, parsePermission, displayPermission, formatMintNotice, shouldNotify, streamMintNotice, } from "./notify.js";
|
|
231
|
+
export { mark, markGlyph, MARK } from "./mark.js";
|
|
232
|
+
export { detectRunningAgent } from "./agent.js";
|
|
233
|
+
export default alnair;
|
package/dist/mark.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Alnair mark — nested squares from brand 4b / 6a.
|
|
3
|
+
*
|
|
4
|
+
* Outer square = identity (constant).
|
|
5
|
+
* Inner square = capability:
|
|
6
|
+
* issued ▣ solid purple — authority holds
|
|
7
|
+
* used ▢ purple outline — exercised, grant still stands
|
|
8
|
+
* expired ⬚ faint ghost — returned to identity
|
|
9
|
+
*
|
|
10
|
+
* Purple (#7C6CF5) only while the capability holds authority.
|
|
11
|
+
*/
|
|
12
|
+
export type MarkState = "issued" | "used" | "expired" | "brand";
|
|
13
|
+
/** Colored Alnair mark for the given lifecycle state. */
|
|
14
|
+
export declare function mark(state?: MarkState): string;
|
|
15
|
+
/** Raw glyph without ANSI (tests, plain logs). */
|
|
16
|
+
export declare function markGlyph(state?: MarkState): string;
|
|
17
|
+
export declare const MARK: {
|
|
18
|
+
readonly brand: () => string;
|
|
19
|
+
readonly issued: () => string;
|
|
20
|
+
readonly used: () => string;
|
|
21
|
+
readonly expired: () => string;
|
|
22
|
+
};
|
package/dist/mark.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Alnair mark — nested squares from brand 4b / 6a.
|
|
3
|
+
*
|
|
4
|
+
* Outer square = identity (constant).
|
|
5
|
+
* Inner square = capability:
|
|
6
|
+
* issued ▣ solid purple — authority holds
|
|
7
|
+
* used ▢ purple outline — exercised, grant still stands
|
|
8
|
+
* expired ⬚ faint ghost — returned to identity
|
|
9
|
+
*
|
|
10
|
+
* Purple (#7C6CF5) only while the capability holds authority.
|
|
11
|
+
*/
|
|
12
|
+
const PURPLE = "\x1b[38;2;124;108;245m";
|
|
13
|
+
const DIM = "\x1b[2m";
|
|
14
|
+
const RESET = "\x1b[0m";
|
|
15
|
+
const GLYPH = {
|
|
16
|
+
brand: "▣",
|
|
17
|
+
issued: "▣",
|
|
18
|
+
used: "▢",
|
|
19
|
+
expired: "⬚",
|
|
20
|
+
};
|
|
21
|
+
function colorEnabled() {
|
|
22
|
+
if (process.env.NO_COLOR)
|
|
23
|
+
return false;
|
|
24
|
+
if (process.env.FORCE_COLOR === "0")
|
|
25
|
+
return false;
|
|
26
|
+
// Don't emit ANSI into Cursor/Claude tool captures — they often hide it.
|
|
27
|
+
if (!process.stdout.isTTY && !process.stderr.isTTY)
|
|
28
|
+
return false;
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
/** Colored Alnair mark for the given lifecycle state. */
|
|
32
|
+
export function mark(state = "brand") {
|
|
33
|
+
const g = GLYPH[state];
|
|
34
|
+
if (!colorEnabled())
|
|
35
|
+
return g;
|
|
36
|
+
if (state === "expired")
|
|
37
|
+
return `${DIM}${g}${RESET}`;
|
|
38
|
+
// brand / issued / used — purple (used is outline glyph, still purple)
|
|
39
|
+
return `${PURPLE}${g}${RESET}`;
|
|
40
|
+
}
|
|
41
|
+
/** Raw glyph without ANSI (tests, plain logs). */
|
|
42
|
+
export function markGlyph(state = "brand") {
|
|
43
|
+
return GLYPH[state];
|
|
44
|
+
}
|
|
45
|
+
export const MARK = {
|
|
46
|
+
brand: () => mark("brand"),
|
|
47
|
+
issued: () => mark("issued"),
|
|
48
|
+
used: () => mark("used"),
|
|
49
|
+
expired: () => mark("expired"),
|
|
50
|
+
};
|
package/dist/notify.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Developer-facing grant notice — one line on stdout for tool UIs.
|
|
3
|
+
*
|
|
4
|
+
* [alnair] Git access granted (piped / agent)
|
|
5
|
+
* ▣ Git access granted (interactive TTY)
|
|
6
|
+
*
|
|
7
|
+
* Opt out with ALNAIR_QUIET=1. No desktop toasts.
|
|
8
|
+
*/
|
|
9
|
+
export declare function prettyAgent(name: string): string;
|
|
10
|
+
export declare function parsePermission(perm: string): {
|
|
11
|
+
provider: string;
|
|
12
|
+
action: string;
|
|
13
|
+
};
|
|
14
|
+
export declare function displayPermission(perm: string): {
|
|
15
|
+
group: string;
|
|
16
|
+
label: string;
|
|
17
|
+
};
|
|
18
|
+
export declare function prettyAction(action: string): string;
|
|
19
|
+
export declare function prettyProvider(provider: string): string;
|
|
20
|
+
export type MintNotice = {
|
|
21
|
+
agent: string;
|
|
22
|
+
task?: string;
|
|
23
|
+
permissions: string[];
|
|
24
|
+
ttl?: string;
|
|
25
|
+
ms: number;
|
|
26
|
+
};
|
|
27
|
+
/** Plain line for agent tool UIs; colored mark only on a real TTY. */
|
|
28
|
+
export declare function formatMintNotice(n: MintNotice): string;
|
|
29
|
+
export declare function shouldNotify(explicit?: boolean): boolean;
|
|
30
|
+
export declare function streamMintNotice(n: MintNotice): void;
|
package/dist/notify.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Developer-facing grant notice — one line on stdout for tool UIs.
|
|
3
|
+
*
|
|
4
|
+
* [alnair] Git access granted (piped / agent)
|
|
5
|
+
* ▣ Git access granted (interactive TTY)
|
|
6
|
+
*
|
|
7
|
+
* Opt out with ALNAIR_QUIET=1. No desktop toasts.
|
|
8
|
+
*/
|
|
9
|
+
import { mark, markGlyph } from "./mark.js";
|
|
10
|
+
function isQuiet() {
|
|
11
|
+
return process.env.ALNAIR_QUIET === "1" || process.env.ALNAIR_QUIET === "true";
|
|
12
|
+
}
|
|
13
|
+
function writeDeveloper(text) {
|
|
14
|
+
try {
|
|
15
|
+
process.stdout.write(text);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
try {
|
|
19
|
+
process.stderr.write(text);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
/* ignore */
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export function prettyAgent(name) {
|
|
27
|
+
const map = {
|
|
28
|
+
"claude-code": "Claude Code",
|
|
29
|
+
claude: "Claude Code",
|
|
30
|
+
"cursor-agent": "Cursor",
|
|
31
|
+
cursor: "Cursor",
|
|
32
|
+
codex: "Codex",
|
|
33
|
+
"ci-runner": "GitHub Actions",
|
|
34
|
+
};
|
|
35
|
+
return map[name] ?? name;
|
|
36
|
+
}
|
|
37
|
+
export function parsePermission(perm) {
|
|
38
|
+
const normalized = perm.replace(/^\s+|\s+$/g, "");
|
|
39
|
+
if (normalized.includes(":")) {
|
|
40
|
+
const [provider, ...rest] = normalized.split(":");
|
|
41
|
+
return { provider, action: rest.join(":") };
|
|
42
|
+
}
|
|
43
|
+
const [provider, ...rest] = normalized.split(".");
|
|
44
|
+
return { provider, action: rest.join(".") };
|
|
45
|
+
}
|
|
46
|
+
export function displayPermission(perm) {
|
|
47
|
+
const { provider, action } = parsePermission(perm);
|
|
48
|
+
const key = `${provider}.${action}`.toLowerCase();
|
|
49
|
+
const known = {
|
|
50
|
+
"github.git.commit": { group: "Git", label: "Commit changes" },
|
|
51
|
+
"github.git.push": { group: "GitHub", label: "Push to repository" },
|
|
52
|
+
"github.pull_request.merge": { group: "GitHub", label: "Merge pull requests" },
|
|
53
|
+
"github.pr.merge": { group: "GitHub", label: "Merge pull requests" },
|
|
54
|
+
"github.pull_request.open": { group: "GitHub", label: "Open pull requests" },
|
|
55
|
+
"vercel.deploy.preview": { group: "Vercel", label: "Deploy preview" },
|
|
56
|
+
"vercel.deploy.production": { group: "Vercel", label: "Deploy to production" },
|
|
57
|
+
"supabase.write": { group: "Supabase", label: "Write data" },
|
|
58
|
+
"supabase.read": { group: "Supabase", label: "Read data" },
|
|
59
|
+
"postgres.write": { group: "Postgres", label: "Write data" },
|
|
60
|
+
"postgres.read": { group: "Postgres", label: "Read data" },
|
|
61
|
+
};
|
|
62
|
+
if (known[key])
|
|
63
|
+
return known[key];
|
|
64
|
+
return {
|
|
65
|
+
group: prettyProvider(provider),
|
|
66
|
+
label: prettyAction(action),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
export function prettyAction(action) {
|
|
70
|
+
const known = {
|
|
71
|
+
"pull_request.merge": "Merge pull requests",
|
|
72
|
+
"pr.merge": "Merge pull requests",
|
|
73
|
+
"pull_request.open": "Open pull requests",
|
|
74
|
+
"git.commit": "Commit changes",
|
|
75
|
+
"git.push": "Push to repository",
|
|
76
|
+
read: "Read data",
|
|
77
|
+
write: "Write data",
|
|
78
|
+
"deploy.preview": "Deploy preview",
|
|
79
|
+
"deploy.production": "Deploy to production",
|
|
80
|
+
};
|
|
81
|
+
if (known[action])
|
|
82
|
+
return known[action];
|
|
83
|
+
return action.replace(/[._]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
84
|
+
}
|
|
85
|
+
export function prettyProvider(provider) {
|
|
86
|
+
const known = {
|
|
87
|
+
github: "GitHub",
|
|
88
|
+
git: "Git",
|
|
89
|
+
supabase: "Supabase",
|
|
90
|
+
vercel: "Vercel",
|
|
91
|
+
postgres: "Postgres",
|
|
92
|
+
};
|
|
93
|
+
return known[provider.toLowerCase()] ?? provider;
|
|
94
|
+
}
|
|
95
|
+
function whoFrom(n) {
|
|
96
|
+
const groups = [...new Set(n.permissions.map((p) => displayPermission(p).group))];
|
|
97
|
+
return groups.length === 1 ? groups[0] : "Access";
|
|
98
|
+
}
|
|
99
|
+
/** Plain line for agent tool UIs; colored mark only on a real TTY. */
|
|
100
|
+
export function formatMintNotice(n) {
|
|
101
|
+
const who = whoFrom(n);
|
|
102
|
+
if (process.stdout.isTTY) {
|
|
103
|
+
return ` ${mark("issued")} ${who} access granted\n`;
|
|
104
|
+
}
|
|
105
|
+
return ` [alnair] ${markGlyph("issued")} ${who} access granted\n`;
|
|
106
|
+
}
|
|
107
|
+
export function shouldNotify(explicit) {
|
|
108
|
+
if (explicit === true)
|
|
109
|
+
return true;
|
|
110
|
+
if (explicit === false)
|
|
111
|
+
return false;
|
|
112
|
+
return !isQuiet();
|
|
113
|
+
}
|
|
114
|
+
export function streamMintNotice(n) {
|
|
115
|
+
if (isQuiet())
|
|
116
|
+
return;
|
|
117
|
+
writeDeveloper(formatMintNotice(n));
|
|
118
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "alnair-sdk",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Give AI agents secure, temporary permissions",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./gate": "./dist/gate.js"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsc -p tsconfig.json",
|
|
20
|
+
"prepare": "npm run build"
|
|
21
|
+
},
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "git+https://github.com/CamiloJac/seren.git",
|
|
25
|
+
"directory": "packages/sdk"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"alnair",
|
|
29
|
+
"ai",
|
|
30
|
+
"agents",
|
|
31
|
+
"permissions",
|
|
32
|
+
"capabilities"
|
|
33
|
+
],
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=18"
|
|
36
|
+
},
|
|
37
|
+
"license": "MIT",
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"typescript": "^5.6.3"
|
|
40
|
+
}
|
|
41
|
+
}
|