smithue-cli 0.8.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 (44) hide show
  1. package/README.md +146 -0
  2. package/dist/cli.d.ts +3 -0
  3. package/dist/cli.d.ts.map +1 -0
  4. package/dist/cli.js +190 -0
  5. package/dist/client.d.ts +37 -0
  6. package/dist/client.d.ts.map +1 -0
  7. package/dist/client.js +184 -0
  8. package/dist/commands/batch.d.ts +13 -0
  9. package/dist/commands/batch.d.ts.map +1 -0
  10. package/dist/commands/batch.js +64 -0
  11. package/dist/commands/exec.d.ts +6 -0
  12. package/dist/commands/exec.d.ts.map +1 -0
  13. package/dist/commands/exec.js +18 -0
  14. package/dist/commands/list.d.ts +8 -0
  15. package/dist/commands/list.d.ts.map +1 -0
  16. package/dist/commands/list.js +18 -0
  17. package/dist/commands/purge.d.ts +3 -0
  18. package/dist/commands/purge.d.ts.map +1 -0
  19. package/dist/commands/purge.js +154 -0
  20. package/dist/commands/search.d.ts +8 -0
  21. package/dist/commands/search.d.ts.map +1 -0
  22. package/dist/commands/search.js +31 -0
  23. package/dist/commands/status.d.ts +9 -0
  24. package/dist/commands/status.d.ts.map +1 -0
  25. package/dist/commands/status.js +51 -0
  26. package/dist/commands/upgrade.d.ts +2 -0
  27. package/dist/commands/upgrade.d.ts.map +1 -0
  28. package/dist/commands/upgrade.js +20 -0
  29. package/dist/index.d.ts +3 -0
  30. package/dist/index.d.ts.map +1 -0
  31. package/dist/index.js +139 -0
  32. package/dist/output.d.ts +8 -0
  33. package/dist/output.d.ts.map +1 -0
  34. package/dist/output.js +46 -0
  35. package/dist/portfile.d.ts +31 -0
  36. package/dist/portfile.d.ts.map +1 -0
  37. package/dist/portfile.js +133 -0
  38. package/dist/types.d.ts +50 -0
  39. package/dist/types.d.ts.map +1 -0
  40. package/dist/types.js +3 -0
  41. package/dist/version-check.d.ts +2 -0
  42. package/dist/version-check.d.ts.map +1 -0
  43. package/dist/version-check.js +9 -0
  44. package/package.json +40 -0
