@invokehq/cli 0.1.15 → 0.2.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.
Files changed (3) hide show
  1. package/index.js +322 -47
  2. package/package.json +35 -35
  3. package/trace.js +5 -5
package/index.js CHANGED
@@ -1,39 +1,137 @@
1
1
  #!/usr/bin/env node
2
2
  const { Command } = require('commander');
3
+ const axios = require('axios');
3
4
  const chalk = require('chalk');
4
5
  const fs = require('fs-extra');
5
- const path = require('path');
6
- const pkg = require(path.resolve(__dirname, 'package.json'));
7
6
  const os = require('os');
7
+ const path = require('path');
8
8
  const readline = require('readline');
9
+ const pkg = require(path.resolve(__dirname, 'package.json'));
9
10
 
10
11
  const program = new Command();
12
+ const DEFAULT_BASE_URL = process.env.INVOKE_BASE_URL || process.env.AGENTGATE_API_URL || 'https://api.invokehq.run';
11
13
  const CONTEXT_DIR = path.join(process.cwd(), '.agentgate');
12
14
  const CONTEXT_FILE = path.join(CONTEXT_DIR, 'context.json');
13
- const CONFIG_FILE = path.join(os.homedir(), '.invoke', 'config.json');
15
+ const CONFIG_DIR = path.join(os.homedir(), '.invoke');
16
+ const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
17
+
18
+ async function readConfig() {
19
+ if (!(await fs.pathExists(CONFIG_FILE))) {
20
+ return {};
21
+ }
22
+ return fs.readJson(CONFIG_FILE);
23
+ }
24
+
25
+ async function writeConfig(config) {
26
+ await fs.ensureDir(CONFIG_DIR);
27
+ await fs.writeJson(CONFIG_FILE, config, { spaces: 2 });
28
+ await fs.chmod(CONFIG_FILE, 0o600).catch(() => {});
29
+ }
30
+
31
+ async function readContext() {
32
+ if (!(await fs.pathExists(CONTEXT_FILE))) {
33
+ return null;
34
+ }
35
+ return fs.readJson(CONTEXT_FILE);
36
+ }
37
+
38
+ async function promptSecret(query) {
39
+ if (!process.stdin.isTTY) {
40
+ return '';
41
+ }
14
42
 
15
- /**
16
- * Securely prompts for sensitive input (API Keys)
17
- */
18
- const PromptSecret = (query) => {
19
43
  return new Promise((resolve) => {
44
+ const mutableStdout = new (require('stream').Writable)({
45
+ write(chunk, encoding, callback) {
46
+ if (!this.muted) {
47
+ process.stdout.write(chunk, encoding);
48
+ }
49
+ callback();
50
+ }
51
+ });
52
+
20
53
  const rl = readline.createInterface({
21
54
  input: process.stdin,
22
- output: process.stdout
55
+ output: mutableStdout,
56
+ terminal: true
23
57
  });
24
-
25
- // Masking logic for the terminal
26
- const stdin = process.stdin;
27
- const onData = (char) => {
28
- if (char === '\n' || char === '\r' || char === '\u0004') stdin.removeListener('data', onData);
29
- };
30
-
31
- rl.question(chalk.cyan(query), (value) => {
58
+
59
+ mutableStdout.muted = false;
60
+ rl.question(chalk.cyan(query), (answer) => {
32
61
  rl.close();
33
- resolve(value);
62
+ process.stdout.write('\n');
63
+ resolve(answer.trim());
34
64
  });
65
+ mutableStdout.muted = true;
35
66
  });
36
- };
67
+ }
68
+
69
+ function maskKey(apiKey) {
70
+ if (!apiKey) {
71
+ return 'not set';
72
+ }
73
+ if (apiKey.length <= 10) {
74
+ return `${apiKey.slice(0, 4)}...`;
75
+ }
76
+ return `${apiKey.slice(0, 8)}...${apiKey.slice(-4)}`;
77
+ }
78
+
79
+ function parseJsonArg(value) {
80
+ if (!value) {
81
+ return {};
82
+ }
83
+ const source = value.startsWith('@')
84
+ ? fs.readFileSync(path.resolve(process.cwd(), value.slice(1)), 'utf8')
85
+ : value;
86
+ try {
87
+ const parsed = JSON.parse(source);
88
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
89
+ throw new Error('JSON value must be an object');
90
+ }
91
+ return parsed;
92
+ } catch (error) {
93
+ throw new Error(`Invalid JSON: ${error.message}`);
94
+ }
95
+ }
96
+
97
+ function printJson(data) {
98
+ console.log(JSON.stringify(data, null, 2));
99
+ }
100
+
101
+ async function runtimeRequest(method, route, { data, query, allowMissingKey = false } = {}) {
102
+ const config = await readConfig();
103
+ const baseUrl = (process.env.INVOKE_BASE_URL || process.env.AGENTGATE_API_URL || config.baseUrl || DEFAULT_BASE_URL).replace(/\/+$/, '');
104
+ const apiKey = process.env.INVOKE_API_KEY || process.env.AGENTGATE_API_KEY || process.env.TRACE_API_KEY || config.apiKey;
105
+
106
+ if (!apiKey && !allowMissingKey) {
107
+ throw new Error('Not logged in. Run "invoke login" first.');
108
+ }
109
+
110
+ try {
111
+ const response = await axios({
112
+ method,
113
+ url: `${baseUrl}${route}`,
114
+ params: query,
115
+ data,
116
+ timeout: 15000,
117
+ headers: {
118
+ 'User-Agent': `invoke-cli/${pkg.version}`,
119
+ 'Content-Type': 'application/json',
120
+ ...(apiKey ? { 'X-API-Key': apiKey } : {})
121
+ }
122
+ });
123
+ return { baseUrl, data: response.data };
124
+ } catch (error) {
125
+ if (error.response) {
126
+ const detail = error.response.data?.detail || error.response.data?.error || error.response.statusText;
127
+ throw new Error(`Invoke API ${error.response.status}: ${typeof detail === 'string' ? detail : JSON.stringify(detail)}`);
128
+ }
129
+ if (error.code === 'ECONNREFUSED') {
130
+ throw new Error(`Could not reach Invoke runtime at ${baseUrl}`);
131
+ }
132
+ throw error;
133
+ }
134
+ }
37
135
 
38
136
  function parseGitHubPath(input) {
39
137
  // Handles https://github.com/owner/repo and git@github.com:owner/repo
@@ -44,33 +142,84 @@ function parseGitHubPath(input) {
44
142
 
45
143
  program
46
144
  .name('invoke')
145
+ .alias('agentgate')
47
146
  .version(pkg.version);
48
147
 
49
148
  program
50
149
  .command('login')
51
150
  .description('Authenticate to your Invoke runtime')
52
- .option('--api-key <key>', 'Invoke API key for non-interactive login')
53
- .option('--base-url <url>', 'Invoke runtime URL', 'https://api.invokehq.run')
151
+ .option('--base-url <url>', 'Invoke runtime URL')
54
152
  .action(async (options) => {
55
- let apiKey = options.apiKey || process.env.INVOKE_API_KEY;
153
+ const baseUrl = (options.baseUrl || DEFAULT_BASE_URL).replace(/\/+$/, '');
154
+ let apiKey = process.env.INVOKE_API_KEY || process.env.AGENTGATE_API_KEY || process.env.TRACE_API_KEY;
56
155
 
57
156
  if (!apiKey) {
58
- apiKey = await PromptSecret('Enter your Invoke API Key: ');
157
+ console.log(chalk.dim(`Using Invoke runtime: ${baseUrl}`));
158
+ console.log(chalk.dim('Paste your Invoke API key. Input is hidden.'));
159
+ apiKey = await promptSecret('Invoke API key: ');
59
160
  }
60
161
 
61
162
  if (!apiKey) {
62
- console.error(chalk.red('Error: API key is required.'));
163
+ console.error(chalk.red('Error: no API key provided. Run "invoke login" or set INVOKE_API_KEY for automation.'));
63
164
  process.exit(1);
64
165
  }
65
166
 
66
- await fs.ensureDir(path.join(os.homedir(), '.invoke'));
67
- await fs.writeJson(path.join(os.homedir(), '.invoke', 'config.json'), {
68
- baseUrl: options.baseUrl.replace(/\/+$/, ''),
167
+ await writeConfig({
168
+ baseUrl,
69
169
  apiKey,
70
170
  updatedAt: new Date().toISOString()
71
- }, { spaces: 2 });
171
+ });
72
172
 
73
- console.log(`Logged in to Invoke (${options.baseUrl})`);
173
+ console.log(chalk.green(`✔ Logged in to Invoke (${baseUrl})`));
174
+ });
175
+
176
+ program
177
+ .command('logout')
178
+ .description('Remove saved Invoke credentials')
179
+ .action(async () => {
180
+ await fs.remove(CONFIG_FILE);
181
+ console.log(chalk.green('✔ Logged out'));
182
+ });
183
+
184
+ program
185
+ .command('status')
186
+ .description('Show login and project context')
187
+ .action(async () => {
188
+ const config = await readConfig();
189
+ const context = await readContext();
190
+ console.log(chalk.bold('Invoke CLI status'));
191
+ console.log(`Version: ${pkg.version}`);
192
+ console.log(`Runtime: ${config.baseUrl || DEFAULT_BASE_URL}`);
193
+ console.log(`API key: ${maskKey(process.env.INVOKE_API_KEY || process.env.AGENTGATE_API_KEY || process.env.TRACE_API_KEY || config.apiKey)}`);
194
+ console.log(`Config: ${await fs.pathExists(CONFIG_FILE) ? CONFIG_FILE : 'not saved'}`);
195
+ console.log(`Project: ${context ? `${context.provider}:${context.repoIdentifier}` : 'not wrapped'}`);
196
+ });
197
+
198
+ program
199
+ .command('config')
200
+ .description('Show or update CLI config')
201
+ .argument('[key]', 'Config key to set, currently only base-url')
202
+ .argument('[value]', 'Value for the config key')
203
+ .action(async (key, value) => {
204
+ const config = await readConfig();
205
+ if (!key) {
206
+ printJson({
207
+ baseUrl: config.baseUrl || DEFAULT_BASE_URL,
208
+ apiKey: maskKey(config.apiKey),
209
+ configFile: CONFIG_FILE
210
+ });
211
+ return;
212
+ }
213
+ if (key !== 'base-url') {
214
+ console.error(chalk.red('Error: only "base-url" can be set.'));
215
+ process.exit(1);
216
+ }
217
+ if (!value) {
218
+ console.error(chalk.red('Error: missing value.'));
219
+ process.exit(1);
220
+ }
221
+ await writeConfig({ ...config, baseUrl: value.replace(/\/+$/, ''), updatedAt: new Date().toISOString() });
222
+ console.log(chalk.green(`✔ base-url set to ${value.replace(/\/+$/, '')}`));
74
223
  });
75
224
 
76
225
  program
@@ -110,37 +259,163 @@ program
110
259
 
111
260
  program
112
261
  .command('upload')
113
- .description('Upload context to AgentGate')
262
+ .description('Mark local context as active')
114
263
  .action(async () => {
115
264
  if (!await fs.pathExists(CONTEXT_FILE)) {
116
- console.error(chalk.red('Error: No context found. Run "agentgate wrap github --path <url>" first.'));
265
+ console.error(chalk.red('Error: No context found. Run "invoke wrap github --path <url>" first.'));
117
266
  return;
118
267
  }
119
268
 
120
- let apiKey = process.env.AGENTGATE_API_KEY || process.env.TRACE_API_KEY;
121
-
122
- if (!apiKey && await fs.pathExists(CONFIG_FILE)) {
123
- const config = await fs.readJson(CONFIG_FILE);
124
- apiKey = config.apiKey;
125
- }
126
-
127
- if (!apiKey) {
128
- console.warn(chalk.yellow('⚠ Warning: No AGENTGATE_API_KEY found in environment. The backend may reject this context.'));
129
- }
130
-
131
269
  try {
132
270
  const context = await fs.readJson(CONTEXT_FILE);
133
- console.log(chalk.blue(`[AgentGate] Synchronizing context for ${chalk.bold(context.repoIdentifier)} with backend...`));
134
-
271
+ console.log(chalk.blue(`[Invoke] Activating context for ${chalk.bold(context.repoIdentifier)}...`));
135
272
  context.status = 'active';
136
273
  context.lastUploaded = new Date().toISOString();
137
-
138
274
  await fs.writeJson(CONTEXT_FILE, context, { spaces: 2 });
139
- await new Promise(r => setTimeout(r, 500)); // Simulating network latency
140
- console.log(chalk.green(`✔ Success: Repository context is now active.`));
275
+ console.log(chalk.green('✔ Context is active.'));
141
276
  } catch (err) {
142
277
  console.error(chalk.red('Upload failed:'), err.message);
143
278
  }
144
279
  });
145
280
 
146
- program.parse(process.argv);
281
+ program
282
+ .command('tools')
283
+ .description('List available tools')
284
+ .argument('[query]', 'Optional discovery query')
285
+ .option('--limit <number>', 'Maximum tools to return', '10')
286
+ .option('--json', 'Print raw JSON')
287
+ .action(async (query, options) => {
288
+ try {
289
+ const limit = Number.parseInt(options.limit, 10);
290
+ const route = query ? '/discover' : '/tools';
291
+ const { data } = await runtimeRequest('get', route, { query: query ? { q: query, limit } : undefined });
292
+ if (options.json) {
293
+ printJson(data);
294
+ return;
295
+ }
296
+ const tools = data.tools || [];
297
+ if (!tools.length) {
298
+ console.log(chalk.yellow('No tools found.'));
299
+ return;
300
+ }
301
+ for (const tool of tools) {
302
+ const key = tool.key || tool.name || tool.id;
303
+ const summary = tool.description || tool.summary || tool.capability_card?.description || '';
304
+ console.log(`${chalk.cyan(key)}${summary ? ` - ${summary}` : ''}`);
305
+ }
306
+ } catch (error) {
307
+ console.error(chalk.red(error.message));
308
+ process.exit(1);
309
+ }
310
+ });
311
+
312
+ program
313
+ .command('call')
314
+ .description('Call a tool through Invoke')
315
+ .argument('<tool>', 'Tool key, for example linear.create_issue')
316
+ .argument('[params]', 'JSON params object, or @file.json', '{}')
317
+ .option('--agent-id <id>', 'Agent identifier', 'cli')
318
+ .option('--idempotency-key <key>', 'Idempotency key for safe retries')
319
+ .option('--json', 'Print raw JSON')
320
+ .action(async (tool, paramsArg, options) => {
321
+ try {
322
+ const params = parseJsonArg(paramsArg);
323
+ const context = await readContext();
324
+ const body = {
325
+ tool,
326
+ params,
327
+ agent_id: options.agentId,
328
+ context: {
329
+ source: 'invoke-cli',
330
+ ...(context ? { project: context } : {})
331
+ }
332
+ };
333
+ if (options.idempotencyKey) {
334
+ body.idempotency_key = options.idempotencyKey;
335
+ }
336
+ const { data } = await runtimeRequest('post', '/call', { data: body });
337
+ if (options.json) {
338
+ printJson(data);
339
+ return;
340
+ }
341
+ console.log(chalk.green(`Status: ${data.status || (data.success ? 'success' : 'unknown')}`));
342
+ if (data.approval_id) {
343
+ console.log(chalk.yellow(`Approval required: ${data.approval_id}`));
344
+ }
345
+ if (data.latency_ms !== undefined) {
346
+ console.log(`Latency: ${data.latency_ms}ms`);
347
+ }
348
+ printJson(data.result || data);
349
+ } catch (error) {
350
+ console.error(chalk.red(error.message));
351
+ process.exit(1);
352
+ }
353
+ });
354
+
355
+ async function listApprovals(options = {}) {
356
+ const { data } = await runtimeRequest('get', '/approvals');
357
+ if (options.json) {
358
+ printJson(data);
359
+ return;
360
+ }
361
+ const limit = Number.parseInt(options.limit || '20', 10);
362
+ const items = (data.approvals || []).slice(0, limit);
363
+ if (!items.length) {
364
+ console.log(chalk.green('No pending approvals.'));
365
+ return;
366
+ }
367
+ for (const item of items) {
368
+ console.log(`${chalk.cyan(item.id)} ${item.status || ''} ${item.tool || item.tool_key || ''} ${item.reason || ''}`);
369
+ }
370
+ }
371
+
372
+ const approvals = program
373
+ .command('approvals')
374
+ .description('List and manage pending approvals')
375
+ .option('--limit <number>', 'Maximum approvals to show', '20')
376
+ .option('--json', 'Print raw JSON')
377
+ .action(async (options) => {
378
+ try {
379
+ await listApprovals(options);
380
+ } catch (error) {
381
+ console.error(chalk.red(error.message));
382
+ process.exit(1);
383
+ }
384
+ });
385
+
386
+ approvals
387
+ .command('list')
388
+ .description('List approvals')
389
+ .option('--limit <number>', 'Maximum approvals to show', '20')
390
+ .option('--json', 'Print raw JSON')
391
+ .action(async (options) => {
392
+ try {
393
+ await listApprovals(options);
394
+ } catch (error) {
395
+ console.error(chalk.red(error.message));
396
+ process.exit(1);
397
+ }
398
+ });
399
+
400
+ for (const action of ['approve', 'reject']) {
401
+ approvals
402
+ .command(`${action} <id>`)
403
+ .description(`${action[0].toUpperCase()}${action.slice(1)} an approval`)
404
+ .option('--json', 'Print raw JSON')
405
+ .action(async (id, options) => {
406
+ try {
407
+ const { data } = await runtimeRequest('post', `/approvals/${id}/${action}`, { data: {} });
408
+ if (options.json) {
409
+ printJson(data);
410
+ return;
411
+ }
412
+ console.log(chalk.green(`✔ ${action}d ${id}`));
413
+ printJson(data);
414
+ } catch (error) {
415
+ console.error(chalk.red(error.message));
416
+ process.exit(1);
417
+ }
418
+ });
419
+ }
420
+
421
+ program.parse(process.argv);
package/package.json CHANGED
@@ -1,35 +1,35 @@
1
- {
2
- "name": "@invokehq/cli",
3
- "version": "0.1.15",
4
- "description": "Invoke CLI",
5
- "main": "trace.js",
6
- "bin": {
7
- "invoke": "index.js",
8
- "agentgate": "index.js"
9
- },
10
- "scripts": {
11
- "build": "echo 'no build step'"
12
- },
13
- "files": [
14
- "index.js",
15
- "trace.js"
16
- ],
17
- "dependencies": {
18
- "axios": "^1.6.0",
19
- "axios-retry": "^4.5.0",
20
- "chalk": "^4.1.2",
21
- "commander": "^11.0.0",
22
- "dotenv": "^16.4.5",
23
- "fs-extra": "^11.1.1"
24
- },
25
- "engines": {
26
- "node": ">=16.0.0"
27
- },
28
- "repository": {
29
- "type": "git",
30
- "url": "git+https://github.com/joel4893/agentgate-cli.git"
31
- },
32
- "publishConfig": {
33
- "access": "public"
34
- }
35
- }
1
+ {
2
+ "name": "@invokehq/cli",
3
+ "version": "0.2.0",
4
+ "description": "Invoke CLI",
5
+ "main": "trace.js",
6
+ "bin": {
7
+ "invoke": "index.js",
8
+ "agentgate": "index.js"
9
+ },
10
+ "scripts": {
11
+ "build": "echo 'no build step'"
12
+ },
13
+ "files": [
14
+ "index.js",
15
+ "trace.js"
16
+ ],
17
+ "dependencies": {
18
+ "axios": "^1.6.0",
19
+ "axios-retry": "^4.5.0",
20
+ "chalk": "^4.1.2",
21
+ "commander": "^11.0.0",
22
+ "dotenv": "^16.4.5",
23
+ "fs-extra": "^11.1.1"
24
+ },
25
+ "engines": {
26
+ "node": ">=16.0.0"
27
+ },
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "git+https://github.com/joel4893/agentgate-cli.git"
31
+ },
32
+ "publishConfig": {
33
+ "access": "public"
34
+ }
35
+ }
package/trace.js CHANGED
@@ -9,6 +9,7 @@ require('dotenv').config({ path: path.resolve(process.cwd(), '.env') });
9
9
  // Load package info once at startup
10
10
  const pkg = require(path.resolve(__dirname, 'package.json'));
11
11
  const CONFIG_FILE = path.join(os.homedir(), '.invoke', 'config.json');
12
+ const DEFAULT_BASE_URL = process.env.INVOKE_BASE_URL || process.env.AGENTGATE_API_URL || "https://api.invokehq.run";
12
13
 
13
14
  const trace = {
14
15
  call: async (action, params) => {
@@ -26,11 +27,10 @@ const trace = {
26
27
  storedConfig = fs.readJsonSync(CONFIG_FILE);
27
28
  }
28
29
 
29
- // The SDK now points to the Agentgate Backend Service
30
- const baseUrl = process.env.AGENTGATE_API_URL || storedConfig.baseUrl || "http://localhost:8000";
30
+ const baseUrl = process.env.INVOKE_BASE_URL || process.env.AGENTGATE_API_URL || storedConfig.baseUrl || DEFAULT_BASE_URL;
31
31
  const endpoint = baseUrl.endsWith('/call') ? baseUrl : `${baseUrl.replace(/\/+$/, '')}/call`;
32
32
 
33
- const apiKey = process.env.AGENTGATE_API_KEY || storedConfig.apiKey;
33
+ const apiKey = process.env.INVOKE_API_KEY || process.env.AGENTGATE_API_KEY || process.env.TRACE_API_KEY || storedConfig.apiKey;
34
34
 
35
35
  try {
36
36
  const response = await axios.post(endpoint, {
@@ -40,7 +40,7 @@ const trace = {
40
40
  }, {
41
41
  timeout: 10000, // 10 second timeout
42
42
  headers: {
43
- 'User-Agent': `agentgate-cli-sdk/${pkg.version}`,
43
+ 'User-Agent': `invoke-cli-sdk/${pkg.version}`,
44
44
  'Content-Type': 'application/json',
45
45
  ...(apiKey ? { 'X-API-Key': apiKey } : {})
46
46
  }
@@ -59,4 +59,4 @@ const trace = {
59
59
  }
60
60
  };
61
61
 
62
- module.exports = { trace };
62
+ module.exports = { trace };