@behalfid/cli 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/dist/commands/agents.d.ts +2 -0
- package/dist/commands/agents.js +159 -0
- package/dist/commands/config.d.ts +2 -0
- package/dist/commands/config.js +67 -0
- package/dist/commands/health.d.ts +2 -0
- package/dist/commands/health.js +25 -0
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.js +71 -0
- package/dist/commands/login.d.ts +2 -0
- package/dist/commands/login.js +45 -0
- package/dist/commands/logout.d.ts +2 -0
- package/dist/commands/logout.js +33 -0
- package/dist/commands/logs.d.ts +2 -0
- package/dist/commands/logs.js +32 -0
- package/dist/commands/mcp.d.ts +2 -0
- package/dist/commands/mcp.js +150 -0
- package/dist/commands/passport.d.ts +2 -0
- package/dist/commands/passport.js +41 -0
- package/dist/commands/permissions.d.ts +2 -0
- package/dist/commands/permissions.js +74 -0
- package/dist/commands/run.d.ts +4 -0
- package/dist/commands/run.js +106 -0
- package/dist/commands/verify.d.ts +2 -0
- package/dist/commands/verify.js +37 -0
- package/dist/commands/whoami.d.ts +2 -0
- package/dist/commands/whoami.js +49 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +68 -0
- package/dist/lib/client.d.ts +13 -0
- package/dist/lib/client.js +62 -0
- package/dist/lib/config.d.ts +13 -0
- package/dist/lib/config.js +48 -0
- package/dist/lib/context-generator.d.ts +3 -0
- package/dist/lib/context-generator.js +70 -0
- package/dist/lib/mcp-server.d.ts +5 -0
- package/dist/lib/mcp-server.js +140 -0
- package/dist/lib/output.d.ts +10 -0
- package/dist/lib/output.js +48 -0
- package/dist/lib/passport-cache.d.ts +31 -0
- package/dist/lib/passport-cache.js +43 -0
- package/dist/lib/prompt.d.ts +3 -0
- package/dist/lib/prompt.js +53 -0
- package/package.json +33 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { createInterface } from "node:readline";
|
|
2
|
+
import { readCachedDetail, fetchAndCacheDetail } from "./passport-cache.js";
|
|
3
|
+
import { apiRequest } from "./client.js";
|
|
4
|
+
function respond(id, result) {
|
|
5
|
+
process.stdout.write(JSON.stringify({ jsonrpc: "2.0", id, result }) + "\n");
|
|
6
|
+
}
|
|
7
|
+
function respondError(id, code, message) {
|
|
8
|
+
process.stdout.write(JSON.stringify({ jsonrpc: "2.0", id, error: { code, message } }) + "\n");
|
|
9
|
+
}
|
|
10
|
+
const TOOLS = [
|
|
11
|
+
{
|
|
12
|
+
name: "verify_action",
|
|
13
|
+
description: "Verify whether this agent is permitted to perform an action BEFORE executing it. " +
|
|
14
|
+
"You MUST call this before any action that touches an external service. " +
|
|
15
|
+
"If the response contains `\"allowed\": false`, do NOT proceed — explain the reason to the user instead.",
|
|
16
|
+
inputSchema: {
|
|
17
|
+
type: "object",
|
|
18
|
+
properties: {
|
|
19
|
+
action: {
|
|
20
|
+
type: "string",
|
|
21
|
+
description: 'The action to verify. Common values: "purchase", "access_data", "browse_web", "schedule", "create_content", "send_message". Use the most specific value that describes what you are about to do.',
|
|
22
|
+
},
|
|
23
|
+
vendor: {
|
|
24
|
+
type: "string",
|
|
25
|
+
description: 'The vendor, service, or domain being accessed (e.g. "amazon.com", "gmail.com", "google-calendar"). Omit for generic actions.',
|
|
26
|
+
},
|
|
27
|
+
amount: {
|
|
28
|
+
type: "number",
|
|
29
|
+
description: "Transaction amount in dollars. Only required for purchase-type actions.",
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
required: ["action"],
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: "get_permissions",
|
|
37
|
+
description: "Get the current active permissions for this agent — what actions are allowed, on which resources, and under what constraints.",
|
|
38
|
+
inputSchema: {
|
|
39
|
+
type: "object",
|
|
40
|
+
properties: {},
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
];
|
|
44
|
+
export async function startMcpServer(config) {
|
|
45
|
+
const rl = createInterface({ input: process.stdin, terminal: false });
|
|
46
|
+
rl.on("line", async (line) => {
|
|
47
|
+
const raw = line.trim();
|
|
48
|
+
if (!raw)
|
|
49
|
+
return;
|
|
50
|
+
let req;
|
|
51
|
+
try {
|
|
52
|
+
req = JSON.parse(raw);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
// Notifications have no id — no response needed
|
|
58
|
+
if (req.id === undefined || req.id === null) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
switch (req.method) {
|
|
63
|
+
case "initialize":
|
|
64
|
+
respond(req.id, {
|
|
65
|
+
protocolVersion: "2024-11-05",
|
|
66
|
+
capabilities: { tools: {} },
|
|
67
|
+
serverInfo: { name: "behalfid", version: "0.1.0" },
|
|
68
|
+
});
|
|
69
|
+
break;
|
|
70
|
+
case "ping":
|
|
71
|
+
respond(req.id, {});
|
|
72
|
+
break;
|
|
73
|
+
case "tools/list":
|
|
74
|
+
respond(req.id, { tools: TOOLS });
|
|
75
|
+
break;
|
|
76
|
+
case "tools/call": {
|
|
77
|
+
const params = req.params;
|
|
78
|
+
const toolName = params?.name;
|
|
79
|
+
const args = params?.arguments ?? {};
|
|
80
|
+
if (toolName === "verify_action") {
|
|
81
|
+
const body = {
|
|
82
|
+
agentId: config.agentId,
|
|
83
|
+
action: args.action,
|
|
84
|
+
};
|
|
85
|
+
if (args.vendor)
|
|
86
|
+
body.vendor = args.vendor;
|
|
87
|
+
if (args.amount !== undefined)
|
|
88
|
+
body.amount = args.amount;
|
|
89
|
+
const result = await apiRequest("/api/verify", {
|
|
90
|
+
method: "POST",
|
|
91
|
+
body,
|
|
92
|
+
apiKey: config.apiKey,
|
|
93
|
+
baseUrl: config.baseUrl,
|
|
94
|
+
skipAuth: false,
|
|
95
|
+
});
|
|
96
|
+
respond(req.id, {
|
|
97
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
else if (toolName === "get_permissions") {
|
|
101
|
+
let detail = readCachedDetail(config.agentId);
|
|
102
|
+
if (!detail) {
|
|
103
|
+
try {
|
|
104
|
+
detail = await fetchAndCacheDetail(config.agentId, config.baseUrl);
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
respond(req.id, {
|
|
108
|
+
content: [
|
|
109
|
+
{
|
|
110
|
+
type: "text",
|
|
111
|
+
text: JSON.stringify({
|
|
112
|
+
error: "Permissions cache is empty. Run `behalf mcp init` to populate it.",
|
|
113
|
+
}),
|
|
114
|
+
},
|
|
115
|
+
],
|
|
116
|
+
isError: true,
|
|
117
|
+
});
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
respond(req.id, {
|
|
122
|
+
content: [{ type: "text", text: JSON.stringify(detail, null, 2) }],
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
respondError(req.id, -32601, `Unknown tool: ${toolName ?? "(none)"}`);
|
|
127
|
+
}
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
default:
|
|
131
|
+
respondError(req.id, -32601, `Method not found: ${req.method}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
catch (err) {
|
|
135
|
+
respondError(req.id, -32603, err instanceof Error ? err.message : "Internal error");
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
// Keep alive until stdin closes
|
|
139
|
+
process.stdin.resume();
|
|
140
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare function setJsonMode(on: boolean): void;
|
|
2
|
+
export declare function isJsonMode(): boolean;
|
|
3
|
+
export declare function printJson(data: unknown): void;
|
|
4
|
+
export declare function printError(message: string): void;
|
|
5
|
+
export declare function printSuccess(message: string): void;
|
|
6
|
+
type Row = Record<string, string | number | boolean | null | undefined>;
|
|
7
|
+
export declare function printTable(rows: Row[], columns?: string[]): void;
|
|
8
|
+
export declare function printKv(pairs: Record<string, string | number | boolean | null | undefined>): void;
|
|
9
|
+
export declare function runAction(fn: (...args: any[]) => Promise<void>): (...args: any[]) => void;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
let _json = false;
|
|
2
|
+
export function setJsonMode(on) { _json = on; }
|
|
3
|
+
export function isJsonMode() { return _json; }
|
|
4
|
+
export function printJson(data) {
|
|
5
|
+
console.log(JSON.stringify(data, null, 2));
|
|
6
|
+
}
|
|
7
|
+
export function printError(message) {
|
|
8
|
+
if (_json) {
|
|
9
|
+
console.error(JSON.stringify({ error: message }));
|
|
10
|
+
}
|
|
11
|
+
else {
|
|
12
|
+
console.error(`Error: ${message}`);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export function printSuccess(message) {
|
|
16
|
+
if (!_json)
|
|
17
|
+
console.log(message);
|
|
18
|
+
}
|
|
19
|
+
export function printTable(rows, columns) {
|
|
20
|
+
if (rows.length === 0) {
|
|
21
|
+
console.log("(none)");
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const keys = columns ?? Object.keys(rows[0]);
|
|
25
|
+
const widths = keys.map(k => Math.max(k.length, ...rows.map(r => String(r[k] ?? "").length)));
|
|
26
|
+
const sep = widths.map(w => "─".repeat(w)).join(" ");
|
|
27
|
+
console.log(keys.map((k, i) => k.toUpperCase().padEnd(widths[i])).join(" "));
|
|
28
|
+
console.log(sep);
|
|
29
|
+
for (const row of rows) {
|
|
30
|
+
console.log(keys.map((k, i) => String(row[k] ?? "").padEnd(widths[i])).join(" "));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export function printKv(pairs) {
|
|
34
|
+
const keyWidth = Math.max(...Object.keys(pairs).map(k => k.length));
|
|
35
|
+
for (const [k, v] of Object.entries(pairs)) {
|
|
36
|
+
console.log(`${k.padEnd(keyWidth)} ${v ?? ""}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
40
|
+
export function runAction(fn) {
|
|
41
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
42
|
+
return (...args) => {
|
|
43
|
+
fn(...args).catch((err) => {
|
|
44
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
45
|
+
process.exit(1);
|
|
46
|
+
});
|
|
47
|
+
};
|
|
48
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export type PermissionEntry = {
|
|
2
|
+
permissionId: string;
|
|
3
|
+
action: string;
|
|
4
|
+
description?: string | null;
|
|
5
|
+
resource?: string | null;
|
|
6
|
+
scope?: string | null;
|
|
7
|
+
allowedActions?: string[] | null;
|
|
8
|
+
blockedActions?: string[] | null;
|
|
9
|
+
requiresApproval?: boolean | null;
|
|
10
|
+
template?: string | null;
|
|
11
|
+
constraints?: {
|
|
12
|
+
maxAmount?: number | null;
|
|
13
|
+
allowedVendors?: string[] | null;
|
|
14
|
+
expiresAt?: string | null;
|
|
15
|
+
} | null;
|
|
16
|
+
status: string;
|
|
17
|
+
};
|
|
18
|
+
export type AgentDetail = {
|
|
19
|
+
agent: {
|
|
20
|
+
agentId: string;
|
|
21
|
+
name: string;
|
|
22
|
+
status: string;
|
|
23
|
+
agentType?: string | null;
|
|
24
|
+
provider?: string | null;
|
|
25
|
+
description?: string | null;
|
|
26
|
+
};
|
|
27
|
+
permissions: PermissionEntry[];
|
|
28
|
+
};
|
|
29
|
+
export declare function readCachedDetail(agentId: string): AgentDetail | null;
|
|
30
|
+
export declare function writeCachedDetail(agentId: string, data: AgentDetail): void;
|
|
31
|
+
export declare function fetchAndCacheDetail(agentId: string, baseUrl?: string, forceRefresh?: boolean): Promise<AgentDetail>;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { apiRequest } from "./client.js";
|
|
5
|
+
const CACHE_DIR = join(homedir(), ".behalf", "cache");
|
|
6
|
+
const TTL_MS = 5 * 60 * 1000;
|
|
7
|
+
function cacheFile(agentId) {
|
|
8
|
+
return join(CACHE_DIR, `${agentId}.json`);
|
|
9
|
+
}
|
|
10
|
+
function ensureCacheDir() {
|
|
11
|
+
if (!existsSync(CACHE_DIR))
|
|
12
|
+
mkdirSync(CACHE_DIR, { recursive: true });
|
|
13
|
+
}
|
|
14
|
+
export function readCachedDetail(agentId) {
|
|
15
|
+
const path = cacheFile(agentId);
|
|
16
|
+
if (!existsSync(path))
|
|
17
|
+
return null;
|
|
18
|
+
try {
|
|
19
|
+
const entry = JSON.parse(readFileSync(path, "utf-8"));
|
|
20
|
+
const age = Date.now() - new Date(entry.fetchedAt).getTime();
|
|
21
|
+
if (age > TTL_MS)
|
|
22
|
+
return null;
|
|
23
|
+
return entry.data;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export function writeCachedDetail(agentId, data) {
|
|
30
|
+
ensureCacheDir();
|
|
31
|
+
const entry = { agentId, fetchedAt: new Date().toISOString(), data };
|
|
32
|
+
writeFileSync(cacheFile(agentId), JSON.stringify(entry, null, 2) + "\n");
|
|
33
|
+
}
|
|
34
|
+
export async function fetchAndCacheDetail(agentId, baseUrl, forceRefresh = false) {
|
|
35
|
+
if (!forceRefresh) {
|
|
36
|
+
const cached = readCachedDetail(agentId);
|
|
37
|
+
if (cached)
|
|
38
|
+
return cached;
|
|
39
|
+
}
|
|
40
|
+
const data = await apiRequest(`/api/dashboard/agents/${encodeURIComponent(agentId)}`, { baseUrl });
|
|
41
|
+
writeCachedDetail(agentId, data);
|
|
42
|
+
return data;
|
|
43
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { createInterface } from "node:readline/promises";
|
|
2
|
+
export async function ask(question, defaultValue) {
|
|
3
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
4
|
+
const suffix = defaultValue ? ` [${defaultValue}]: ` : ": ";
|
|
5
|
+
const answer = await rl.question(question + suffix);
|
|
6
|
+
rl.close();
|
|
7
|
+
return answer.trim() || defaultValue || "";
|
|
8
|
+
}
|
|
9
|
+
export async function askPassword(question) {
|
|
10
|
+
return new Promise((resolve) => {
|
|
11
|
+
process.stdout.write(question + ": ");
|
|
12
|
+
const chars = [];
|
|
13
|
+
const cleanup = () => {
|
|
14
|
+
process.stdin.setRawMode(false);
|
|
15
|
+
process.stdin.pause();
|
|
16
|
+
process.stdin.removeAllListeners("data");
|
|
17
|
+
process.stdout.write("\n");
|
|
18
|
+
};
|
|
19
|
+
process.stdin.setRawMode(true);
|
|
20
|
+
process.stdin.resume();
|
|
21
|
+
process.stdin.setEncoding("utf-8");
|
|
22
|
+
process.stdin.on("data", (chunk) => {
|
|
23
|
+
for (const char of chunk) {
|
|
24
|
+
if (char === "\r" || char === "\n") {
|
|
25
|
+
cleanup();
|
|
26
|
+
resolve(chars.join(""));
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
else if (char === "\x7f" || char === "\b") {
|
|
30
|
+
if (chars.length > 0) {
|
|
31
|
+
chars.pop();
|
|
32
|
+
process.stdout.write("\b \b");
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
else if (char === "\x03") {
|
|
36
|
+
cleanup();
|
|
37
|
+
process.exit(0);
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
chars.push(char);
|
|
41
|
+
process.stdout.write("*");
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
export async function confirm(question, defaultYes = false) {
|
|
48
|
+
const hint = defaultYes ? "Y/n" : "y/N";
|
|
49
|
+
const answer = await ask(`${question} (${hint})`);
|
|
50
|
+
if (!answer)
|
|
51
|
+
return defaultYes;
|
|
52
|
+
return answer.toLowerCase().startsWith("y");
|
|
53
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@behalfid/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Official CLI for BehalfID — agent permission management",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"behalf": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"README.md"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsc -p tsconfig.json",
|
|
15
|
+
"dev": "tsc -p tsconfig.json --watch"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"commander": "^13.0.0"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@types/node": "^25.6.0",
|
|
22
|
+
"typescript": "^6.0.3"
|
|
23
|
+
},
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=18"
|
|
26
|
+
},
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "git+https://github.com/potatobeyonddefeat/behalf.git",
|
|
31
|
+
"directory": "packages/cli"
|
|
32
|
+
}
|
|
33
|
+
}
|