@carto-knowledge/runner 0.2.3 → 0.2.5

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/src/envelope.ts DELETED
@@ -1,62 +0,0 @@
1
- // ABOUTME: Wraps CLI command outputs in structured JSON envelopes
2
- // ABOUTME: Provides consistent response format for Mastra tool integration
3
-
4
- export interface CommandError {
5
- code: string;
6
- message: string;
7
- }
8
-
9
- export interface CommandOutput<T = unknown> {
10
- ok: boolean;
11
- command: string;
12
- result?: T;
13
- error?: CommandError;
14
- requestId?: string;
15
- schemaVersion: 1;
16
- }
17
-
18
- /**
19
- * Creates a successful output envelope.
20
- */
21
- export function successEnvelope<T>(
22
- command: string,
23
- result: T,
24
- requestId?: string
25
- ): CommandOutput<T> {
26
- return {
27
- ok: true,
28
- command,
29
- result,
30
- requestId,
31
- schemaVersion: 1,
32
- };
33
- }
34
-
35
- /**
36
- * Creates an error output envelope.
37
- */
38
- export function errorEnvelope(
39
- command: string,
40
- error: CommandError,
41
- requestId?: string
42
- ): CommandOutput<never> {
43
- return {
44
- ok: false,
45
- command,
46
- error,
47
- requestId,
48
- schemaVersion: 1,
49
- };
50
- }
51
-
52
- /**
53
- * Common error codes for CLI operations.
54
- */
55
- export const ErrorCodes = {
56
- COMMAND_NOT_ALLOWED: 'COMMAND_NOT_ALLOWED',
57
- COMMAND_NOT_FOUND: 'COMMAND_NOT_FOUND',
58
- INVALID_ARGUMENTS: 'INVALID_ARGUMENTS',
59
- AUTH_ERROR: 'AUTH_ERROR',
60
- EXECUTION_ERROR: 'EXECUTION_ERROR',
61
- PARSE_ERROR: 'PARSE_ERROR',
62
- } as const;
package/src/index.ts DELETED
@@ -1,12 +0,0 @@
1
- // ABOUTME: Public API for @carto/runner package
2
- // ABOUTME: Exports in-process runner and supporting utilities
3
-
4
- export { runInProcess } from './runner';
5
- export type { RunOptions } from './runner';
6
- export type { CommandOutput } from './envelope';
7
- export { successEnvelope, errorEnvelope, ErrorCodes } from './envelope';
8
- export { tokenize, extractCommandPath } from './tokenizer';
9
- export { checkAllowlist, DEFAULT_ALLOWLIST, WRITE_ALLOWLIST } from './allowlist';
10
- export type { AllowlistConfig } from './allowlist';
11
- export { WorkerRuntime, CliRuntime } from './runtime';
12
- export type { CartoRuntime } from './runtime';
@@ -1,95 +0,0 @@
1
- // ABOUTME: Tests for in-process CLI runner
2
- // ABOUTME: Validates command execution without subprocess spawning
3
-
4
- import { describe, it, expect } from 'bun:test';
5
- import { runInProcess } from './runner';
6
-
7
- describe('runInProcess', () => {
8
- const baseOptions = {
9
- authToken: 'test-token',
10
- apiBaseUrl: 'https://api.carto.so',
11
- };
12
-
13
- it('returns error for empty command', async () => {
14
- const result = await runInProcess('', baseOptions);
15
- expect(result.ok).toBe(false);
16
- expect(result.error?.code).toBe('INVALID_ARGUMENTS');
17
- });
18
-
19
- it('returns error for whitespace-only command', async () => {
20
- const result = await runInProcess(' ', baseOptions);
21
- expect(result.ok).toBe(false);
22
- expect(result.error?.code).toBe('INVALID_ARGUMENTS');
23
- });
24
-
25
- it('returns error for missing auth token', async () => {
26
- const result = await runInProcess('folder tree', {
27
- ...baseOptions,
28
- authToken: '',
29
- });
30
- expect(result.ok).toBe(false);
31
- expect(result.error?.code).toBe('AUTH_ERROR');
32
- });
33
-
34
- it('returns error for disallowed command', async () => {
35
- const result = await runInProcess('admin delete-all', {
36
- ...baseOptions,
37
- allowlist: { commands: ['folder tree'] },
38
- });
39
- expect(result.ok).toBe(false);
40
- expect(result.error?.code).toBe('COMMAND_NOT_ALLOWED');
41
- expect(result.error?.message).toContain('admin delete-all');
42
- });
43
-
44
- it('includes requestId in response', async () => {
45
- const result = await runInProcess('folder tree', {
46
- ...baseOptions,
47
- requestId: 'req-123',
48
- allowlist: { commands: ['folder tree'] },
49
- });
50
- expect(result.requestId).toBe('req-123');
51
- });
52
-
53
- it('extracts correct command path from disallowed command error', async () => {
54
- // Test with a disallowed command to verify path extraction without network
55
- const result = await runInProcess('folder tree --library test', {
56
- ...baseOptions,
57
- allowlist: { commands: ['item search'] }, // folder tree not allowed
58
- });
59
- expect(result.command).toBe('folder tree');
60
- });
61
-
62
- it('includes schemaVersion in error response', async () => {
63
- const result = await runInProcess('folder tree', {
64
- ...baseOptions,
65
- allowlist: { commands: [] }, // Empty allowlist to trigger error
66
- });
67
- expect(result.schemaVersion).toBe(1);
68
- });
69
-
70
- it('extracts folder restructure command path from disallowed command error', async () => {
71
- const result = await runInProcess(
72
- 'folder restructure --library 00000000-0000-0000-0000-000000000001 --item-folder-map "{\\"item-1\\":\\"folder-a\\"}"',
73
- {
74
- ...baseOptions,
75
- allowlist: { commands: ['folder tree'] },
76
- }
77
- );
78
-
79
- expect(result.ok).toBe(false);
80
- expect(result.command).toBe('folder restructure');
81
- });
82
-
83
- it('extracts folder create-tree command path from disallowed command error', async () => {
84
- const result = await runInProcess(
85
- 'folder create-tree --library 00000000-0000-0000-0000-000000000001 --spec "{\\"name\\":\\"Root\\",\\"children\\":[]}"',
86
- {
87
- ...baseOptions,
88
- allowlist: { commands: ['folder tree'] },
89
- }
90
- );
91
-
92
- expect(result.ok).toBe(false);
93
- expect(result.command).toBe('folder create-tree');
94
- });
95
- });
package/src/runner.ts DELETED
@@ -1,169 +0,0 @@
1
- // ABOUTME: Executes CLI commands in-process without subprocess spawning
2
- // ABOUTME: Designed for Cloudflare Worker compatibility
3
-
4
- import { Cli } from 'clipanion';
5
- import { tokenize, extractCommandPath } from './tokenizer';
6
- import { checkAllowlist, AllowlistConfig, DEFAULT_ALLOWLIST } from './allowlist';
7
- import { successEnvelope, errorEnvelope, CommandOutput, ErrorCodes } from './envelope';
8
- import { WorkerRuntime } from './runtime';
9
- import { allCommands } from '@carto-knowledge/commands';
10
- import { CartoClient } from '@carto-knowledge/core';
11
-
12
- export interface RunOptions {
13
- /** Correlation ID for tracing */
14
- requestId?: string;
15
-
16
- /** Auth token for API calls */
17
- authToken: string;
18
-
19
- /** Base URL for Carto API */
20
- apiBaseUrl: string;
21
-
22
- /** Command allowlist (defaults to read-only operations) */
23
- allowlist?: AllowlistConfig;
24
- }
25
-
26
- /**
27
- * Executes a CLI command in-process.
28
- *
29
- * Designed for Worker runtime:
30
- * - No subprocess spawning
31
- * - Captures output to memory streams
32
- * - Forces JSON output and non-interactive mode
33
- * - Validates against allowlist
34
- *
35
- * @param commandLine - Command string, e.g., "folder tree --library my-lib"
36
- * @param options - Execution options including auth and allowlist
37
- * @returns Structured command output envelope
38
- */
39
- export async function runInProcess(
40
- commandLine: string,
41
- options: RunOptions
42
- ): Promise<CommandOutput> {
43
- const { requestId, authToken, apiBaseUrl, allowlist = DEFAULT_ALLOWLIST } = options;
44
-
45
- // Defensive: Validate inputs
46
- if (!commandLine || typeof commandLine !== 'string') {
47
- return errorEnvelope('', {
48
- code: ErrorCodes.INVALID_ARGUMENTS,
49
- message: 'commandLine must be a non-empty string',
50
- }, requestId);
51
- }
52
-
53
- if (!authToken) {
54
- return errorEnvelope('', {
55
- code: ErrorCodes.AUTH_ERROR,
56
- message: 'authToken is required',
57
- }, requestId);
58
- }
59
-
60
- // Parse command line
61
- const tokens = tokenize(commandLine.trim());
62
-
63
- if (tokens.length === 0) {
64
- return errorEnvelope('', {
65
- code: ErrorCodes.INVALID_ARGUMENTS,
66
- message: 'Empty command line',
67
- }, requestId);
68
- }
69
-
70
- const commandPath = extractCommandPath(tokens);
71
-
72
- // Check allowlist
73
- const allowlistResult = checkAllowlist(commandPath, allowlist);
74
- if (!allowlistResult.allowed) {
75
- return errorEnvelope(commandPath, allowlistResult.error!, requestId);
76
- }
77
-
78
- // Force tool-mode flags
79
- const augmentedTokens = [
80
- ...tokens,
81
- '--format', 'json',
82
- ];
83
-
84
- // Create CLI instance
85
- const cli = new Cli({
86
- binaryName: 'carto',
87
- enableCapture: true,
88
- });
89
-
90
- // Register all commands
91
- for (const CommandClass of allCommands) {
92
- cli.register(CommandClass);
93
- }
94
-
95
- // Capture output to memory using simple buffers (Worker-compatible)
96
- let stdoutData = '';
97
- let stderrData = '';
98
-
99
- const stdout = {
100
- write(chunk: string | Uint8Array): boolean {
101
- stdoutData += typeof chunk === 'string' ? chunk : new TextDecoder().decode(chunk);
102
- return true;
103
- },
104
- };
105
-
106
- const stderr = {
107
- write(chunk: string | Uint8Array): boolean {
108
- stderrData += typeof chunk === 'string' ? chunk : new TextDecoder().decode(chunk);
109
- return true;
110
- },
111
- };
112
-
113
- // Create runtime and client
114
- const runtime = new WorkerRuntime(authToken, apiBaseUrl);
115
- const client = new CartoClient({
116
- baseUrl: apiBaseUrl,
117
- getAuthToken: async () => authToken,
118
- });
119
-
120
- // Create context matching CartoCommandContext
121
- // Type assertion needed because we use minimal stream stubs for Worker compatibility
122
- // Clipanion only uses the write() method at runtime
123
- const context = {
124
- stdin: process.stdin,
125
- stdout,
126
- stderr,
127
- env: {},
128
- colorDepth: 1,
129
- client,
130
- runtime,
131
- } as unknown as Parameters<typeof cli.run>[1];
132
-
133
- try {
134
- const exitCode = await cli.run(augmentedTokens, context);
135
- const output = stdoutData.trim();
136
- const errorOutput = stderrData.trim();
137
-
138
- if (exitCode === 0) {
139
- // Parse JSON output from command
140
- try {
141
- const result = output ? JSON.parse(output) : null;
142
- return successEnvelope(commandPath, result, requestId);
143
- } catch {
144
- // Command succeeded but output wasn't JSON (shouldn't happen in tool mode)
145
- return successEnvelope(commandPath, { text: output }, requestId);
146
- }
147
- } else {
148
- // Command failed
149
- return errorEnvelope(commandPath, {
150
- code: ErrorCodes.EXECUTION_ERROR,
151
- message: errorOutput || output || `Command exited with code ${exitCode}`,
152
- }, requestId);
153
- }
154
- } catch (error) {
155
- // Unexpected error during execution
156
- const message = error instanceof Error ? error.message : String(error);
157
- console.error(`[carto-cli] Execution error for '${commandPath}':`, error);
158
-
159
- return errorEnvelope(commandPath, {
160
- code: ErrorCodes.EXECUTION_ERROR,
161
- message,
162
- }, requestId);
163
- }
164
- }
165
-
166
- // Re-export for convenience
167
- export { DEFAULT_ALLOWLIST, WRITE_ALLOWLIST } from './allowlist';
168
- export type { AllowlistConfig } from './allowlist';
169
- export type { CommandOutput } from './envelope';
package/src/runtime.ts DELETED
@@ -1,90 +0,0 @@
1
- // ABOUTME: Abstracts runtime differences between CLI and Worker execution
2
- // ABOUTME: Provides consistent interface for auth, logging, and config
3
-
4
- export interface CartoRuntime {
5
- // Authentication
6
- getAuthToken(): Promise<string>;
7
-
8
- // API access
9
- apiBaseUrl: string;
10
-
11
- // Environment flags
12
- isInteractive: boolean;
13
- isToolMode: boolean;
14
-
15
- // Logging (respects --quiet, routes to correct streams)
16
- log(message: string): void;
17
- warn(message: string): void;
18
- error(message: string): void;
19
- }
20
-
21
- /**
22
- * Worker runtime - receives auth from request context.
23
- * Non-interactive, always tool mode.
24
- */
25
- export class WorkerRuntime implements CartoRuntime {
26
- isInteractive = false;
27
- isToolMode = true;
28
-
29
- constructor(
30
- private authToken: string,
31
- public apiBaseUrl: string,
32
- ) {}
33
-
34
- async getAuthToken(): Promise<string> {
35
- return this.authToken;
36
- }
37
-
38
- log(message: string): void {
39
- console.log(`[carto-cli] ${message}`);
40
- }
41
-
42
- warn(message: string): void {
43
- console.warn(`[carto-cli] ${message}`);
44
- }
45
-
46
- error(message: string): void {
47
- console.error(`[carto-cli] ${message}`);
48
- }
49
- }
50
-
51
- /**
52
- * CLI runtime - reads from config file or env vars.
53
- * Interactive when terminal attached.
54
- *
55
- * Compatible with both Bun and Node.js runtimes.
56
- */
57
- export class CliRuntime implements CartoRuntime {
58
- isInteractive: boolean;
59
- isToolMode = false;
60
-
61
- constructor(
62
- private config: { apiBaseUrl: string; authToken?: string },
63
- ) {
64
- // Works in both Bun and Node.js
65
- this.isInteractive = process.stdout.isTTY ?? false;
66
- }
67
-
68
- get apiBaseUrl(): string {
69
- return this.config.apiBaseUrl;
70
- }
71
-
72
- async getAuthToken(): Promise<string> {
73
- if (!this.config.authToken) {
74
- throw new Error('Not authenticated. Run `carto auth login` first.');
75
- }
76
- return this.config.authToken;
77
- }
78
-
79
- log(message: string): void {
80
- console.log(message);
81
- }
82
-
83
- warn(message: string): void {
84
- console.warn(message);
85
- }
86
-
87
- error(message: string): void {
88
- console.error(message);
89
- }
90
- }
@@ -1,83 +0,0 @@
1
- // ABOUTME: Tests for command line tokenizer
2
- // ABOUTME: Validates parsing of quoted strings and shell-like arguments
3
-
4
- import { describe, it, expect } from 'bun:test';
5
- import { tokenize, extractCommandPath } from './tokenizer';
6
-
7
- describe('tokenize', () => {
8
- it('splits simple command', () => {
9
- expect(tokenize('folder tree')).toEqual(['folder', 'tree']);
10
- });
11
-
12
- it('handles single quotes', () => {
13
- expect(tokenize("--name 'My Folder'")).toEqual(['--name', 'My Folder']);
14
- });
15
-
16
- it('handles double quotes', () => {
17
- expect(tokenize('--name "My Folder"')).toEqual(['--name', 'My Folder']);
18
- });
19
-
20
- it('handles mixed quotes', () => {
21
- expect(tokenize(`--a "foo" --b 'bar'`)).toEqual(['--a', 'foo', '--b', 'bar']);
22
- });
23
-
24
- it('handles escaped quotes', () => {
25
- expect(tokenize(`--name "Test\\'s Folder"`)).toEqual(['--name', "Test's Folder"]);
26
- });
27
-
28
- it('handles empty input', () => {
29
- expect(tokenize('')).toEqual([]);
30
- });
31
-
32
- it('handles multiple spaces', () => {
33
- expect(tokenize('folder tree')).toEqual(['folder', 'tree']);
34
- });
35
-
36
- it('handles tabs', () => {
37
- expect(tokenize('folder\ttree')).toEqual(['folder', 'tree']);
38
- });
39
-
40
- it('handles leading and trailing whitespace', () => {
41
- expect(tokenize(' folder tree ')).toEqual(['folder', 'tree']);
42
- });
43
-
44
- it('handles quoted string with equals sign', () => {
45
- expect(tokenize('--filter="status=active"')).toEqual(['--filter=status=active']);
46
- });
47
-
48
- it('handles empty quoted string', () => {
49
- expect(tokenize('--name ""')).toEqual(['--name', '']);
50
- });
51
-
52
- it('handles complex real-world command', () => {
53
- expect(tokenize('folder create --library my-lib --name "Research Papers" --color blue')).toEqual([
54
- 'folder', 'create', '--library', 'my-lib', '--name', 'Research Papers', '--color', 'blue'
55
- ]);
56
- });
57
- });
58
-
59
- describe('extractCommandPath', () => {
60
- it('extracts command before options', () => {
61
- expect(extractCommandPath(['folder', 'tree', '--library', 'x'])).toBe('folder tree');
62
- });
63
-
64
- it('handles no options', () => {
65
- expect(extractCommandPath(['folder', 'tree'])).toBe('folder tree');
66
- });
67
-
68
- it('handles single command', () => {
69
- expect(extractCommandPath(['help'])).toBe('help');
70
- });
71
-
72
- it('handles empty tokens', () => {
73
- expect(extractCommandPath([])).toBe('');
74
- });
75
-
76
- it('stops at first option', () => {
77
- expect(extractCommandPath(['item', 'search', '-q', 'hello', '--format', 'json'])).toBe('item search');
78
- });
79
-
80
- it('handles three-level command', () => {
81
- expect(extractCommandPath(['librarian', 'chat', 'start', '--library', 'x'])).toBe('librarian chat start');
82
- });
83
- });
package/src/tokenizer.ts DELETED
@@ -1,75 +0,0 @@
1
- // ABOUTME: Tokenizes command line strings for in-process CLI execution
2
- // ABOUTME: Handles quoted strings and shell-like argument parsing
3
-
4
- /**
5
- * Tokenizes a command line string, handling quoted strings.
6
- *
7
- * Examples:
8
- * - "folder tree" => ['folder', 'tree']
9
- * - "--name 'My Folder'" => ['--name', 'My Folder']
10
- * - '--name "My Folder"' => ['--name', 'My Folder']
11
- * - "folder create --name 'Test's Folder'" => handles escaped quotes
12
- */
13
- export function tokenize(commandLine: string): string[] {
14
- const tokens: string[] = [];
15
- let current = '';
16
- let inQuote: string | null = null;
17
- let escaped = false;
18
-
19
- for (let i = 0; i < commandLine.length; i++) {
20
- const char = commandLine[i];
21
-
22
- if (escaped) {
23
- current += char;
24
- escaped = false;
25
- continue;
26
- }
27
-
28
- if (char === '\\') {
29
- escaped = true;
30
- continue;
31
- }
32
-
33
- if (inQuote) {
34
- if (char === inQuote) {
35
- // Empty quoted string should still produce a token
36
- tokens.push(current);
37
- current = '';
38
- inQuote = null;
39
- } else {
40
- current += char;
41
- }
42
- } else if (char === '"' || char === "'") {
43
- inQuote = char;
44
- } else if (char === ' ' || char === '\t') {
45
- if (current) {
46
- tokens.push(current);
47
- current = '';
48
- }
49
- } else {
50
- current += char;
51
- }
52
- }
53
-
54
- if (current) {
55
- tokens.push(current);
56
- }
57
-
58
- return tokens;
59
- }
60
-
61
- /**
62
- * Extracts the command path from tokens (before any options).
63
- *
64
- * Examples:
65
- * - ['folder', 'tree', '--library', 'x'] => 'folder tree'
66
- * - ['item', 'search', '-q', 'hello'] => 'item search'
67
- */
68
- export function extractCommandPath(tokens: string[]): string {
69
- const path: string[] = [];
70
- for (const token of tokens) {
71
- if (token.startsWith('-')) break;
72
- path.push(token);
73
- }
74
- return path.join(' ');
75
- }
package/tsconfig.json DELETED
@@ -1,15 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ESNext",
4
- "module": "ESNext",
5
- "moduleResolution": "bundler",
6
- "strict": true,
7
- "esModuleInterop": true,
8
- "skipLibCheck": true,
9
- "noEmit": true,
10
- "declaration": true,
11
- "declarationMap": true,
12
- "types": ["bun-types"]
13
- },
14
- "include": ["src/**/*"]
15
- }