Skip to content

Commit d6f0729

Browse files
committed
switch to camel_case
1 parent 6f94d4d commit d6f0729

File tree

2 files changed

+84
-24
lines changed

2 files changed

+84
-24
lines changed

src/AXQueryExecutor.ts

Lines changed: 68 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,35 +4,75 @@ import path from 'node:path';
44
import { spawn } from 'node:child_process';
55
import { fileURLToPath } from 'node:url';
66
import { Logger } from './logger.js';
7+
import type { AXQueryInput } from './schemas.js'; // Import AXQueryInput type
78

89
// Get the directory of the current module
910
const __filename = fileURLToPath(import.meta.url);
1011
const __dirname = path.dirname(__filename);
1112
const logger = new Logger('AXQueryExecutor');
1213

14+
export interface AXQueryExecutionResult {
15+
result: Record<string, unknown>;
16+
execution_time_seconds: number;
17+
}
18+
1319
export class AXQueryExecutor {
1420
private axUtilityPath: string;
1521
private scriptPath: string;
1622

1723
constructor() {
18-
// Calculate the path to the AX utility relative to this file
19-
this.axUtilityPath = path.resolve(__dirname, '..', 'ax');
20-
// Path to the wrapper script
24+
// Determine if running from source or dist to set the correct base path
25+
// __dirname will be like /path/to/project/src or /path/to/project/dist/src
26+
const isProdBuild = __dirname.includes(path.join(path.sep, 'dist', path.sep));
27+
28+
if (isProdBuild) {
29+
// In production (dist), ax_runner.sh and ax binary are directly in dist/
30+
// So, utility path is one level up from dist/src (i.e., dist/)
31+
this.axUtilityPath = path.resolve(__dirname, '..');
32+
} else {
33+
// In development (src), ax_runner.sh and ax binary are in project_root/ax/
34+
// So, utility path is one level up from src/ and then into ax/
35+
this.axUtilityPath = path.resolve(__dirname, '..', 'ax');
36+
}
37+
2138
this.scriptPath = path.join(this.axUtilityPath, 'ax_runner.sh');
39+
logger.debug('AXQueryExecutor initialized', {
40+
isProdBuild,
41+
axUtilityPath: this.axUtilityPath,
42+
scriptPath: this.scriptPath
43+
});
2244
}
2345

2446
/**
2547
* Execute a query against the AX utility
2648
* @param queryData The query to execute
2749
* @returns The result of the query
2850
*/
29-
async execute(queryData: Record<string, unknown>): Promise<Record<string, unknown>> {
30-
logger.debug('Executing AX query', queryData);
51+
async execute(queryData: AXQueryInput): Promise<AXQueryExecutionResult> {
52+
logger.debug('Executing AX query with input:', queryData);
53+
const startTime = Date.now();
54+
55+
// Map to the keys expected by the Swift binary
56+
const mappedQueryData = {
57+
cmd: queryData.command,
58+
multi: queryData.return_all_matches,
59+
locator: {
60+
app: queryData.locator.app,
61+
role: queryData.locator.role,
62+
match: queryData.locator.match,
63+
pathHint: queryData.locator.navigation_path_hint,
64+
},
65+
attributes: queryData.attributes_to_query,
66+
requireAction: queryData.required_action_name,
67+
action: queryData.action_to_perform,
68+
// report_execution_time is not sent to the Swift binary
69+
};
70+
logger.debug('Mapped AX query for Swift binary:', mappedQueryData);
3171

3272
return new Promise((resolve, reject) => {
3373
try {
34-
// Get the query string
35-
const queryString = JSON.stringify(queryData) + '\n';
74+
// Get the query string from the mapped data
75+
const queryString = JSON.stringify(mappedQueryData) + '\n';
3676

3777
logger.debug('Running AX utility through wrapper script', { path: this.scriptPath });
3878
logger.debug('Query to run: ', { query: queryString});
@@ -63,12 +103,18 @@ export class AXQueryExecutor {
63103
// Handle process errors
64104
process.on('error', (error) => {
65105
logger.error('Process error:', { error });
66-
reject(new Error(`Process error: ${error.message}`));
106+
const endTime = Date.now();
107+
const execution_time_seconds = parseFloat(((endTime - startTime) / 1000).toFixed(3));
108+
const errorToReject = new Error(`Process error: ${error.message}`) as Error & { execution_time_seconds?: number };
109+
errorToReject.execution_time_seconds = execution_time_seconds;
110+
reject(errorToReject);
67111
});
68112

69113
// Handle process exit
70114
process.on('exit', (code, signal) => {
71115
logger.debug('Process exited:', { code, signal });
116+
const endTime = Date.now();
117+
const execution_time_seconds = parseFloat(((endTime - startTime) / 1000).toFixed(3));
72118

73119
// Check for log file if we had issues
74120
if (code !== 0 || signal) {
@@ -86,20 +132,24 @@ export class AXQueryExecutor {
86132
if (stdoutData.trim()) {
87133
try {
88134
const result = JSON.parse(stdoutData) as Record<string, unknown>;
89-
return resolve(result);
135+
return resolve({ result, execution_time_seconds });
90136
} catch (error) {
91137
logger.error('Failed to parse JSON output', { error, stdout: stdoutData });
138+
// Fall through to error handling below if JSON parsing fails
92139
}
93140
}
94141

95-
// If we didn't return a result above, handle as error
142+
let errorMessage = '';
96143
if (signal) {
97-
reject(new Error(`Process terminated by signal ${signal}: ${stderrData}`));
144+
errorMessage = `Process terminated by signal ${signal}: ${stderrData}`;
98145
} else if (code !== 0) {
99-
reject(new Error(`Process exited with code ${code}: ${stderrData}`));
146+
errorMessage = `Process exited with code ${code}: ${stderrData}`;
100147
} else {
101-
reject(new Error(`Process completed but no valid output: ${stderrData}`));
148+
errorMessage = `Process completed but no valid output: ${stderrData}`;
102149
}
150+
const errorToReject = new Error(errorMessage) as Error & { execution_time_seconds?: number };
151+
errorToReject.execution_time_seconds = execution_time_seconds;
152+
reject(errorToReject);
103153
});
104154

105155
// Write the query to stdin and close
@@ -109,7 +159,11 @@ export class AXQueryExecutor {
109159

110160
} catch (error) {
111161
logger.error('Failed to execute AX utility:', { error });
112-
reject(new Error(`Failed to execute AX utility: ${error}`));
162+
const endTime = Date.now();
163+
const execution_time_seconds = parseFloat(((endTime - startTime) / 1000).toFixed(3));
164+
const errorToReject = new Error(`Failed to execute AX utility: ${error instanceof Error ? error.message : String(error)}`) as Error & { execution_time_seconds?: number };
165+
errorToReject.execution_time_seconds = execution_time_seconds;
166+
reject(errorToReject);
113167
}
114168
});
115169
}

src/schemas.ts

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -67,25 +67,31 @@ export type GetScriptingTipsInput = z.infer<typeof GetScriptingTipsInputSchema>;
6767

6868
// AX Query Input Schema
6969
export const AXQueryInputSchema = z.object({
70-
cmd: z.enum(['query', 'perform']),
71-
multi: z.boolean().optional(),
70+
command: z.enum(['query', 'perform']).describe('The operation to perform. (Formerly cmd)'),
71+
return_all_matches: z.boolean().optional().describe('When true, returns all matching elements rather than just the first match. Default is false. (Formerly multi)'),
7272
locator: z.object({
7373
app: z.string().describe('Bundle ID or display name of the application to query'),
7474
role: z.string().describe('Accessibility role to match, e.g., "AXButton", "AXStaticText"'),
7575
match: z.record(z.string()).describe('Attributes to match for the element'),
76-
pathHint: z.array(z.string()).optional().describe('Optional path to navigate within the application hierarchy, e.g., ["window[1]", "toolbar[1]"]'),
76+
navigation_path_hint: z.array(z.string()).optional().describe('Optional path to navigate within the application hierarchy, e.g., ["window[1]", "toolbar[1]"]. (Formerly pathHint)'),
7777
}),
78-
attributes: z.array(z.string()).optional().describe('Attributes to query for matched elements. If not provided, common attributes will be included'),
79-
requireAction: z.string().optional().describe('Filter elements to only those supporting this action, e.g., "AXPress"'),
80-
action: z.string().optional().describe('Only used with cmd: "perform" - The action to perform on the matched element'),
78+
attributes_to_query: z.array(z.string()).optional().describe('Attributes to query for matched elements. If not provided, common attributes will be included. (Formerly attributes)'),
79+
required_action_name: z.string().optional().describe('Filter elements to only those supporting this action, e.g., "AXPress". (Formerly requireAction)'),
80+
action_to_perform: z.string().optional().describe('Only used with command: "perform" - The action to perform on the matched element. (Formerly action)'),
81+
report_execution_time: z.boolean().optional().default(false).describe(
82+
'If true, the tool will return an additional message containing the formatted script execution time. Defaults to false.',
83+
),
84+
limit: z.number().int().positive().optional().default(500).describe(
85+
'Maximum number of lines to return in the output. Defaults to 500. Output will be truncated if it exceeds this limit.'
86+
)
8187
}).refine(
8288
(data) => {
83-
// If cmd is 'perform', action must be provided
84-
return data.cmd !== 'perform' || (!!data.action);
89+
// If command is 'perform', action_to_perform must be provided
90+
return data.command !== 'perform' || (!!data.action_to_perform);
8591
},
8692
{
87-
message: "When cmd is 'perform', an action must be provided",
88-
path: ["action"],
93+
message: "When command is 'perform', an action_to_perform must be provided",
94+
path: ["action_to_perform"],
8995
}
9096
);
9197

0 commit comments

Comments
 (0)