package/README.md ADDED
@@ -0,0 +1,146 @@
1
+ # smithue-cli
2
+
3
+ Stateless command line interface for the SmithUE Unreal Engine plugin.
4
+
5
+ ## For AI Agents
6
+
7
+ ### Quick Deploy
8
+ - **Windows**: `npm install -g smithue-cli`
9
+ - **macOS**: `npm install -g smithue-cli`
10
+ - **Linux**: `npm install -g smithue-cli`
11
+
12
+ *Note: The v1 plugin server is Windows-only. The CLI npm package installs on any OS, but requires a Windows machine running the UE editor.*
13
+
14
+ ### Prerequisites check
15
+ Ensure Node.js 18+ is installed. Run `node -v` to check.
16
+
17
+ ### Verification
18
+ Run this command to check if the CLI can find your running Unreal Engine project:
19
+ ```powershell
20
+ smithue-cli status
21
+ ```
22
+
23
+ ### Fault tree (if things go wrong)
24
+ 1. **Editor not running**: Start Unreal Engine and verify the SmithUE plugin is active.
25
+ 2. **Multiple instances**: If you have two editors open, the CLI needs help. Use `smithue-cli status` to see PIDs, then pass `--pid <number>` to your command.
26
+ 3. **Stale portfile**: If the editor crashed, a `.port` file might remain in `%LOCALAPPDATA%\.smithue\`. Run `smithue-cli prune` to clean up dead files.
27
+ 4. **Connection refused**: Check if your firewall blocks local port traffic. SmithUE only listens on 127.0.0.1.
28
+
29
+ ## Installation
30
+ Standard installation via npm:
31
+ ```bash
32
+ npm install -g smithue-cli
33
+ ```
34
+ Or run directly without installing:
35
+ ```bash
36
+ npx smithue-cli <command>
37
+ ```
38
+
39
+ ## Subcommands
40
+
41
+ | Command | Description |
42
+ |---|---|
43
+ | `exec` | Run a remote command in UE |
44
+ | `list` | List available domains or objects |
45
+ | `search` | Find assets or objects by string |
46
+ | `status` | Show running UE instances and their ports |
47
+ | `batch` | Run multiple read-only commands sequentially |
48
+ | `upgrade` | Update the CLI to the latest version via npm |
49
+ | `prune` | Remove stale port files from crashed instances |
50
+ | `purge` | Remove the entire `.smithue` directory (full uninstall cleanup) |
51
+
52
+ ## Output Modes
53
+
54
+ By default, `smithue-cli` outputs pretty-printed JSON (2-space indent).
55
+
56
+ - `--terse` — Minified JSON (no whitespace). Recommended for AI agents to save tokens.
57
+ - `--out <file>` — Write result to file; stdout is silent. Useful for large responses.
58
+ - Combined: `smithue-cli status --terse --out result.json`
59
+
60
+ ## Batch Mode
61
+
62
+ Run multiple read-only commands in a single call:
63
+
64
+ ```bash
65
+ smithue-cli batch "status" "list"
66
+ ```
67
+
68
+ Returns a JSON array: `[{command, ok, data?, error?}, ...]`
69
+
70
+ Supported commands: `status`, `list`, `search`. Sequential execution only.
71
+
72
+ ## Upgrading
73
+
74
+ ```bash
75
+ smithue-cli upgrade
76
+ ```
77
+
78
+ Updates `smithue-cli` to the latest version via npm. A warning is printed to stderr if the CLI version does not match the plugin version.
79
+
80
+ ## AI Agent Integration
81
+
82
+ Recommended flags for AI agent usage:
83
+
84
+ ```bash
85
+ # Minified output saves tokens
86
+ smithue-cli status --terse
87
+
88
+ # Write large responses to file, keep context clean
89
+ smithue-cli list --out tools.json
90
+
91
+ # Multiple queries in one call
92
+ smithue-cli batch "status" "list" --terse
93
+ ```
94
+
95
+ ## Examples
96
+ List all Material assets:
97
+ ```bash
98
+ smithue-cli list Material
99
+ ```
100
+
101
+ Search for blueprints:
102
+ ```bash
103
+ smithue-cli search blueprint
104
+ ```
105
+
106
+ Execute a custom action:
107
+ ```bash
108
+ smithue-cli exec my_action '{"key": "value"}'
109
+ ```
110
+
111
+ ## Security Notes
112
+ - Binds to 127.0.0.1 only. No external network exposure.
113
+ - Port files in `%LOCALAPPDATA%\.smithue` are ACL-restricted to the current Windows user.
114
+
115
+ ## Uninstall
116
+
117
+ Use `purge` to fully clean up after removing SmithUE. Unlike `prune` (which removes stale port files during normal use), `purge` deletes the entire `%LOCALAPPDATA%\.smithue\` directory as the final step of uninstalling the CLI.
118
+
119
+ ```bash
120
+ smithue-cli purge # interactive: lists files and asks for confirmation
121
+ smithue-cli purge --dry-run # preview what would be deleted
122
+ smithue-cli purge -y # non-interactive full purge (CI/scripts)
123
+ ```
124
+
125
+ ### Options
126
+
127
+ | Flag | Description |
128
+ |---|---|
129
+ | `--force` | Skip liveness check; delete all files including non-portfiles |
130
+ | `--dry-run` | Show what would be deleted without making changes |
131
+ | `-y, --yes` | Skip the confirmation prompt (required when stdin is not a TTY) |
132
+
133
+ ### Exit codes
134
+
135
+ | Code | Meaning |
136
+ |---|---|
137
+ | 0 | Success (including cancelled and dry-run) |
138
+ | 1 | Non-interactive context without `-y` |
139
+ | 2 | `LOCALAPPDATA` not set (Windows-only command) |
140
+ | 3 | `.smithue` is a symlink or junction — refused for safety |
141
+
142
+ For routine cleanup of stale portfiles without removing the directory, use `smithue-cli prune` instead.
143
+
144
+ ## Known Limitations
145
+ - Version 1 is Windows-only due to portfile path conventions.
146
+ - No persistent configuration files. Use environment variables like `SMITHUE_PORT` or `SMITHUE_PID` for overrides.
package/dist/cli.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
package/dist/cli.js ADDED
@@ -0,0 +1,190 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import { readdir, readFile, unlink } from 'fs/promises';
4
+ import { createRequire } from 'module';
5
+ import { join } from 'path';
6
+ import { execCommand } from './commands/exec.js';
7
+ import { listCommand } from './commands/list.js';
8
+ import { searchCommand } from './commands/search.js';
9
+ import { statusCommand } from './commands/status.js';
10
+ import { purge } from './commands/purge.js';
11
+ import { upgradeCommand } from './commands/upgrade.js';
12
+ import { batchCommand } from './commands/batch.js';
13
+ import { printResult, printError, setOutputOptions } from './output.js';
14
+ const program = new Command();
15
+ const require = createRequire(import.meta.url);
16
+ const { version: cliVersion } = require('../package.json');
17
+ program
18
+ .name('smithue')
19
+ .description('CLI for SmithUE Unreal Engine plugin')
20
+ .version(cliVersion)
21
+ .option('--pid <pid>', 'target SmithUE instance by PID', parseInt)
22
+ .option('--project <path>', 'target SmithUE instance by project path')
23
+ .option('--port <port>', 'connect directly to port (skip discovery)', parseInt)
24
+ .option('--terse', 'emit minified JSON output')
25
+ .option('--out <file>', 'write result to file instead of stdout');
26
+ program.hook('preAction', () => {
27
+ const opts = program.opts();
28
+ setOutputOptions({ terse: opts.terse, outPath: opts.out });
29
+ });
30
+ // ---------------------------------------------------------------------------
31
+ // exec
32
+ // ---------------------------------------------------------------------------
33
+ program
34
+ .command('exec <command> [params]')
35
+ .description('Execute a SmithUE command')
36
+ .action(async (command, params) => {
37
+ const globals = program.opts();
38
+ let parsedParams = {};
39
+ if (params) {
40
+ try {
41
+ parsedParams = JSON.parse(params);
42
+ }
43
+ catch {
44
+ printError(new Error(`params must be valid JSON, got: ${params}`));
45
+ return;
46
+ }
47
+ }
48
+ await execCommand(command, parsedParams, { ...globals, cliVersion });
49
+ });
50
+ // ---------------------------------------------------------------------------
51
+ // list
52
+ // ---------------------------------------------------------------------------
53
+ program
54
+ .command('list [domain]')
55
+ .description('List available tools, optionally filtered by domain')
56
+ .action(async (domain) => {
57
+ const globals = program.opts();
58
+ await listCommand(domain, { ...globals, cliVersion });
59
+ });
60
+ // ---------------------------------------------------------------------------
61
+ // search
62
+ // ---------------------------------------------------------------------------
63
+ program
64
+ .command('search <keyword>')
65
+ .description('Search tools by keyword')
66
+ .action(async (keyword) => {
67
+ const globals = program.opts();
68
+ await searchCommand(keyword, { ...globals, cliVersion });
69
+ });
70
+ // ---------------------------------------------------------------------------
71
+ // status
72
+ // ---------------------------------------------------------------------------
73
+ program
74
+ .command('status')
75
+ .description('Get SmithUE editor status')
76
+ .option('--wait <seconds>', 'wait up to N seconds for editor to be ready', parseInt)
77
+ .action(async (cmdOpts) => {
78
+ const globals = program.opts();
79
+ await statusCommand({ ...globals, wait: cmdOpts.wait, cliVersion });
80
+ });
81
+ // ---------------------------------------------------------------------------
82
+ // prune
83
+ // ---------------------------------------------------------------------------
84
+ program
85
+ .command('prune')
86
+ .description('Remove stale portfiles for SmithUE instances that are no longer running')
87
+ .action(async () => {
88
+ const localAppData = process.env['LOCALAPPDATA'];
89
+ if (!localAppData) {
90
+ printResult({ scanned: 0, pruned: 0, kept: 0 });
91
+ return;
92
+ }
93
+ const dir = join(localAppData, '.smithue');
94
+ let entries;
95
+ try {
96
+ entries = await readdir(dir);
97
+ }
98
+ catch {
99
+ printResult({ scanned: 0, pruned: 0, kept: 0 });
100
+ return;
101
+ }
102
+ const portFiles = entries.filter((e) => e.endsWith('.port'));
103
+ let scanned = 0;
104
+ let pruned = 0;
105
+ let kept = 0;
106
+ for (const entry of portFiles) {
107
+ const filePath = join(dir, entry);
108
+ scanned++;
109
+ let port;
110
+ try {
111
+ const content = await readFile(filePath, 'utf8');
112
+ const data = JSON.parse(content);
113
+ port = data.port;
114
+ if (!Number.isInteger(port) || port <= 0)
115
+ throw new Error('bad port');
116
+ }
117
+ catch {
118
+ // malformed portfile - treat as stale
119
+ try {
120
+ await unlink(filePath);
121
+ }
122
+ catch { /* best effort */ }
123
+ pruned++;
124
+ continue;
125
+ }
126
+ let alive = false;
127
+ try {
128
+ await fetch(`http://127.0.0.1:${port}/ready`, {
129
+ signal: AbortSignal.timeout(1000),
130
+ });
131
+ // any HTTP response = server is alive (including 503 during startup)
132
+ alive = true;
133
+ }
134
+ catch {
135
+ alive = false;
136
+ }
137
+ if (alive) {
138
+ kept++;
139
+ }
140
+ else {
141
+ try {
142
+ await unlink(filePath);
143
+ }
144
+ catch {
145
+ // best effort
146
+ }
147
+ pruned++;
148
+ }
149
+ }
150
+ printResult({ scanned, pruned, kept });
151
+ });
152
+ // ---------------------------------------------------------------------------
153
+ // purge
154
+ // ---------------------------------------------------------------------------
155
+ program
156
+ .command('purge')
157
+ .description('Remove the .smithue directory entirely (full uninstall cleanup)')
158
+ .option('--force', 'skip liveness check and delete all files including unknown ones')
159
+ .option('--dry-run', 'show what would be deleted without modifying anything')
160
+ .option('-y, --yes', 'skip confirmation prompt (required for non-interactive use)')
161
+ .action(async (cmdOpts) => {
162
+ await purge({
163
+ force: cmdOpts.force ?? false,
164
+ dryRun: cmdOpts.dryRun ?? false,
165
+ yes: cmdOpts.yes ?? false,
166
+ });
167
+ });
168
+ // ---------------------------------------------------------------------------
169
+ // upgrade
170
+ // ---------------------------------------------------------------------------
171
+ program
172
+ .command('upgrade')
173
+ .description('Update smithue-cli globally via npm')
174
+ .action(async () => {
175
+ await upgradeCommand();
176
+ });
177
+ // ---------------------------------------------------------------------------
178
+ // batch
179
+ // ---------------------------------------------------------------------------
180
+ program
181
+ .command('batch [commands...]')
182
+ .description('Execute multiple read-only commands sequentially')
183
+ .action(async (commands = []) => {
184
+ const globals = program.opts();
185
+ await batchCommand(commands, globals);
186
+ });
187
+ // ---------------------------------------------------------------------------
188
+ // Parse
189
+ // ---------------------------------------------------------------------------
190
+ await program.parseAsync(process.argv);
@@ -0,0 +1,37 @@
1
+ import type { SmithUEExecuteResponse, SmithUEToolSchema } from './types.js';
2
+ export interface SmithUEClientConfig {
3
+ host: string;
4
+ port: number;
5
+ timeout?: number;
6
+ }
7
+ export declare class SmithUEClient {
8
+ private host;
9
+ private port;
10
+ private timeout;
11
+ constructor(config: SmithUEClientConfig);
12
+ private get baseUrl();
13
+ private headers;
14
+ private fetchJson;
15
+ private postJson;
16
+ private getJson;
17
+ private normalizeRequestError;
18
+ execute(command: string, params?: Record<string, unknown>): Promise<SmithUEExecuteResponse>;
19
+ executeCommand(command: string, params?: Record<string, unknown>): Promise<SmithUEExecuteResponse>;
20
+ ping(): Promise<{
21
+ message: string;
22
+ }>;
23
+ listTools(domain?: string): Promise<SmithUEToolSchema[]>;
24
+ getReady(): Promise<{
25
+ ready: boolean;
26
+ version?: string;
27
+ pie_active?: boolean;
28
+ }>;
29
+ isConnected(): Promise<boolean>;
30
+ executeWithFailover(command: string, params?: Record<string, unknown>): Promise<SmithUEExecuteResponse>;
31
+ private mapErrorCodeToSmithUEError;
32
+ private startAsyncTask;
33
+ private isAsyncTaskComplete;
34
+ private isRecord;
35
+ private sleep;
36
+ }
37
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,sBAAsB,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAE5E,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,qBAAa,aAAa;IACxB,OAAO,CAAC,IAAI,CAAS;IACrB,OAAO,CAAC,IAAI,CAAS;IACrB,OAAO,CAAC,OAAO,CAAS;gBAEZ,MAAM,EAAE,mBAAmB;IAMvC,OAAO,KAAK,OAAO,GAElB;IAED,OAAO,CAAC,OAAO;YAID,SAAS;YAwBT,QAAQ;YAQR,OAAO;IAOrB,OAAO,CAAC,qBAAqB;IAqBvB,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GAAG,OAAO,CAAC,sBAAsB,CAAC;IAa/F,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GAAG,OAAO,CAAC,sBAAsB,CAAC;IAItG,IAAI,IAAI,OAAO,CAAC;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IAKpC,SAAS,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,EAAE,CAAC;IAUxD,QAAQ,IAAI,OAAO,CAAC;QAAE,KAAK,EAAE,OAAO,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;IAI/E,WAAW,IAAI,OAAO,CAAC,OAAO,CAAC;IAS/B,mBAAmB,CACvB,OAAO,EAAE,MAAM,EACf,MAAM,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GACnC,OAAO,CAAC,sBAAsB,CAAC;IA6ClC,OAAO,CAAC,0BAA0B;YAmBpB,cAAc;IAe5B,OAAO,CAAC,mBAAmB;IAI3B,OAAO,CAAC,QAAQ;YAIF,KAAK;CAGpB"}
package/dist/client.js ADDED
@@ -0,0 +1,184 @@
1
+ import { SmithUEError } from './portfile.js';
2
+ export class SmithUEClient {
3
+ host;
4
+ port;
5
+ timeout;
6
+ constructor(config) {
7
+ this.host = config.host;
8
+ this.port = config.port;
9
+ this.timeout = config.timeout ?? 30000;
10
+ }
11
+ get baseUrl() {
12
+ return `http://${this.host}:${this.port}`;
13
+ }
14
+ headers() {
15
+ return { 'Content-Type': 'application/json' };
16
+ }
17
+ async fetchJson(path, init, timeoutMs) {
18
+ const ms = timeoutMs ?? this.timeout;
19
+ const controller = new AbortController();
20
+ const timer = setTimeout(() => controller.abort(), ms);
21
+ try {
22
+ const response = await fetch(`${this.baseUrl}${path}`, {
23
+ ...init,
24
+ signal: controller.signal,
25
+ });
26
+ if (!response.ok) {
27
+ const body = await response.text().catch(() => '');
28
+ throw new Error(`SmithUE plugin returned HTTP ${response.status}. Body: ${body.slice(0, 200)}`);
29
+ }
30
+ return (await response.json());
31
+ }
32
+ finally {
33
+ clearTimeout(timer);
34
+ }
35
+ }
36
+ async postJson(path, body, timeoutMs) {
37
+ return this.fetchJson(path, {
38
+ method: 'POST',
39
+ headers: this.headers(),
40
+ body: JSON.stringify(body),
41
+ }, timeoutMs);
42
+ }
43
+ async getJson(path, timeoutMs) {
44
+ return this.fetchJson(path, {
45
+ method: 'GET',
46
+ headers: this.headers(),
47
+ }, timeoutMs);
48
+ }
49
+ normalizeRequestError(err, command) {
50
+ const msg = err.message ?? '';
51
+ if (msg.includes('ECONNREFUSED') ||
52
+ msg.includes('fetch failed') ||
53
+ msg.includes('Failed to fetch') ||
54
+ msg.includes('ENOTFOUND') ||
55
+ msg.includes('connect ECONNREFUSED')) {
56
+ return new Error(`SmithUE plugin unreachable at ${this.host}:${this.port}. Start UE Editor with SmithUE plugin enabled.`);
57
+ }
58
+ if (err.name === 'AbortError') {
59
+ return new Error(`SmithUE plugin timed out. Command: ${command} (port: ${this.port})`);
60
+ }
61
+ return err instanceof Error ? err : new Error(String(err));
62
+ }
63
+ async execute(command, params = {}) {
64
+ try {
65
+ const data = await this.postJson('/api/v1/execute', { command, params });
66
+ if (data.status === 'error') {
67
+ throw this.mapErrorCodeToSmithUEError(data);
68
+ }
69
+ return data;
70
+ }
71
+ catch (err) {
72
+ if (err instanceof SmithUEError)
73
+ throw err;
74
+ throw this.normalizeRequestError(err, command);
75
+ }
76
+ }
77
+ async executeCommand(command, params = {}) {
78
+ return this.execute(command, params);
79
+ }
80
+ async ping() {
81
+ const res = await this.execute('ping', {});
82
+ return res.data;
83
+ }
84
+ async listTools(domain) {
85
+ const res = await this.execute('list_tools', domain ? { domain } : {});
86
+ if (domain) {
87
+ const data = res.data;
88
+ return data.tools;
89
+ }
90
+ // no domain filter - server returns {domains: [...]} not tools
91
+ return [];
92
+ }
93
+ async getReady() {
94
+ return this.getJson('/ready');
95
+ }
96
+ async isConnected() {
97
+ try {
98
+ await this.ping();
99
+ return true;
100
+ }
101
+ catch {
102
+ return false;
103
+ }
104
+ }
105
+ async executeWithFailover(command, params = {}) {
106
+ const syncTimeout = parseInt(process.env.SMITHUE_SYNC_TIMEOUT || '10000', 10);
107
+ const asyncTimeout = parseInt(process.env.SMITHUE_ASYNC_TIMEOUT || '120000', 10);
108
+ try {
109
+ return await this.postJson('/api/v1/execute', { command, params }, syncTimeout)
110
+ .then((data) => {
111
+ if (data.status === 'error')
112
+ throw this.mapErrorCodeToSmithUEError(data);
113
+ return data;
114
+ });
115
+ }
116
+ catch (err) {
117
+ if (err instanceof SmithUEError)
118
+ throw err;
119
+ if (err.name !== 'AbortError') {
120
+ throw this.normalizeRequestError(err, command);
121
+ }
122
+ }
123
+ const taskId = await this.startAsyncTask(command, params);
124
+ const startedAt = Date.now();
125
+ let delayMs = 500;
126
+ while (Date.now() - startedAt <= asyncTimeout) {
127
+ const remainingMs = asyncTimeout - (Date.now() - startedAt);
128
+ if (remainingMs <= 0)
129
+ break;
130
+ const poll = await this.getJson(`/api/v1/async/${encodeURIComponent(taskId)}`, remainingMs);
131
+ if (poll.status === 'error') {
132
+ if (poll.error?.startsWith('Unknown task_id:')) {
133
+ throw new Error('Task lost — server may have restarted');
134
+ }
135
+ throw this.mapErrorCodeToSmithUEError(poll);
136
+ }
137
+ const data = poll.data;
138
+ if (this.isAsyncTaskComplete(data)) {
139
+ return { status: 'success', data: data.result };
140
+ }
141
+ await this.sleep(Math.min(delayMs, Math.max(asyncTimeout - (Date.now() - startedAt), 0)));
142
+ delayMs = Math.min(delayMs * 2, 4000);
143
+ }
144
+ throw new Error(`Async task timed out after ${Math.ceil(asyncTimeout / 1000)}s`);
145
+ }
146
+ mapErrorCodeToSmithUEError(data) {
147
+ const msg = data.error ?? 'SmithUE command failed';
148
+ switch (data.error_code) {
149
+ case 'STALE_NID':
150
+ return new SmithUEError(msg, 5);
151
+ case 'PIE_LOCKED':
152
+ case 'ASSET_NOT_FOUND':
153
+ case 'INVALID_REQUEST':
154
+ return new SmithUEError(msg, 3);
155
+ case 'PAYLOAD_TOO_LARGE':
156
+ return new SmithUEError(msg, 1);
157
+ case 'INTERNAL_ERROR':
158
+ case 'EDITOR_NOT_READY':
159
+ return new SmithUEError(msg, 4);
160
+ default:
161
+ return new SmithUEError(msg, 3);
162
+ }
163
+ }
164
+ async startAsyncTask(command, params) {
165
+ const data = await this.postJson('/api/v1/async', { command, params });
166
+ if (data.status === 'error') {
167
+ throw this.mapErrorCodeToSmithUEError(data);
168
+ }
169
+ const taskId = data.data?.task_id;
170
+ if (typeof taskId !== 'string' || taskId.length === 0) {
171
+ throw new Error('SmithUE async task did not return a task_id');
172
+ }
173
+ return taskId;
174
+ }
175
+ isAsyncTaskComplete(data) {
176
+ return data?.completed === true && this.isRecord(data.result);
177
+ }
178
+ isRecord(value) {
179
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
180
+ }
181
+ async sleep(ms) {
182
+ await new Promise((resolve) => setTimeout(resolve, ms));
183
+ }
184
+ }
@@ -0,0 +1,13 @@
1
+ export interface GlobalOpts {
2
+ pid?: number;
3
+ project?: string;
4
+ port?: number;
5
+ }
6
+ export type BatchResult = {
7
+ command: string;
8
+ ok: boolean;
9
+ data?: unknown;
10
+ error?: string;
11
+ };
12
+ export declare function batchCommand(commands: string[], opts: GlobalOpts): Promise<void>;
13
+ //# sourceMappingURL=batch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"batch.d.ts","sourceRoot":"","sources":["../../src/commands/batch.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,UAAU;IACzB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,MAAM,WAAW,GAAG;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,EAAE,EAAE,OAAO,CAAC;IACZ,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAIF,wBAAsB,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAiEtF"}
@@ -0,0 +1,64 @@
1
+ import { SmithUEClient } from '../client.js';
2
+ import { discoverPort } from '../portfile.js';
3
+ import { printResult, printError } from '../output.js';
4
+ const ALLOWED_COMMANDS = ['status', 'list', 'search'];
5
+ export async function batchCommand(commands, opts) {
6
+ // Short-circuit for empty list — no connection needed
7
+ if (commands.length === 0) {
8
+ printResult([]);
9
+ return;
10
+ }
11
+ try {
12
+ const discovered = await discoverPort(opts);
13
+ const client = new SmithUEClient({ host: '127.0.0.1', port: discovered.port });
14
+ const results = [];
15
+ for (const cmdEntry of commands) {
16
+ const parts = cmdEntry.trim().split(/\s+/);
17
+ const cmd = parts[0];
18
+ if (!ALLOWED_COMMANDS.includes(cmd)) {
19
+ results.push({
20
+ command: cmdEntry,
21
+ ok: false,
22
+ error: `${cmd} is not allowed in batch mode. Allowed commands: ${ALLOWED_COMMANDS.join(', ')}`,
23
+ });
24
+ continue;
25
+ }
26
+ try {
27
+ let data;
28
+ if (cmd === 'status') {
29
+ data = await client.getReady();
30
+ }
31
+ else if (cmd === 'list') {
32
+ const domain = parts[1];
33
+ data = await client.listTools(domain);
34
+ }
35
+ else if (cmd === 'search') {
36
+ const query = parts.slice(1).join(' ').toLowerCase();
37
+ const domains = await client.listTools();
38
+ const matches = [];
39
+ for (const domainTool of domains) {
40
+ const domainName = domainTool.name;
41
+ const tools = await client.listTools(domainName);
42
+ for (const tool of tools) {
43
+ const name = tool.name ?? '';
44
+ const description = tool.description ?? '';
45
+ if (name.toLowerCase().includes(query) || description.toLowerCase().includes(query)) {
46
+ matches.push({ domain: domainName, name, description });
47
+ }
48
+ }
49
+ }
50
+ data = matches;
51
+ }
52
+ results.push({ command: cmdEntry, ok: true, data });
53
+ }
54
+ catch (err) {
55
+ const message = err instanceof Error ? err.message : String(err);
56
+ results.push({ command: cmdEntry, ok: false, error: message });
57
+ }
58
+ }
59
+ printResult(results);
60
+ }
61
+ catch (err) {
62
+ printError(err);
63
+ }
64
+ }
@@ -0,0 +1,6 @@
1
+ import { type DiscoverOpts } from '../portfile.js';
2
+ export interface ExecOpts extends DiscoverOpts {
3
+ cliVersion?: string;
4
+ }
5
+ export declare function execCommand(command: string, params: Record<string, unknown>, opts?: ExecOpts): Promise<void>;
6
+ //# sourceMappingURL=exec.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"exec.d.ts","sourceRoot":"","sources":["../../src/commands/exec.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,KAAK,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAKjE,MAAM,WAAW,QAAS,SAAQ,YAAY;IAC5C,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,wBAAsB,WAAW,CAC/B,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,IAAI,GAAE,QAAa,GAClB,OAAO,CAAC,IAAI,CAAC,CAWf"}
@@ -0,0 +1,18 @@
1
+ import { discoverPort } from '../portfile.js';
2
+ import { SmithUEClient } from '../client.js';
3
+ import { printResult, printError } from '../output.js';
4
+ import { checkVersionCompat } from '../version-check.js';
5
+ export async function execCommand(command, params, opts = {}) {
6
+ try {
7
+ const discovered = await discoverPort(opts);
8
+ if (opts.cliVersion)
9
+ checkVersionCompat(opts.cliVersion, discovered.plugin_version);
10
+ const { port } = discovered;
11
+ const client = new SmithUEClient({ host: '127.0.0.1', port });
12
+ const result = await client.executeCommand(command, params);
13
+ printResult(result);
14
+ }
15
+ catch (err) {
16
+ printError(err);
17
+ }
18
+ }