@techdigger/humanode-agentlink-cli 0.2.0 → 0.3.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/REGISTRATION.md +2 -2
- package/dist/commands/init.js +9 -8
- package/dist/commands/keystore.d.ts +3 -0
- package/dist/commands/keystore.js +37 -0
- package/dist/help.d.ts +1 -0
- package/dist/help.js +36 -11
- package/dist/index.d.ts +1 -0
- package/dist/index.js +58 -10
- package/dist/lib/core.d.ts +1 -0
- package/dist/lib/core.js +40 -1
- package/dist/lib/keystore.d.ts +5 -0
- package/dist/lib/keystore.js +81 -0
- package/dist/lib/telemetry.js +1 -1
- package/dist/version.d.ts +1 -0
- package/dist/version.js +4 -0
- package/package.json +2 -2
package/REGISTRATION.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Humanode Agentlink — Agent Developer Guide
|
|
2
2
|
|
|
3
3
|
Link your AI agent to a biomapped human and unlock free or discounted access to x402 APIs.
|
|
4
4
|
|
|
5
5
|
## What linking does
|
|
6
6
|
|
|
7
|
-
When your agent is linked to a biomapped owner, platforms using
|
|
7
|
+
When your agent is linked to a biomapped owner, platforms using Humanode Agentlink can identify your agent as acting on behalf of a real human. This unlocks better pricing: free tiers, free trials, or discounts that are unavailable to unlinked agents.
|
|
8
8
|
|
|
9
9
|
## Prerequisites
|
|
10
10
|
|
package/dist/commands/init.js
CHANGED
|
@@ -30,20 +30,21 @@ export async function runInit(options, depsInput = {}) {
|
|
|
30
30
|
const cwd = depsInput.cwd ?? process.cwd();
|
|
31
31
|
const env = depsInput.env ?? process.env;
|
|
32
32
|
const installDependencies = depsInput.installProjectDependencies ?? installProjectDependencies;
|
|
33
|
-
|
|
33
|
+
let _prompter = depsInput.prompter;
|
|
34
|
+
const getPrompter = () => (_prompter ??= createConsolePrompter());
|
|
34
35
|
try {
|
|
35
|
-
const rawDirectory = await resolveInteractiveValue(options.directory, options.yes, () => DEFAULT_PROJECT_DIR, () =>
|
|
36
|
+
const rawDirectory = await resolveInteractiveValue(options.directory, options.yes, () => DEFAULT_PROJECT_DIR, () => getPrompter().text({
|
|
36
37
|
message: 'Project directory',
|
|
37
38
|
defaultValue: DEFAULT_PROJECT_DIR,
|
|
38
39
|
}));
|
|
39
40
|
const targetDir = path.resolve(cwd, rawDirectory);
|
|
40
41
|
await ensureTargetDirectoryIsWritable(targetDir);
|
|
41
|
-
const template = await resolveInteractiveValue(options.template, options.yes, () => 'langchain', () =>
|
|
42
|
+
const template = await resolveInteractiveValue(options.template, options.yes, () => 'langchain', () => getPrompter().select({
|
|
42
43
|
message: 'Starter template',
|
|
43
44
|
options: TEMPLATE_NAMES.map(value => ({ value, label: value })),
|
|
44
45
|
defaultValue: 'langchain',
|
|
45
46
|
}));
|
|
46
|
-
const network = await resolveInteractiveValue(options.network, options.yes, () => resolveNetwork(undefined, env), () =>
|
|
47
|
+
const network = await resolveInteractiveValue(options.network, options.yes, () => resolveNetwork(undefined, env), () => getPrompter().select({
|
|
47
48
|
message: 'Biomapper network',
|
|
48
49
|
options: [
|
|
49
50
|
{ value: 'base-sepolia', label: 'base-sepolia' },
|
|
@@ -51,11 +52,11 @@ export async function runInit(options, depsInput = {}) {
|
|
|
51
52
|
],
|
|
52
53
|
defaultValue: resolveNetwork(undefined, env),
|
|
53
54
|
}));
|
|
54
|
-
const registry = await resolveInteractiveValue(options.registry, options.yes, () => env.BIOMAPPER_REGISTRY ?? DEFAULT_REGISTRY, async () => parseAddressOrPlaceholder('registry', await
|
|
55
|
+
const registry = await resolveInteractiveValue(options.registry, options.yes, () => env.BIOMAPPER_REGISTRY ?? DEFAULT_REGISTRY, async () => parseAddressOrPlaceholder('registry', await getPrompter().text({
|
|
55
56
|
message: 'Biomapper registry address',
|
|
56
57
|
defaultValue: env.BIOMAPPER_REGISTRY ?? DEFAULT_REGISTRY,
|
|
57
58
|
}), DEFAULT_REGISTRY));
|
|
58
|
-
const packageManager = await resolveInteractiveValue(options.packageManager, options.yes, () => 'npm', () =>
|
|
59
|
+
const packageManager = await resolveInteractiveValue(options.packageManager, options.yes, () => 'npm', () => getPrompter().select({
|
|
59
60
|
message: 'Package manager',
|
|
60
61
|
options: PACKAGE_MANAGERS.map(value => ({ value, label: value })),
|
|
61
62
|
defaultValue: 'npm',
|
|
@@ -65,7 +66,7 @@ export async function runInit(options, depsInput = {}) {
|
|
|
65
66
|
? true
|
|
66
67
|
: options.yes
|
|
67
68
|
? false
|
|
68
|
-
: await
|
|
69
|
+
: await getPrompter().confirm({
|
|
69
70
|
message: 'Install dependencies now',
|
|
70
71
|
defaultValue: true,
|
|
71
72
|
});
|
|
@@ -102,7 +103,7 @@ export async function runInit(options, depsInput = {}) {
|
|
|
102
103
|
}
|
|
103
104
|
finally {
|
|
104
105
|
if (!depsInput.prompter) {
|
|
105
|
-
|
|
106
|
+
_prompter?.close();
|
|
106
107
|
}
|
|
107
108
|
}
|
|
108
109
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { privateKeyToAccount } from 'viem/accounts';
|
|
2
|
+
import { CliError, normalizePrivateKey, readPassword } from '../lib/core.js';
|
|
3
|
+
import { decryptKeystore, deleteKeystore, encryptAndSave, keystoreExists, keystorePath } from '../lib/keystore.js';
|
|
4
|
+
export async function runKeystoreSet() {
|
|
5
|
+
const exists = await keystoreExists();
|
|
6
|
+
if (exists) {
|
|
7
|
+
throw new CliError(`A keystore already exists at ${keystorePath()}.\nRun "agentlink keystore delete" first to replace it.`);
|
|
8
|
+
}
|
|
9
|
+
const rawKey = await readPassword('Agent private key (hidden): ');
|
|
10
|
+
if (!rawKey.trim()) {
|
|
11
|
+
throw new CliError('No private key provided.');
|
|
12
|
+
}
|
|
13
|
+
const privateKey = normalizePrivateKey(rawKey.trim());
|
|
14
|
+
const address = privateKeyToAccount(privateKey).address;
|
|
15
|
+
const password = await readPassword('Keystore password (hidden): ');
|
|
16
|
+
if (!password) {
|
|
17
|
+
throw new CliError('Password cannot be empty.');
|
|
18
|
+
}
|
|
19
|
+
const confirm = await readPassword('Confirm password (hidden): ');
|
|
20
|
+
if (password !== confirm) {
|
|
21
|
+
throw new CliError('Passwords do not match.');
|
|
22
|
+
}
|
|
23
|
+
await encryptAndSave(privateKey, password);
|
|
24
|
+
return address;
|
|
25
|
+
}
|
|
26
|
+
export async function runKeystoreDelete() {
|
|
27
|
+
const exists = await keystoreExists();
|
|
28
|
+
if (!exists) {
|
|
29
|
+
throw new CliError('No keystore found.');
|
|
30
|
+
}
|
|
31
|
+
await deleteKeystore();
|
|
32
|
+
}
|
|
33
|
+
export async function runKeystoreAddress() {
|
|
34
|
+
const password = await readPassword('Keystore password (hidden): ');
|
|
35
|
+
const privateKey = normalizePrivateKey(await decryptKeystore(password));
|
|
36
|
+
return privateKeyToAccount(privateKey).address;
|
|
37
|
+
}
|
package/dist/help.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export declare function getRootHelpText(): string;
|
|
2
|
+
export declare function getKeystoreHelpText(): string;
|
|
2
3
|
export declare function getAuthorizeLinkHelpText(): string;
|
|
3
4
|
export declare function getStatusHelpText(): string;
|
|
4
5
|
export declare function getLinkHelpText(): string;
|
package/dist/help.js
CHANGED
|
@@ -2,19 +2,21 @@ export function getRootHelpText() {
|
|
|
2
2
|
return `Usage: agentlink <command> [options]
|
|
3
3
|
|
|
4
4
|
Commands:
|
|
5
|
+
keystore Manage the encrypted agent private key keystore
|
|
5
6
|
init Scaffold a Biomapper-ready agent starter
|
|
6
|
-
link Generate consent and open a
|
|
7
|
+
link Generate consent and open a linker URL for owner approval
|
|
7
8
|
authorize-link Generate a one-time EIP-712 consent blob for manual linking
|
|
8
9
|
status Check whether an agent is linked and active
|
|
9
10
|
|
|
11
|
+
Flags:
|
|
12
|
+
--help, -h Show this help text
|
|
13
|
+
|
|
10
14
|
Run agentlink <command> --help for command-specific options.
|
|
11
15
|
|
|
12
16
|
Environment:
|
|
13
|
-
AGENT_PRIVATE_KEY Local/dev secret source; prefer stdin or secret managers in production
|
|
14
17
|
BIOMAPPER_NETWORK Default network (base | base-sepolia)
|
|
15
18
|
BIOMAPPER_REGISTRY Default BiomapperAgentRegistry address
|
|
16
19
|
BIOMAPPER_RPC_URL Optional RPC URL override
|
|
17
|
-
BIOMAPPER_LINKER_URL Default linker URL (defaults to http://localhost:5173/)
|
|
18
20
|
AGENTLINK_TELEMETRY Optional override for the saved CLI telemetry choice (on | off)
|
|
19
21
|
AGENTLINK_POSTHOG_KEY Optional PostHog API key for CLI telemetry
|
|
20
22
|
AGENTLINK_POSTHOG_HOST Optional PostHog host override (defaults to https://us.i.posthog.com)
|
|
@@ -23,6 +25,25 @@ Environment:
|
|
|
23
25
|
AGENTLINK_CONTENT_ID Optional content identifier for telemetry events
|
|
24
26
|
`;
|
|
25
27
|
}
|
|
28
|
+
export function getKeystoreHelpText() {
|
|
29
|
+
return `Usage: agentlink keystore <subcommand>
|
|
30
|
+
|
|
31
|
+
Description:
|
|
32
|
+
Manage an AES-256-GCM encrypted keystore for your agent private key.
|
|
33
|
+
The key is stored at ~/.agentlink/keystore.json, readable only by your user.
|
|
34
|
+
All commands that need the private key unlock it automatically at runtime.
|
|
35
|
+
|
|
36
|
+
Subcommands:
|
|
37
|
+
set Store your agent private key (prompts for key + password, never logged)
|
|
38
|
+
delete Remove the keystore
|
|
39
|
+
address Show the agent address derived from the stored key (prompts for password)
|
|
40
|
+
|
|
41
|
+
Examples:
|
|
42
|
+
agentlink keystore set
|
|
43
|
+
agentlink keystore address
|
|
44
|
+
agentlink keystore delete
|
|
45
|
+
`;
|
|
46
|
+
}
|
|
26
47
|
export function getAuthorizeLinkHelpText() {
|
|
27
48
|
return `Usage:
|
|
28
49
|
agentlink authorize-link --owner <address> [options]
|
|
@@ -36,10 +57,12 @@ Options:
|
|
|
36
57
|
--registry BiomapperAgentRegistry contract address (falls back to BIOMAPPER_REGISTRY)
|
|
37
58
|
--deadline Unix timestamp in seconds for signature expiry
|
|
38
59
|
--network base | base-sepolia (default: BIOMAPPER_NETWORK or base-sepolia)
|
|
39
|
-
--private-key
|
|
40
|
-
--private-key-
|
|
60
|
+
--private-key-stdin Read the agent private key from stdin (for CI/secret managers)
|
|
61
|
+
--private-key Dev-only: pass key directly as 0x-prefixed hex. Prefer keystore or stdin.
|
|
41
62
|
--rpc-url Optional RPC URL override for the selected network
|
|
42
63
|
--help Show this help message
|
|
64
|
+
|
|
65
|
+
Key resolution order: keystore (default) → --private-key-stdin → --private-key → AGENT_PRIVATE_KEY env
|
|
43
66
|
`;
|
|
44
67
|
}
|
|
45
68
|
export function getStatusHelpText() {
|
|
@@ -52,13 +75,15 @@ Description:
|
|
|
52
75
|
|
|
53
76
|
Options:
|
|
54
77
|
--registry BiomapperAgentRegistry contract address (falls back to BIOMAPPER_REGISTRY)
|
|
55
|
-
--agent Agent wallet address to check
|
|
78
|
+
--agent Agent wallet address to check (skips key lookup entirely)
|
|
56
79
|
--network base | base-sepolia (default: BIOMAPPER_NETWORK or base-sepolia)
|
|
57
|
-
--private-key
|
|
58
|
-
--private-key-
|
|
80
|
+
--private-key-stdin Read the agent private key from stdin (for CI/secret managers)
|
|
81
|
+
--private-key Dev-only: pass key directly as 0x-prefixed hex
|
|
59
82
|
--rpc-url Optional RPC URL override for the selected network
|
|
60
83
|
--json Output raw JSON instead of human-readable text
|
|
61
84
|
--help Show this help message
|
|
85
|
+
|
|
86
|
+
Key resolution order: keystore (default) → --private-key-stdin → --private-key → AGENT_PRIVATE_KEY env
|
|
62
87
|
`;
|
|
63
88
|
}
|
|
64
89
|
export function getLinkHelpText() {
|
|
@@ -74,10 +99,10 @@ Options:
|
|
|
74
99
|
--registry BiomapperAgentRegistry contract address (falls back to BIOMAPPER_REGISTRY)
|
|
75
100
|
--network base | base-sepolia (default: BIOMAPPER_NETWORK or base-sepolia)
|
|
76
101
|
--deadline Unix timestamp in seconds for signature and session expiry (default: 24 hours from now)
|
|
77
|
-
--private-key
|
|
78
|
-
--private-key-
|
|
102
|
+
--private-key-stdin Read the agent private key from stdin (for CI/secret managers)
|
|
103
|
+
--private-key Dev-only: pass key directly as 0x-prefixed hex
|
|
79
104
|
--rpc-url Optional RPC URL override for the selected network
|
|
80
|
-
--linker-url Linker base URL (default:
|
|
105
|
+
--linker-url Linker base URL override (default: https://agent-kit-biomapper.vercel.app/)
|
|
81
106
|
--open Open the generated linker URL in the default browser
|
|
82
107
|
--json Output consent, session, and linker URL as JSON
|
|
83
108
|
--help Show this help message
|
package/dist/index.d.ts
CHANGED
|
@@ -12,5 +12,6 @@ export interface CliRuntime {
|
|
|
12
12
|
buildLinkUrl: (baseUrl: string, session: Awaited<ReturnType<typeof createLinkSession>>) => string;
|
|
13
13
|
openUrl: (url: string) => Promise<void>;
|
|
14
14
|
readStdin: () => Promise<string>;
|
|
15
|
+
readPassword: (prompt: string) => Promise<string>;
|
|
15
16
|
}
|
|
16
17
|
export declare function runCli(argv?: string[], overrides?: Partial<CliRuntime>): Promise<number>;
|
package/dist/index.js
CHANGED
|
@@ -6,8 +6,11 @@ import { formatInitOutput, parseInitOptions, runInit } from './commands/init.js'
|
|
|
6
6
|
import { formatLinkOutput, parseLinkOptions, runLink } from './commands/link.js';
|
|
7
7
|
import { formatStatusOutput, parseStatusOptions, runStatus } from './commands/status.js';
|
|
8
8
|
import { parseAuthorizeLinkOptions, runAuthorizeLink } from './commands/authorize-link.js';
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
9
|
+
import { runKeystoreAddress, runKeystoreDelete, runKeystoreSet } from './commands/keystore.js';
|
|
10
|
+
import { CliError, formatJson, loadEnvFiles, openUrlInBrowser, parseFlags, readAllFromStdin, readPassword } from './lib/core.js';
|
|
11
|
+
import { decryptKeystore, keystoreExists, keystorePath } from './lib/keystore.js';
|
|
12
|
+
import { getAuthorizeLinkHelpText, getInitHelpText, getKeystoreHelpText, getLinkHelpText, getRootHelpText, getStatusHelpText, } from './help.js';
|
|
13
|
+
import { VERSION } from './version.js';
|
|
11
14
|
import { buildEmbeddedHostedLinkUrl, createAgentLinkConsent, createBiomapperQueryClient, createLinkSession, } from '@techdigger/humanode-agentlink';
|
|
12
15
|
function writeLine(write, text) {
|
|
13
16
|
write(text.endsWith('\n') ? text : `${text}\n`);
|
|
@@ -30,6 +33,7 @@ export async function runCli(argv = process.argv.slice(2), overrides = {}) {
|
|
|
30
33
|
buildLinkUrl: overrides.buildLinkUrl ?? buildEmbeddedHostedLinkUrl,
|
|
31
34
|
openUrl: overrides.openUrl ?? openUrlInBrowser,
|
|
32
35
|
readStdin: overrides.readStdin ?? readAllFromStdin,
|
|
36
|
+
readPassword: overrides.readPassword ?? readPassword,
|
|
33
37
|
};
|
|
34
38
|
const [command, ...args] = argv;
|
|
35
39
|
try {
|
|
@@ -37,7 +41,42 @@ export async function runCli(argv = process.argv.slice(2), overrides = {}) {
|
|
|
37
41
|
writeLine(runtime.stdout, getRootHelpText());
|
|
38
42
|
return 0;
|
|
39
43
|
}
|
|
44
|
+
if (command === '--version' || command === '-v') {
|
|
45
|
+
writeLine(runtime.stdout, VERSION);
|
|
46
|
+
return 0;
|
|
47
|
+
}
|
|
40
48
|
switch (command) {
|
|
49
|
+
case 'keystore': {
|
|
50
|
+
const [subcommand, ...subArgs] = args;
|
|
51
|
+
if (!subcommand || subcommand === '--help' || subcommand === '-h') {
|
|
52
|
+
writeLine(runtime.stdout, getKeystoreHelpText());
|
|
53
|
+
return 0;
|
|
54
|
+
}
|
|
55
|
+
switch (subcommand) {
|
|
56
|
+
case 'set': {
|
|
57
|
+
if (subArgs.includes('--help') || subArgs.includes('-h')) {
|
|
58
|
+
writeLine(runtime.stdout, getKeystoreHelpText());
|
|
59
|
+
return 0;
|
|
60
|
+
}
|
|
61
|
+
const address = await runKeystoreSet();
|
|
62
|
+
writeLine(runtime.stdout, `Keystore saved at ${keystorePath()}`);
|
|
63
|
+
writeLine(runtime.stdout, `Agent address: ${address}`);
|
|
64
|
+
return 0;
|
|
65
|
+
}
|
|
66
|
+
case 'delete': {
|
|
67
|
+
await runKeystoreDelete();
|
|
68
|
+
writeLine(runtime.stdout, 'Keystore deleted.');
|
|
69
|
+
return 0;
|
|
70
|
+
}
|
|
71
|
+
case 'address': {
|
|
72
|
+
const address = await runKeystoreAddress();
|
|
73
|
+
writeLine(runtime.stdout, `Agent address: ${address}`);
|
|
74
|
+
return 0;
|
|
75
|
+
}
|
|
76
|
+
default:
|
|
77
|
+
throw new CliError(`Unknown keystore subcommand "${subcommand}". Run "agentlink keystore --help" for available subcommands.`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
41
80
|
case 'authorize-link':
|
|
42
81
|
if (args.includes('--help') || args.includes('-h')) {
|
|
43
82
|
writeLine(runtime.stdout, getAuthorizeLinkHelpText());
|
|
@@ -155,17 +194,26 @@ export async function runCli(argv = process.argv.slice(2), overrides = {}) {
|
|
|
155
194
|
}
|
|
156
195
|
async function readPrivateKeyFromStdin(argv, runtime) {
|
|
157
196
|
const parsed = parseFlags(argv);
|
|
158
|
-
if (
|
|
159
|
-
|
|
197
|
+
if (parsed.flags.has('private-key-stdin')) {
|
|
198
|
+
if (parsed.values['private-key']) {
|
|
199
|
+
throw new CliError('Pass either --private-key or --private-key-stdin, not both.');
|
|
200
|
+
}
|
|
201
|
+
const privateKey = (await runtime.readStdin()).trim();
|
|
202
|
+
if (!privateKey) {
|
|
203
|
+
throw new CliError('Missing agent private key on stdin. Pipe a 32-byte hex key into --private-key-stdin.');
|
|
204
|
+
}
|
|
205
|
+
return privateKey;
|
|
160
206
|
}
|
|
161
|
-
|
|
162
|
-
|
|
207
|
+
// If an explicit key source is already present, let resolvePrivateKey handle it
|
|
208
|
+
if (parsed.values['private-key'] || runtime.env.AGENT_PRIVATE_KEY) {
|
|
209
|
+
return undefined;
|
|
163
210
|
}
|
|
164
|
-
|
|
165
|
-
if (
|
|
166
|
-
|
|
211
|
+
// Fall back to keystore
|
|
212
|
+
if (await keystoreExists()) {
|
|
213
|
+
const password = await runtime.readPassword('Keystore password: ');
|
|
214
|
+
return await decryptKeystore(password);
|
|
167
215
|
}
|
|
168
|
-
return
|
|
216
|
+
return undefined;
|
|
169
217
|
}
|
|
170
218
|
function isExecutedDirectly() {
|
|
171
219
|
if (!process.argv[1]) {
|
package/dist/lib/core.d.ts
CHANGED
|
@@ -67,6 +67,7 @@ export declare function parseAddressOrPlaceholder(name: string, value: string |
|
|
|
67
67
|
export declare function resolveRegistryAddress(value: string | undefined, env: EnvSource): Address;
|
|
68
68
|
export declare function resolveRpcUrl(value: string | undefined, env: EnvSource): string | undefined;
|
|
69
69
|
export declare function resolveLinkerUrl(value: string | undefined, env: EnvSource): string;
|
|
70
|
+
export declare function readPassword(prompt: string): Promise<string>;
|
|
70
71
|
export declare function normalizePrivateKey(value: string | undefined): Hex;
|
|
71
72
|
export declare function resolvePrivateKey(value: string | undefined, env: EnvSource, override?: string): Hex;
|
|
72
73
|
export declare function deriveAgentAddress(privateKey: Hex): Address;
|
package/dist/lib/core.js
CHANGED
|
@@ -134,7 +134,46 @@ export function resolveRpcUrl(value, env) {
|
|
|
134
134
|
return value ?? env.BIOMAPPER_RPC_URL;
|
|
135
135
|
}
|
|
136
136
|
export function resolveLinkerUrl(value, env) {
|
|
137
|
-
return value ?? env.BIOMAPPER_LINKER_URL ?? '
|
|
137
|
+
return value ?? env.BIOMAPPER_LINKER_URL ?? 'https://agent-kit-biomapper.vercel.app/';
|
|
138
|
+
}
|
|
139
|
+
export function readPassword(prompt) {
|
|
140
|
+
return new Promise((resolve, reject) => {
|
|
141
|
+
if (!process.stdin.isTTY) {
|
|
142
|
+
reject(new CliError('Cannot read keystore password: not a TTY. Set AGENT_PRIVATE_KEY or use --private-key-stdin in non-interactive environments.'));
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
process.stdout.write(prompt);
|
|
146
|
+
process.stdin.setRawMode(true);
|
|
147
|
+
process.stdin.resume();
|
|
148
|
+
process.stdin.setEncoding('utf8');
|
|
149
|
+
let password = '';
|
|
150
|
+
const onData = (char) => {
|
|
151
|
+
switch (char) {
|
|
152
|
+
case '\n':
|
|
153
|
+
case '\r':
|
|
154
|
+
process.stdin.setRawMode(false);
|
|
155
|
+
process.stdin.pause();
|
|
156
|
+
process.stdin.removeListener('data', onData);
|
|
157
|
+
process.stdout.write('\n');
|
|
158
|
+
resolve(password);
|
|
159
|
+
break;
|
|
160
|
+
case '\u0003':
|
|
161
|
+
// Ctrl+C
|
|
162
|
+
process.stdin.setRawMode(false);
|
|
163
|
+
process.stdin.pause();
|
|
164
|
+
process.stdin.removeListener('data', onData);
|
|
165
|
+
reject(new CliError('Cancelled.'));
|
|
166
|
+
break;
|
|
167
|
+
case '\u007F':
|
|
168
|
+
// Backspace
|
|
169
|
+
password = password.slice(0, -1);
|
|
170
|
+
break;
|
|
171
|
+
default:
|
|
172
|
+
password += char;
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
process.stdin.on('data', onData);
|
|
176
|
+
});
|
|
138
177
|
}
|
|
139
178
|
export function normalizePrivateKey(value) {
|
|
140
179
|
if (!value) {
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare function keystorePath(): string;
|
|
2
|
+
export declare function keystoreExists(): Promise<boolean>;
|
|
3
|
+
export declare function encryptAndSave(privateKey: string, password: string): Promise<void>;
|
|
4
|
+
export declare function decryptKeystore(password: string): Promise<string>;
|
|
5
|
+
export declare function deleteKeystore(): Promise<void>;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from 'node:crypto';
|
|
2
|
+
import { mkdir, readFile, unlink, writeFile } from 'node:fs/promises';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
const KEYSTORE_VERSION = 1;
|
|
6
|
+
const SCRYPT_N = 16384;
|
|
7
|
+
const SCRYPT_R = 8;
|
|
8
|
+
const SCRYPT_P = 1;
|
|
9
|
+
const KEY_LEN = 32;
|
|
10
|
+
export function keystorePath() {
|
|
11
|
+
return path.join(os.homedir(), '.agentlink', 'keystore.json');
|
|
12
|
+
}
|
|
13
|
+
export async function keystoreExists() {
|
|
14
|
+
try {
|
|
15
|
+
await readFile(keystorePath());
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function deriveKey(password, salt) {
|
|
23
|
+
return scryptSync(password, salt, KEY_LEN, { N: SCRYPT_N, r: SCRYPT_R, p: SCRYPT_P });
|
|
24
|
+
}
|
|
25
|
+
export async function encryptAndSave(privateKey, password) {
|
|
26
|
+
const salt = randomBytes(32);
|
|
27
|
+
const iv = randomBytes(12);
|
|
28
|
+
const key = deriveKey(password, salt);
|
|
29
|
+
const cipher = createCipheriv('aes-256-gcm', key, iv);
|
|
30
|
+
const ciphertext = Buffer.concat([cipher.update(privateKey, 'utf8'), cipher.final()]);
|
|
31
|
+
const tag = cipher.getAuthTag();
|
|
32
|
+
const data = {
|
|
33
|
+
version: KEYSTORE_VERSION,
|
|
34
|
+
salt: salt.toString('hex'),
|
|
35
|
+
iv: iv.toString('hex'),
|
|
36
|
+
tag: tag.toString('hex'),
|
|
37
|
+
ciphertext: ciphertext.toString('hex'),
|
|
38
|
+
};
|
|
39
|
+
const filePath = keystorePath();
|
|
40
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
41
|
+
await writeFile(filePath, JSON.stringify(data, null, 2), { mode: 0o600 });
|
|
42
|
+
}
|
|
43
|
+
export async function decryptKeystore(password) {
|
|
44
|
+
let raw;
|
|
45
|
+
try {
|
|
46
|
+
raw = await readFile(keystorePath(), 'utf8');
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
throw new Error('No keystore found. Run "agentlink keystore set" first.');
|
|
50
|
+
}
|
|
51
|
+
let data;
|
|
52
|
+
try {
|
|
53
|
+
data = JSON.parse(raw);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
throw new Error('Keystore file is corrupted.');
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
const salt = Buffer.from(data.salt, 'hex');
|
|
60
|
+
const iv = Buffer.from(data.iv, 'hex');
|
|
61
|
+
const tag = Buffer.from(data.tag, 'hex');
|
|
62
|
+
const ciphertext = Buffer.from(data.ciphertext, 'hex');
|
|
63
|
+
const key = deriveKey(password, salt);
|
|
64
|
+
const decipher = createDecipheriv('aes-256-gcm', key, iv);
|
|
65
|
+
decipher.setAuthTag(tag);
|
|
66
|
+
return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString('utf8');
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
throw new Error('Wrong password or corrupted keystore.');
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
export async function deleteKeystore() {
|
|
73
|
+
try {
|
|
74
|
+
await unlink(keystorePath());
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
if (error.code !== 'ENOENT') {
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
package/dist/lib/telemetry.js
CHANGED
|
@@ -55,7 +55,7 @@ async function promptForConsent(anonymousId, filePath = TELEMETRY_CONFIG_PATH) {
|
|
|
55
55
|
const prompter = createConsolePrompter();
|
|
56
56
|
try {
|
|
57
57
|
const accepted = await prompter.confirm({
|
|
58
|
-
message: 'Share anonymous CLI usage telemetry to improve
|
|
58
|
+
message: 'Share anonymous CLI usage telemetry to improve Humanode Agentlink? Override anytime with AGENTLINK_TELEMETRY=on|off',
|
|
59
59
|
defaultValue: false,
|
|
60
60
|
});
|
|
61
61
|
const record = {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const VERSION: string;
|
package/dist/version.js
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@techdigger/humanode-agentlink-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Scaffold, link, and inspect Biomapper-ready agent projects.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"cli": "tsx src/index.ts"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@techdigger/humanode-agentlink": "^0.
|
|
26
|
+
"@techdigger/humanode-agentlink": "^0.1.0",
|
|
27
27
|
"posthog-node": "^5.21.2",
|
|
28
28
|
"viem": "^2.46.2"
|
|
29
29
|
},
|