autohand-cli 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 (48) hide show
  1. package/README.md +134 -0
  2. package/dist/agents-RB34F4XE.js +9 -0
  3. package/dist/agents-new-5I3B2W2I.js +9 -0
  4. package/dist/chunk-2EPIFDFM.js +68 -0
  5. package/dist/chunk-2NUX2RAI.js +145 -0
  6. package/dist/chunk-2QAL3HH4.js +79 -0
  7. package/dist/chunk-4UISIRMD.js +288 -0
  8. package/dist/chunk-55DQY6B5.js +49 -0
  9. package/dist/chunk-A7HRTONQ.js +382 -0
  10. package/dist/chunk-ALMJANSA.js +197 -0
  11. package/dist/chunk-GSOEIEOU.js +19 -0
  12. package/dist/chunk-I4HVBWYF.js +55 -0
  13. package/dist/chunk-KZ7VMQTC.js +20 -0
  14. package/dist/chunk-OC5YDNFC.js +373 -0
  15. package/dist/chunk-PQJIQBQ5.js +57 -0
  16. package/dist/chunk-PX5AGAEX.js +105 -0
  17. package/dist/chunk-QJ53OSGF.js +60 -0
  18. package/dist/chunk-SVLBJMYO.js +33 -0
  19. package/dist/chunk-TAZJSKFD.js +57 -0
  20. package/dist/chunk-TVWTD63Y.js +50 -0
  21. package/dist/chunk-UW2LYWIM.js +131 -0
  22. package/dist/chunk-VRI7EXV6.js +20 -0
  23. package/dist/chunk-XDVG3NM4.js +339 -0
  24. package/dist/chunk-YWKZF2SA.js +364 -0
  25. package/dist/chunk-ZWS3KSMK.js +30 -0
  26. package/dist/completion-Y42FKDT3.js +10 -0
  27. package/dist/export-WJ5P6E5Z.js +8 -0
  28. package/dist/feedback-NEDFOKMA.js +9 -0
  29. package/dist/formatters-UG6VZJJ5.js +8 -0
  30. package/dist/help-CNOV6OXY.js +10 -0
  31. package/dist/index.cjs +13418 -0
  32. package/dist/index.d.cts +1 -0
  33. package/dist/index.d.ts +1 -0
  34. package/dist/index.js +10450 -0
  35. package/dist/init-DML7AOII.js +8 -0
  36. package/dist/lint-TA2ZHVLM.js +8 -0
  37. package/dist/login-GPXDNB2F.js +10 -0
  38. package/dist/logout-43W7N6JU.js +10 -0
  39. package/dist/memory-4GSP7NKV.js +8 -0
  40. package/dist/model-HKEFSH5E.js +8 -0
  41. package/dist/new-EEZC4XXV.js +8 -0
  42. package/dist/quit-RSYIERO5.js +8 -0
  43. package/dist/resume-2NERFSTD.js +8 -0
  44. package/dist/session-H5QWKE5E.js +8 -0
  45. package/dist/sessions-4KXIT76T.js +8 -0
  46. package/dist/status-XAJH67SE.js +8 -0
  47. package/dist/undo-7QJBXARS.js +8 -0
  48. package/package.json +69 -0
@@ -0,0 +1,50 @@
1
+ // src/commands/help.ts
2
+ import chalk from "chalk";
3
+ import terminalLink from "terminal-link";
4
+ async function help() {
5
+ console.log(chalk.cyan("\n\u{1F4DA} Available Commands:\n"));
6
+ const commands = [
7
+ { cmd: "/quit", desc: "Exit Autohand" },
8
+ { cmd: "/model", desc: "Configure providers (OpenRouter, Ollama, OpenAI, llama.cpp)" },
9
+ { cmd: "/session", desc: "Show current session info" },
10
+ { cmd: "/sessions", desc: "List sessions" },
11
+ { cmd: "/resume", desc: "Resume a session by id" },
12
+ { cmd: "/init", desc: "Create AGENTS.md file" },
13
+ { cmd: "/agents", desc: "List available sub-agents" },
14
+ { cmd: "/feedback", desc: "Send feedback with env details" },
15
+ { cmd: "/help / ?", desc: "Show this help" }
16
+ ];
17
+ commands.forEach(({ cmd, desc }) => {
18
+ console.log(` ${chalk.yellow(cmd.padEnd(14))} ${chalk.gray(desc)}`);
19
+ });
20
+ console.log(chalk.cyan("\n\u{1F4A1} Tips:\n"));
21
+ console.log(chalk.gray(" \u2022 Type @ to mention files for the AI"));
22
+ console.log(chalk.gray(" \u2022 Use arrow keys to navigate file suggestions"));
23
+ console.log(chalk.gray(" \u2022 Press Tab to autocomplete file paths"));
24
+ console.log(chalk.gray(" \u2022 Press Esc to cancel current operation\n"));
25
+ const docLink = terminalLink("docs.autohand.ai", "https://docs.autohand.ai");
26
+ console.log(chalk.gray(`For more information, visit ${docLink}
27
+ `));
28
+ return null;
29
+ }
30
+ var metadata = {
31
+ command: "/help",
32
+ description: "describe available slash commands and tips",
33
+ implemented: true
34
+ };
35
+ var aliasMetadata = {
36
+ command: "/?",
37
+ description: "alias for /help",
38
+ implemented: true
39
+ };
40
+
41
+ export {
42
+ help,
43
+ metadata,
44
+ aliasMetadata
45
+ };
46
+ /**
47
+ * @license
48
+ * Copyright 2025 Autohand AI LLC
49
+ * SPDX-License-Identifier: Apache-2.0
50
+ */
@@ -0,0 +1,131 @@
1
+ import {
2
+ getAuthClient,
3
+ saveConfig
4
+ } from "./chunk-A7HRTONQ.js";
5
+ import {
6
+ AUTH_CONFIG
7
+ } from "./chunk-2EPIFDFM.js";
8
+
9
+ // src/commands/login.ts
10
+ import chalk from "chalk";
11
+ import enquirer from "enquirer";
12
+ var metadata = {
13
+ command: "/login",
14
+ description: "sign in to your Autohand account",
15
+ implemented: true
16
+ };
17
+ async function openBrowser(url) {
18
+ try {
19
+ const open = await import("open").then((m) => m.default).catch(() => null);
20
+ if (open) {
21
+ await open(url);
22
+ return true;
23
+ }
24
+ const { exec } = await import("child_process");
25
+ const { promisify } = await import("util");
26
+ const execAsync = promisify(exec);
27
+ const platform = process.platform;
28
+ let command;
29
+ if (platform === "darwin") {
30
+ command = `open "${url}"`;
31
+ } else if (platform === "win32") {
32
+ command = `start "" "${url}"`;
33
+ } else {
34
+ command = `xdg-open "${url}"`;
35
+ }
36
+ await execAsync(command);
37
+ return true;
38
+ } catch {
39
+ return false;
40
+ }
41
+ }
42
+ function sleep(ms) {
43
+ return new Promise((resolve) => setTimeout(resolve, ms));
44
+ }
45
+ async function login(ctx) {
46
+ const config = ctx.config;
47
+ if (config?.auth?.token && config?.auth?.user) {
48
+ const { continueLogin } = await enquirer.prompt({
49
+ type: "confirm",
50
+ name: "continueLogin",
51
+ message: `Already logged in as ${chalk.cyan(config.auth.user.email)}. Log in with a different account?`,
52
+ initial: false
53
+ });
54
+ if (!continueLogin) {
55
+ console.log(chalk.gray("Login cancelled."));
56
+ return null;
57
+ }
58
+ }
59
+ const authClient = getAuthClient();
60
+ console.log(chalk.gray("Initiating authentication..."));
61
+ const initResult = await authClient.initiateDeviceAuth();
62
+ if (!initResult.success || !initResult.deviceCode || !initResult.userCode) {
63
+ console.log(chalk.red(`Failed to start login: ${initResult.error || "Unknown error"}`));
64
+ return null;
65
+ }
66
+ console.log();
67
+ console.log(chalk.bold("To sign in, visit:"));
68
+ console.log(chalk.cyan.underline(initResult.verificationUriComplete || `${AUTH_CONFIG.authorizationUrl}?code=${initResult.userCode}`));
69
+ console.log();
70
+ console.log(chalk.gray("Or enter this code manually:"));
71
+ console.log(chalk.bold.yellow(` ${initResult.userCode}`));
72
+ console.log();
73
+ const browserOpened = await openBrowser(
74
+ initResult.verificationUriComplete || `${AUTH_CONFIG.authorizationUrl}?code=${initResult.userCode}`
75
+ );
76
+ if (browserOpened) {
77
+ console.log(chalk.gray("Browser opened. Complete the login in your browser."));
78
+ } else {
79
+ console.log(chalk.yellow("Could not open browser automatically. Please visit the URL above."));
80
+ }
81
+ console.log();
82
+ console.log(chalk.gray("Waiting for authorization..."));
83
+ console.log(chalk.gray("(Press Ctrl+C to cancel)"));
84
+ const startTime = Date.now();
85
+ const timeout = AUTH_CONFIG.authTimeout;
86
+ const pollInterval = initResult.interval ? initResult.interval * 1e3 : AUTH_CONFIG.pollInterval;
87
+ let dots = 0;
88
+ const maxDots = 3;
89
+ while (Date.now() - startTime < timeout) {
90
+ process.stdout.write(`\r${chalk.gray("Waiting" + ".".repeat(dots + 1) + " ".repeat(maxDots - dots))}`);
91
+ dots = (dots + 1) % (maxDots + 1);
92
+ await sleep(pollInterval);
93
+ const pollResult = await authClient.pollDeviceAuth(initResult.deviceCode);
94
+ if (pollResult.status === "authorized" && pollResult.token && pollResult.user) {
95
+ process.stdout.write("\r" + " ".repeat(20) + "\r");
96
+ const expiresAt = new Date(Date.now() + AUTH_CONFIG.sessionExpiryDays * 24 * 60 * 60 * 1e3).toISOString();
97
+ const updatedConfig = {
98
+ ...config,
99
+ auth: {
100
+ token: pollResult.token,
101
+ user: pollResult.user,
102
+ expiresAt
103
+ }
104
+ };
105
+ await saveConfig(updatedConfig);
106
+ console.log();
107
+ console.log(chalk.green("Login successful!"));
108
+ console.log(chalk.cyan(`Welcome, ${pollResult.user.name || pollResult.user.email}!`));
109
+ console.log();
110
+ return null;
111
+ }
112
+ if (pollResult.status === "expired") {
113
+ process.stdout.write("\r" + " ".repeat(20) + "\r");
114
+ console.log(chalk.red("Authorization code expired. Please try again."));
115
+ return null;
116
+ }
117
+ }
118
+ process.stdout.write("\r" + " ".repeat(20) + "\r");
119
+ console.log(chalk.red("Authorization timed out. Please try again."));
120
+ return null;
121
+ }
122
+
123
+ export {
124
+ metadata,
125
+ login
126
+ };
127
+ /**
128
+ * @license
129
+ * Copyright 2025 Autohand AI LLC
130
+ * SPDX-License-Identifier: Apache-2.0
131
+ */
@@ -0,0 +1,20 @@
1
+ // src/commands/init.ts
2
+ async function init(ctx) {
3
+ await ctx.createAgentsFile();
4
+ return null;
5
+ }
6
+ var metadata = {
7
+ command: "/init",
8
+ description: "create an AGENTS.md file with instructions for Autohand",
9
+ implemented: true
10
+ };
11
+
12
+ export {
13
+ init,
14
+ metadata
15
+ };
16
+ /**
17
+ * @license
18
+ * Copyright 2025 Autohand AI LLC
19
+ * SPDX-License-Identifier: Apache-2.0
20
+ */
@@ -0,0 +1,339 @@
1
+ // src/commands/formatters.ts
2
+ import chalk from "chalk";
3
+
4
+ // src/actions/formatters.ts
5
+ import { spawn } from "child_process";
6
+ import path from "path";
7
+ var EXTERNAL_FORMATTERS = {
8
+ prettier: {
9
+ name: "prettier",
10
+ command: "prettier",
11
+ extensions: [".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs", ".json", ".css", ".scss", ".less", ".html", ".md", ".yaml", ".yml", ".graphql"],
12
+ description: "Opinionated code formatter for JavaScript, TypeScript, CSS, and more",
13
+ checkCmd: ["prettier", "--version"]
14
+ },
15
+ black: {
16
+ name: "black",
17
+ command: "black",
18
+ extensions: [".py", ".pyi"],
19
+ description: "The uncompromising Python code formatter",
20
+ checkCmd: ["black", "--version"]
21
+ },
22
+ rustfmt: {
23
+ name: "rustfmt",
24
+ command: "rustfmt",
25
+ extensions: [".rs"],
26
+ description: "Format Rust code according to style guidelines",
27
+ checkCmd: ["rustfmt", "--version"]
28
+ },
29
+ gofmt: {
30
+ name: "gofmt",
31
+ command: "gofmt",
32
+ extensions: [".go"],
33
+ description: "Format Go source code",
34
+ checkCmd: ["gofmt", "-h"]
35
+ },
36
+ clangformat: {
37
+ name: "clang-format",
38
+ command: "clang-format",
39
+ extensions: [".c", ".cpp", ".h", ".hpp", ".cc", ".cxx"],
40
+ description: "Format C/C++ code",
41
+ checkCmd: ["clang-format", "--version"]
42
+ },
43
+ shfmt: {
44
+ name: "shfmt",
45
+ command: "shfmt",
46
+ extensions: [".sh", ".bash"],
47
+ description: "Format shell scripts",
48
+ checkCmd: ["shfmt", "--version"]
49
+ },
50
+ sqlformat: {
51
+ name: "sqlformat",
52
+ command: "sqlformat",
53
+ extensions: [".sql"],
54
+ description: "Format SQL files",
55
+ checkCmd: ["sqlformat", "--version"]
56
+ },
57
+ xmllint: {
58
+ name: "xmllint",
59
+ command: "xmllint",
60
+ extensions: [".xml", ".xsl", ".xslt"],
61
+ description: "Format XML files",
62
+ checkCmd: ["xmllint", "--version"]
63
+ }
64
+ };
65
+ async function isCommandAvailable(command) {
66
+ return new Promise((resolve) => {
67
+ const proc = spawn(command, ["--version"], {
68
+ stdio: "ignore",
69
+ shell: process.platform === "win32"
70
+ });
71
+ proc.on("error", () => resolve(false));
72
+ proc.on("close", (code) => resolve(code === 0));
73
+ setTimeout(() => {
74
+ proc.kill();
75
+ resolve(false);
76
+ }, 2e3);
77
+ });
78
+ }
79
+ async function runExternalFormatter(command, args, input, cwd) {
80
+ return new Promise((resolve) => {
81
+ const proc = spawn(command, args, {
82
+ cwd,
83
+ shell: process.platform === "win32",
84
+ stdio: ["pipe", "pipe", "pipe"]
85
+ });
86
+ let stdout = "";
87
+ let stderr = "";
88
+ proc.stdout.on("data", (data) => {
89
+ stdout += data.toString();
90
+ });
91
+ proc.stderr.on("data", (data) => {
92
+ stderr += data.toString();
93
+ });
94
+ proc.on("error", (err) => {
95
+ resolve({
96
+ success: false,
97
+ output: input,
98
+ error: `Failed to run ${command}: ${err.message}`
99
+ });
100
+ });
101
+ proc.on("close", (code) => {
102
+ if (code === 0) {
103
+ resolve({ success: true, output: stdout || input });
104
+ } else {
105
+ resolve({
106
+ success: false,
107
+ output: input,
108
+ error: stderr || `${command} exited with code ${code}`
109
+ });
110
+ }
111
+ });
112
+ proc.stdin.write(input);
113
+ proc.stdin.end();
114
+ setTimeout(() => {
115
+ proc.kill();
116
+ resolve({
117
+ success: false,
118
+ output: input,
119
+ error: `${command} timed out after 30 seconds`
120
+ });
121
+ }, 3e4);
122
+ });
123
+ }
124
+ async function formatWithPrettier(contents, file, workspaceRoot) {
125
+ const ext = path.extname(file);
126
+ const parser = getPrettierParser(ext);
127
+ const result = await runExternalFormatter(
128
+ "prettier",
129
+ ["--stdin-filepath", file, ...parser ? ["--parser", parser] : []],
130
+ contents,
131
+ workspaceRoot
132
+ );
133
+ if (!result.success) {
134
+ throw new Error(result.error || "Prettier formatting failed");
135
+ }
136
+ return result.output;
137
+ }
138
+ function getPrettierParser(ext) {
139
+ const parserMap = {
140
+ ".js": "babel",
141
+ ".jsx": "babel",
142
+ ".ts": "typescript",
143
+ ".tsx": "typescript",
144
+ ".mjs": "babel",
145
+ ".cjs": "babel",
146
+ ".json": "json",
147
+ ".css": "css",
148
+ ".scss": "scss",
149
+ ".less": "less",
150
+ ".html": "html",
151
+ ".md": "markdown",
152
+ ".yaml": "yaml",
153
+ ".yml": "yaml",
154
+ ".graphql": "graphql"
155
+ };
156
+ return parserMap[ext] || null;
157
+ }
158
+ async function formatWithBlack(contents, file, workspaceRoot) {
159
+ const result = await runExternalFormatter(
160
+ "black",
161
+ ["-", "--quiet"],
162
+ contents,
163
+ workspaceRoot
164
+ );
165
+ if (!result.success) {
166
+ throw new Error(result.error || "Black formatting failed");
167
+ }
168
+ return result.output;
169
+ }
170
+ async function formatWithRustfmt(contents, file, workspaceRoot) {
171
+ const result = await runExternalFormatter(
172
+ "rustfmt",
173
+ ["--emit", "stdout"],
174
+ contents,
175
+ workspaceRoot
176
+ );
177
+ if (!result.success) {
178
+ throw new Error(result.error || "rustfmt formatting failed");
179
+ }
180
+ return result.output;
181
+ }
182
+ async function formatWithGofmt(contents, file, workspaceRoot) {
183
+ const result = await runExternalFormatter(
184
+ "gofmt",
185
+ [],
186
+ contents,
187
+ workspaceRoot
188
+ );
189
+ if (!result.success) {
190
+ throw new Error(result.error || "gofmt formatting failed");
191
+ }
192
+ return result.output;
193
+ }
194
+ async function formatWithClangFormat(contents, file, workspaceRoot) {
195
+ const result = await runExternalFormatter(
196
+ "clang-format",
197
+ [`--assume-filename=${file}`],
198
+ contents,
199
+ workspaceRoot
200
+ );
201
+ if (!result.success) {
202
+ throw new Error(result.error || "clang-format formatting failed");
203
+ }
204
+ return result.output;
205
+ }
206
+ async function formatWithShfmt(contents, file, workspaceRoot) {
207
+ const result = await runExternalFormatter(
208
+ "shfmt",
209
+ ["-i", "2"],
210
+ // 2-space indent
211
+ contents,
212
+ workspaceRoot
213
+ );
214
+ if (!result.success) {
215
+ throw new Error(result.error || "shfmt formatting failed");
216
+ }
217
+ return result.output;
218
+ }
219
+ var builtinFormatters = {
220
+ json: async (contents) => {
221
+ const parsed = JSON.parse(contents);
222
+ return JSON.stringify(parsed, null, 2) + "\n";
223
+ },
224
+ trim: async (contents) => contents.trim() + "\n",
225
+ "normalize-newlines": async (contents) => {
226
+ return contents.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
227
+ },
228
+ "trailing-newline": async (contents) => {
229
+ return contents.endsWith("\n") ? contents : contents + "\n";
230
+ },
231
+ "remove-trailing-whitespace": async (contents) => {
232
+ return contents.split("\n").map((line) => line.trimEnd()).join("\n");
233
+ }
234
+ };
235
+ var externalFormatters = {
236
+ prettier: formatWithPrettier,
237
+ black: formatWithBlack,
238
+ rustfmt: formatWithRustfmt,
239
+ gofmt: formatWithGofmt,
240
+ "clang-format": formatWithClangFormat,
241
+ clangformat: formatWithClangFormat,
242
+ shfmt: formatWithShfmt
243
+ };
244
+ async function checkAvailableFormatters() {
245
+ const results = {};
246
+ for (const name of Object.keys(builtinFormatters)) {
247
+ results[name] = true;
248
+ }
249
+ const checks = Object.entries(EXTERNAL_FORMATTERS).map(async ([name, info]) => {
250
+ const available = await isCommandAvailable(info.command);
251
+ results[name] = available;
252
+ });
253
+ await Promise.all(checks);
254
+ return results;
255
+ }
256
+ async function listFormatters() {
257
+ const available = await checkAvailableFormatters();
258
+ const formatters = [
259
+ // Built-in formatters
260
+ { name: "json", command: "built-in", extensions: [".json"], description: "Format JSON with 2-space indent", checkCmd: [], installed: true },
261
+ { name: "trim", command: "built-in", extensions: ["*"], description: "Trim whitespace and ensure trailing newline", checkCmd: [], installed: true },
262
+ { name: "normalize-newlines", command: "built-in", extensions: ["*"], description: "Convert all line endings to LF", checkCmd: [], installed: true },
263
+ { name: "trailing-newline", command: "built-in", extensions: ["*"], description: "Ensure file ends with newline", checkCmd: [], installed: true },
264
+ { name: "remove-trailing-whitespace", command: "built-in", extensions: ["*"], description: "Remove trailing whitespace from lines", checkCmd: [], installed: true },
265
+ // External formatters
266
+ ...Object.entries(EXTERNAL_FORMATTERS).map(([name, info]) => ({
267
+ ...info,
268
+ installed: available[name] ?? false
269
+ }))
270
+ ];
271
+ return formatters;
272
+ }
273
+ async function applyFormatter(name, contents, file, workspaceRoot) {
274
+ const builtin = builtinFormatters[name];
275
+ if (builtin) {
276
+ return builtin(contents, file, workspaceRoot);
277
+ }
278
+ const external = externalFormatters[name];
279
+ if (external) {
280
+ return external(contents, file, workspaceRoot);
281
+ }
282
+ throw new Error(`Formatter "${name}" is not available. Run /formatters to see available formatters.`);
283
+ }
284
+
285
+ // src/commands/formatters.ts
286
+ var metadata = {
287
+ command: "/formatters",
288
+ description: "List available code formatters",
289
+ implemented: true
290
+ };
291
+ async function execute() {
292
+ console.log();
293
+ console.log(chalk.cyan.bold("Available Code Formatters"));
294
+ console.log(chalk.gray("\u2500".repeat(60)));
295
+ console.log();
296
+ const formatters = await listFormatters();
297
+ const builtIn = formatters.filter((f) => f.command === "built-in");
298
+ const external = formatters.filter((f) => f.command !== "built-in");
299
+ console.log(chalk.yellow.bold("Built-in Formatters (always available):"));
300
+ console.log();
301
+ for (const f of builtIn) {
302
+ console.log(` ${chalk.green("\u2713")} ${chalk.white.bold(f.name)}`);
303
+ console.log(` ${chalk.gray(f.description)}`);
304
+ console.log(` ${chalk.gray("Extensions:")} ${f.extensions.join(", ")}`);
305
+ console.log();
306
+ }
307
+ console.log(chalk.yellow.bold("External Formatters:"));
308
+ console.log();
309
+ for (const f of external) {
310
+ const status = f.installed ? chalk.green("\u2713 installed") : chalk.red("\u2717 not found");
311
+ console.log(` ${f.installed ? chalk.green("\u2713") : chalk.red("\u2717")} ${chalk.white.bold(f.name)} ${chalk.gray(`(${f.command})`)} - ${status}`);
312
+ console.log(` ${chalk.gray(f.description)}`);
313
+ console.log(` ${chalk.gray("Extensions:")} ${f.extensions.join(", ")}`);
314
+ console.log();
315
+ }
316
+ console.log(chalk.gray("\u2500".repeat(60)));
317
+ console.log(chalk.gray("Usage: The agent can use format_file action with any installed formatter."));
318
+ console.log(chalk.gray("Install missing formatters via your package manager (npm, pip, cargo, etc.)"));
319
+ console.log();
320
+ }
321
+
322
+ export {
323
+ applyFormatter,
324
+ metadata,
325
+ execute
326
+ };
327
+ /**
328
+ * @license
329
+ * Copyright 2025 Autohand AI LLC
330
+ * SPDX-License-Identifier: Apache-2.0
331
+ *
332
+ * Code Formatters
333
+ * Supports prettier, black, rustfmt, gofmt, and more
334
+ */
335
+ /**
336
+ * @license
337
+ * Copyright 2025 Autohand AI LLC
338
+ * SPDX-License-Identifier: Apache-2.0
339
+ */