ai-cmd 1.0.2 → 1.0.3

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 (49) hide show
  1. package/README.md +89 -10
  2. package/dist/analytics/client.d.ts +2 -0
  3. package/dist/analytics/client.js +95 -0
  4. package/dist/analytics/client.js.map +1 -0
  5. package/dist/analytics/session.d.ts +15 -0
  6. package/dist/analytics/session.js +70 -0
  7. package/dist/analytics/session.js.map +1 -0
  8. package/dist/cli/commands.d.ts +2 -1
  9. package/dist/cli/commands.js +134 -90
  10. package/dist/cli/commands.js.map +1 -1
  11. package/dist/cli/repl.d.ts +4 -1
  12. package/dist/cli/repl.js +19 -1
  13. package/dist/cli/repl.js.map +1 -1
  14. package/dist/config/configurator.d.ts +18 -0
  15. package/dist/config/configurator.js +106 -0
  16. package/dist/config/configurator.js.map +1 -0
  17. package/dist/config/providerCatalog.d.ts +8 -0
  18. package/dist/config/providerCatalog.js +33 -0
  19. package/dist/config/providerCatalog.js.map +1 -0
  20. package/dist/config/userConfig.d.ts +2 -0
  21. package/dist/config/userConfig.js +68 -18
  22. package/dist/config/userConfig.js.map +1 -1
  23. package/dist/core/generateCommand.js +7 -2
  24. package/dist/core/generateCommand.js.map +1 -1
  25. package/dist/core/prompts.d.ts +1 -0
  26. package/dist/core/prompts.js +4 -0
  27. package/dist/core/prompts.js.map +1 -1
  28. package/dist/providers/anthropic.d.ts +8 -0
  29. package/dist/providers/anthropic.js +67 -0
  30. package/dist/providers/anthropic.js.map +1 -0
  31. package/dist/providers/factory.js +16 -1
  32. package/dist/providers/factory.js.map +1 -1
  33. package/dist/providers/google.d.ts +8 -0
  34. package/dist/providers/google.js +76 -0
  35. package/dist/providers/google.js.map +1 -0
  36. package/dist/providers/ollama.d.ts +8 -0
  37. package/dist/providers/ollama.js +67 -0
  38. package/dist/providers/ollama.js.map +1 -0
  39. package/dist/providers/openai.d.ts +2 -2
  40. package/dist/providers/openai.js +10 -6
  41. package/dist/providers/openai.js.map +1 -1
  42. package/dist/types/index.d.ts +28 -2
  43. package/dist/utils/branding.d.ts +1 -1
  44. package/dist/utils/branding.js +7 -2
  45. package/dist/utils/branding.js.map +1 -1
  46. package/dist/workspace/inspectWorkspace.d.ts +1 -0
  47. package/dist/workspace/inspectWorkspace.js +174 -0
  48. package/dist/workspace/inspectWorkspace.js.map +1 -0
  49. package/package.json +68 -68
package/README.md CHANGED
@@ -44,7 +44,10 @@ This project is part of the **Ottili ONE ecosystem** — a modular AI system for
44
44
  - Clipboard copy support
45
45
  - Optional execution with confirmation
46
46
  - Heuristic risk classification for destructive commands
47
- - OpenAI-compatible provider abstraction
47
+ - First-run configurator for provider, API key, and analytics consent
48
+ - Configurable provider abstraction for OpenAI, Anthropic, Google, Ollama, and vLLM
49
+ - Workspace-aware command generation based on the current folder
50
+ - Opt-in anonymous analytics with separate error reporting
48
51
  - JSON mode for scripting
49
52
 
50
53
  ## Installation
@@ -69,23 +72,68 @@ npm link
69
72
 
70
73
  Environment variables take precedence over the config file.
71
74
 
72
- If no API key is configured yet, `ai-cmd` creates a starter config file for you on first run.
75
+ If required configuration is missing and you launch `ai-cmd` interactively, a first-run configurator opens and asks for:
73
76
 
74
- ### Required
77
+ - AI provider
78
+ - API key
79
+ - analytics consent
75
80
 
76
- ```bash
77
- export AI_API_KEY="your-api-key"
78
- ```
81
+ If you opt in, `config.json` stores `"analytics": true` and a random anonymous install id. If you opt out, it stores `"analytics": false`.
79
82
 
80
- ### Optional
83
+ ### OpenAI
84
+
85
+ OpenAI remains the default provider:
81
86
 
82
87
  ```bash
88
+ export AI_API_KEY="your-api-key"
83
89
  export AI_PROVIDER="openai"
84
90
  export AI_MODEL="gpt-5.4-mini"
85
91
  export AI_BASE_URL="https://api.openai.com/v1"
86
92
  export AI_TIMEOUT_MS="30000"
87
93
  ```
88
94
 
95
+ ### Anthropic
96
+
97
+ ```bash
98
+ export AI_PROVIDER="anthropic"
99
+ export AI_API_KEY="your-anthropic-key"
100
+ export AI_MODEL="claude-sonnet-4-20250514"
101
+ export AI_BASE_URL="https://api.anthropic.com/v1"
102
+ ```
103
+
104
+ ### Google
105
+
106
+ ```bash
107
+ export AI_PROVIDER="google"
108
+ export AI_API_KEY="your-google-ai-key"
109
+ export AI_MODEL="gemini-2.5-flash"
110
+ export AI_BASE_URL="https://generativelanguage.googleapis.com/v1beta"
111
+ ```
112
+
113
+ ### Ollama
114
+
115
+ Use a local Ollama model such as Gemma:
116
+
117
+ ```bash
118
+ export AI_PROVIDER="ollama"
119
+ export AI_MODEL="gemma3:4b"
120
+ export AI_BASE_URL="http://localhost:11434/api"
121
+ ```
122
+
123
+ No API key is required for the default local Ollama setup.
124
+
125
+ ### vLLM
126
+
127
+ Use a local or self-hosted OpenAI-compatible vLLM server:
128
+
129
+ ```bash
130
+ export AI_PROVIDER="vllm"
131
+ export AI_MODEL="google/gemma-3-4b-it"
132
+ export AI_BASE_URL="http://localhost:8000/v1"
133
+ ```
134
+
135
+ No API key is required for a local vLLM server unless you configured auth yourself.
136
+
89
137
  ### Example config file
90
138
 
91
139
  `~/.ai-cmd/config.json`
@@ -96,11 +144,41 @@ export AI_TIMEOUT_MS="30000"
96
144
  "model": "gpt-5.4-mini",
97
145
  "apiKey": "your-api-key",
98
146
  "baseUrl": "https://api.openai.com/v1",
99
- "timeoutMs": 30000
147
+ "timeoutMs": 30000,
148
+ "analytics": false
100
149
  }
101
150
  ```
102
151
 
103
- If configuration is missing, `ai-cmd` fails clearly:
152
+ ### Analytics
153
+
154
+ Analytics are opt-in only.
155
+
156
+ When enabled, `ai-cmd` sends anonymous usage events to:
157
+
158
+ ```text
159
+ https://tracking.ottili.one/api/aicmd
160
+ ```
161
+
162
+ Usage events include:
163
+
164
+ - anonymous install count
165
+ - CLI starts
166
+ - prompt count
167
+
168
+ Error reports can include:
169
+
170
+ - prompt
171
+ - OS
172
+ - version
173
+ - timestamp
174
+
175
+ Regular usage analytics do not store generated command content.
176
+
177
+ Analytics requests now use a short-lived server-issued session plus proof-of-work. This does not create perfect authentication for a public open-source client, but it makes automated spam substantially harder and gives the server something real to validate.
178
+
179
+ Server setup notes live in [docs/analytics-server.md](docs/analytics-server.md).
180
+
181
+ If OpenAI configuration is missing, `ai-cmd` fails clearly:
104
182
 
105
183
  ```text
106
184
  Missing AI_API_KEY. Set it in your environment or edit ~/.ai-cmd/config.json. A starter config has been created if it did not already exist.
@@ -118,6 +196,8 @@ ai "remove node_modules and reinstall packages" --exec
118
196
  ai "find all jpg files" --copy
119
197
  ```
120
198
 
199
+ `ai-cmd` now inspects the current folder structure and common project files like `package.json`, `Makefile`, `Cargo.toml`, `go.mod`, and compose files so it can suggest commands that fit the active workspace better.
200
+
121
201
  ### Interactive mode
122
202
 
123
203
  ```bash
@@ -263,7 +343,6 @@ Example shape:
263
343
 
264
344
  ## Roadmap
265
345
 
266
- - additional provider adapters
267
346
  - richer shell support
268
347
  - more precise filesystem-aware risk detection
269
348
  - Homebrew distribution
@@ -0,0 +1,2 @@
1
+ import type { AnalyticsClient, AppConfig } from "../types/index.js";
2
+ export declare function createAnalyticsClient(config: AppConfig): AnalyticsClient;
@@ -0,0 +1,95 @@
1
+ import { APP_NAME, APP_VERSION } from "../utils/branding.js";
2
+ import { createAnalyticsProof, createAnalyticsSession, isAnalyticsSessionFresh } from "./session.js";
3
+ const TRACKING_BASE_URL = "https://tracking.ottili.one/api/aicmd";
4
+ function createSessionGetter(config) {
5
+ let cachedSession;
6
+ let sessionPromise;
7
+ return async () => {
8
+ if (isAnalyticsSessionFresh(cachedSession)) {
9
+ return cachedSession;
10
+ }
11
+ if (!sessionPromise) {
12
+ sessionPromise = createAnalyticsSession(config).then((session) => {
13
+ cachedSession = session;
14
+ sessionPromise = undefined;
15
+ return session;
16
+ });
17
+ }
18
+ return sessionPromise;
19
+ };
20
+ }
21
+ async function postJson(path, payload, config, getSession) {
22
+ const controller = new AbortController();
23
+ const timeout = setTimeout(() => controller.abort(), 1_500);
24
+ try {
25
+ const session = await getSession();
26
+ if (!session) {
27
+ return;
28
+ }
29
+ await fetch(`${TRACKING_BASE_URL}${path}`, {
30
+ method: "POST",
31
+ headers: {
32
+ "Content-Type": "application/json",
33
+ "User-Agent": `${APP_NAME}/${APP_VERSION}`,
34
+ "X-AI-CMD-Install-Id": config.analyticsId ?? "",
35
+ "X-AI-CMD-Session-Id": session.sessionId,
36
+ "X-AI-CMD-Session-Expires": session.expiresAt,
37
+ "X-AI-CMD-Session-Signature": session.signature
38
+ },
39
+ body: JSON.stringify({
40
+ payload,
41
+ auth: createAnalyticsProof(session, payload)
42
+ }),
43
+ signal: controller.signal
44
+ });
45
+ }
46
+ catch {
47
+ // Analytics should never block or break the CLI.
48
+ }
49
+ finally {
50
+ clearTimeout(timeout);
51
+ }
52
+ }
53
+ function createNoopAnalyticsClient() {
54
+ return {
55
+ async trackCliStart() { },
56
+ async trackPromptSent() { },
57
+ async trackError() { }
58
+ };
59
+ }
60
+ export function createAnalyticsClient(config) {
61
+ if (!config.analytics || !config.analyticsId) {
62
+ return createNoopAnalyticsClient();
63
+ }
64
+ const getSession = createSessionGetter(config);
65
+ const basePayload = () => ({
66
+ installId: config.analyticsId,
67
+ app: APP_NAME.toLowerCase(),
68
+ version: APP_VERSION,
69
+ time: new Date().toISOString()
70
+ });
71
+ return {
72
+ async trackCliStart(payload) {
73
+ await postJson("/events", {
74
+ ...basePayload(),
75
+ event: "cli_started",
76
+ ...payload
77
+ }, config, getSession);
78
+ },
79
+ async trackPromptSent(payload) {
80
+ await postJson("/events", {
81
+ ...basePayload(),
82
+ event: "prompt_sent",
83
+ ...payload
84
+ }, config, getSession);
85
+ },
86
+ async trackError(payload) {
87
+ await postJson("/errors", {
88
+ ...basePayload(),
89
+ event: "error_reported",
90
+ ...payload
91
+ }, config, getSession);
92
+ }
93
+ };
94
+ }
95
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/analytics/client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ import type { AppConfig } from "../types/index.js";
2
+ export interface AnalyticsSession {
3
+ sessionId: string;
4
+ nonce: string;
5
+ difficulty: number;
6
+ expiresAt: string;
7
+ signature: string;
8
+ }
9
+ export declare function createAnalyticsSession(config: AppConfig): Promise<AnalyticsSession | undefined>;
10
+ export declare function isAnalyticsSessionFresh(session: AnalyticsSession | undefined): session is AnalyticsSession;
11
+ export declare function createAnalyticsProof(session: AnalyticsSession, payload: Record<string, unknown>): {
12
+ payloadHash: string;
13
+ counter: number;
14
+ proof: string;
15
+ };
@@ -0,0 +1,70 @@
1
+ import { createHash } from "node:crypto";
2
+ import { APP_NAME, APP_VERSION } from "../utils/branding.js";
3
+ const TRACKING_BASE_URL = "https://tracking.ottili.one/api/aicmd";
4
+ const SESSION_REFRESH_BUFFER_MS = 60_000;
5
+ function sha256(input) {
6
+ return createHash("sha256").update(input).digest("hex");
7
+ }
8
+ function hasLeadingZeroes(hex, zeroCount) {
9
+ return hex.startsWith("0".repeat(Math.max(0, zeroCount)));
10
+ }
11
+ export async function createAnalyticsSession(config) {
12
+ if (!config.analyticsId) {
13
+ return undefined;
14
+ }
15
+ const controller = new AbortController();
16
+ const timeout = setTimeout(() => controller.abort(), 1_500);
17
+ try {
18
+ const response = await fetch(`${TRACKING_BASE_URL}/session`, {
19
+ method: "POST",
20
+ headers: {
21
+ "Content-Type": "application/json",
22
+ "User-Agent": `${APP_NAME}/${APP_VERSION}`
23
+ },
24
+ body: JSON.stringify({
25
+ installId: config.analyticsId,
26
+ app: APP_NAME.toLowerCase(),
27
+ version: APP_VERSION
28
+ }),
29
+ signal: controller.signal
30
+ });
31
+ if (!response.ok) {
32
+ return undefined;
33
+ }
34
+ return (await response.json());
35
+ }
36
+ catch {
37
+ return undefined;
38
+ }
39
+ finally {
40
+ clearTimeout(timeout);
41
+ }
42
+ }
43
+ export function isAnalyticsSessionFresh(session) {
44
+ if (!session) {
45
+ return false;
46
+ }
47
+ return (new Date(session.expiresAt).getTime() - Date.now() >
48
+ SESSION_REFRESH_BUFFER_MS);
49
+ }
50
+ export function createAnalyticsProof(session, payload) {
51
+ const payloadHash = sha256(JSON.stringify(payload));
52
+ let counter = 0;
53
+ while (counter < 250_000) {
54
+ const proof = sha256(`${session.sessionId}:${session.nonce}:${payloadHash}:${counter}`);
55
+ if (hasLeadingZeroes(proof, session.difficulty)) {
56
+ return {
57
+ payloadHash,
58
+ counter,
59
+ proof
60
+ };
61
+ }
62
+ counter += 1;
63
+ }
64
+ return {
65
+ payloadHash,
66
+ counter,
67
+ proof: sha256(`${session.sessionId}:${session.nonce}:${payloadHash}:${counter}`)
68
+ };
69
+ }
70
+ //# sourceMappingURL=session.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session.js","sourceRoot":"","sources":["../../src/analytics/session.ts"],"names":[],"mappings":"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"}
@@ -1,9 +1,10 @@
1
1
  import { runCommand } from "../exec/runCommand.js";
2
- import type { AIProvider, AppConfig, PlatformContext, PromptAdapter } from "../types/index.js";
2
+ import type { AIProvider, AnalyticsClient, AppConfig, PlatformContext, PromptAdapter } from "../types/index.js";
3
3
  export interface CliDependencies {
4
4
  loadConfig: () => Promise<AppConfig>;
5
5
  detectPlatformContext: () => Promise<PlatformContext>;
6
6
  createProvider: (config: AppConfig) => AIProvider;
7
+ createAnalyticsClient: (config: AppConfig) => AnalyticsClient;
7
8
  createPromptAdapter: () => PromptAdapter;
8
9
  copyToClipboard: (command: string) => Promise<void>;
9
10
  commandRunner: typeof runCommand;
@@ -1,8 +1,9 @@
1
1
  import clipboardy from "clipboardy";
2
2
  import { Command, Option } from "commander";
3
+ import { createAnalyticsClient as createDefaultAnalyticsClient } from "../analytics/client.js";
3
4
  import { startRepl } from "./repl.js";
4
5
  import { createPromptAdapter } from "./prompts.js";
5
- import { loadConfig } from "../config/userConfig.js";
6
+ import { loadOrConfigureConfig } from "../config/configurator.js";
6
7
  import { generateCommand } from "../core/generateCommand.js";
7
8
  import { formatSuggestion } from "../core/output.js";
8
9
  import { runCommand } from "../exec/runCommand.js";
@@ -10,14 +11,16 @@ import { detectPlatformContext } from "../platform/detectPlatform.js";
10
11
  import { createProvider } from "../providers/factory.js";
11
12
  import { assessCommandRisk } from "../safety/classifyRisk.js";
12
13
  import { enforceExecutionPolicy } from "../safety/executionPolicy.js";
13
- import { ClipboardError, ConfigurationError, getErrorMessage } from "../utils/errors.js";
14
+ import { ClipboardError, ConfigurationError, ExecutionPolicyError, UserCancelledError, getErrorMessage } from "../utils/errors.js";
14
15
  import { formatVersionBanner } from "../utils/branding.js";
15
16
  import { Logger } from "../utils/logger.js";
17
+ import { inspectWorkspace } from "../workspace/inspectWorkspace.js";
16
18
  export function createDefaultDependencies() {
17
19
  return {
18
- loadConfig: () => loadConfig(),
20
+ loadConfig: () => loadOrConfigureConfig(),
19
21
  detectPlatformContext,
20
22
  createProvider,
23
+ createAnalyticsClient: createDefaultAnalyticsClient,
21
24
  createPromptAdapter,
22
25
  copyToClipboard: async (command) => {
23
26
  try {
@@ -53,6 +56,32 @@ function ensureInteractiveFlagsAreValid(options) {
53
56
  throw new ConfigurationError("--json, --exec, --yes, and --copy require a question in one-shot mode.");
54
57
  }
55
58
  }
59
+ function shouldReportError(error) {
60
+ return !(error instanceof UserCancelledError || error instanceof ExecutionPolicyError);
61
+ }
62
+ async function prepareRuntime(options, deps, mode) {
63
+ const config = await deps.loadConfig();
64
+ const platform = await deps.detectPlatformContext();
65
+ const effectivePlatform = options.shell
66
+ ? { ...platform, shell: options.shell }
67
+ : platform;
68
+ const provider = deps.createProvider(config);
69
+ const analytics = deps.createAnalyticsClient(config);
70
+ const workspaceContext = await inspectWorkspace(effectivePlatform.cwd);
71
+ await analytics.trackCliStart({
72
+ os: effectivePlatform.os,
73
+ shell: effectivePlatform.shell,
74
+ provider: config.provider,
75
+ mode
76
+ });
77
+ return {
78
+ config,
79
+ effectivePlatform,
80
+ provider,
81
+ analytics,
82
+ ...(workspaceContext ? { workspaceContext } : {})
83
+ };
84
+ }
56
85
  function writeJsonWithExecution(suggestion, execution) {
57
86
  process.stdout.write(`${JSON.stringify({
58
87
  question: suggestion.question,
@@ -65,31 +94,71 @@ function writeJsonWithExecution(suggestion, execution) {
65
94
  execution
66
95
  }, null, 2)}\n`);
67
96
  }
68
- async function handleOneShot(question, options, deps) {
97
+ async function handleOneShot(question, options, deps, runtime) {
69
98
  const logger = new Logger(options.debug);
70
- const config = await deps.loadConfig();
71
- const platform = await deps.detectPlatformContext();
72
- const provider = deps.createProvider(config);
73
- const effectivePlatform = options.shell
74
- ? { ...platform, shell: options.shell }
75
- : platform;
76
- if (options.exec && effectivePlatform.os === "unsupported") {
77
- throw new ConfigurationError("Execution is disabled on unsupported host OSes. Use a Unix-like shell or WSL.");
78
- }
79
- logger.debug("Using platform context", effectivePlatform);
80
- logger.debug("Using provider", {
81
- provider: config.provider,
82
- model: config.model,
83
- baseUrl: config.baseUrl
84
- });
85
- const suggestion = await generateCommand({
86
- question,
87
- platform: effectivePlatform,
88
- provider,
89
- explainRequested: true
90
- });
91
- let execution;
92
- if (options.json) {
99
+ try {
100
+ if (options.exec && runtime.effectivePlatform.os === "unsupported") {
101
+ throw new ConfigurationError("Execution is disabled on unsupported host OSes. Use a Unix-like shell or WSL.");
102
+ }
103
+ logger.debug("Using platform context", runtime.effectivePlatform);
104
+ logger.debug("Using provider", {
105
+ provider: runtime.config.provider,
106
+ model: runtime.config.model,
107
+ baseUrl: runtime.config.baseUrl
108
+ });
109
+ await runtime.analytics.trackPromptSent({
110
+ os: runtime.effectivePlatform.os,
111
+ shell: runtime.effectivePlatform.shell,
112
+ provider: runtime.config.provider,
113
+ mode: "one-shot"
114
+ });
115
+ const suggestion = await generateCommand({
116
+ question,
117
+ platform: runtime.effectivePlatform,
118
+ provider: runtime.provider,
119
+ explainRequested: true,
120
+ ...(runtime.workspaceContext
121
+ ? { workspaceContext: runtime.workspaceContext }
122
+ : {})
123
+ });
124
+ let execution;
125
+ if (options.json) {
126
+ if (options.copy) {
127
+ try {
128
+ await deps.copyToClipboard(suggestion.command);
129
+ process.stderr.write("Command copied to clipboard.\n");
130
+ }
131
+ catch (error) {
132
+ if (error instanceof ClipboardError) {
133
+ process.stderr.write(`${error.message}\n${suggestion.command}\n`);
134
+ }
135
+ else {
136
+ throw error;
137
+ }
138
+ }
139
+ }
140
+ if (options.exec) {
141
+ const assessment = assessCommandRisk(suggestion.command);
142
+ await enforceExecutionPolicy({
143
+ command: suggestion.command,
144
+ risk: assessment.level,
145
+ yes: options.yes,
146
+ prompt: deps.createPromptAdapter(),
147
+ ...(assessment.reasons[0] ? { reason: assessment.reasons[0] } : {})
148
+ });
149
+ execution = await deps.commandRunner(suggestion.command, {
150
+ cwd: runtime.effectivePlatform.cwd,
151
+ stdio: "pipe"
152
+ });
153
+ }
154
+ writeJsonWithExecution(suggestion, execution);
155
+ return;
156
+ }
157
+ process.stdout.write(`${formatSuggestion(suggestion, {
158
+ color: !options.noColor,
159
+ explain: true,
160
+ json: false
161
+ })}\n`);
93
162
  if (options.copy) {
94
163
  try {
95
164
  await deps.copyToClipboard(suggestion.command);
@@ -104,57 +173,36 @@ async function handleOneShot(question, options, deps) {
104
173
  }
105
174
  }
106
175
  }
107
- if (options.exec) {
108
- const assessment = assessCommandRisk(suggestion.command);
109
- await enforceExecutionPolicy({
110
- command: suggestion.command,
111
- risk: assessment.level,
112
- yes: options.yes,
113
- prompt: deps.createPromptAdapter(),
114
- ...(assessment.reasons[0] ? { reason: assessment.reasons[0] } : {})
115
- });
116
- execution = await deps.commandRunner(suggestion.command, {
117
- cwd: effectivePlatform.cwd,
118
- stdio: "pipe"
119
- });
176
+ if (!options.exec) {
177
+ return;
120
178
  }
121
- writeJsonWithExecution(suggestion, execution);
122
- return;
179
+ const assessment = assessCommandRisk(suggestion.command);
180
+ await enforceExecutionPolicy({
181
+ command: suggestion.command,
182
+ risk: assessment.level,
183
+ yes: options.yes,
184
+ prompt: deps.createPromptAdapter(),
185
+ ...(assessment.reasons[0] ? { reason: assessment.reasons[0] } : {})
186
+ });
187
+ await deps.commandRunner(suggestion.command, {
188
+ cwd: runtime.effectivePlatform.cwd,
189
+ stdio: "inherit"
190
+ });
123
191
  }
124
- process.stdout.write(`${formatSuggestion(suggestion, {
125
- color: !options.noColor,
126
- explain: true,
127
- json: false
128
- })}\n`);
129
- if (options.copy) {
130
- try {
131
- await deps.copyToClipboard(suggestion.command);
132
- process.stderr.write("Command copied to clipboard.\n");
133
- }
134
- catch (error) {
135
- if (error instanceof ClipboardError) {
136
- process.stderr.write(`${error.message}\n${suggestion.command}\n`);
137
- }
138
- else {
139
- throw error;
140
- }
192
+ catch (error) {
193
+ if (shouldReportError(error)) {
194
+ await runtime.analytics.trackError({
195
+ prompt: question,
196
+ os: runtime.effectivePlatform.os,
197
+ shell: runtime.effectivePlatform.shell,
198
+ provider: runtime.config.provider,
199
+ message: getErrorMessage(error),
200
+ time: new Date().toISOString(),
201
+ ...(error instanceof Error ? { code: error.name } : {})
202
+ });
141
203
  }
204
+ throw error;
142
205
  }
143
- if (!options.exec) {
144
- return;
145
- }
146
- const assessment = assessCommandRisk(suggestion.command);
147
- await enforceExecutionPolicy({
148
- command: suggestion.command,
149
- risk: assessment.level,
150
- yes: options.yes,
151
- prompt: deps.createPromptAdapter(),
152
- ...(assessment.reasons[0] ? { reason: assessment.reasons[0] } : {})
153
- });
154
- await deps.commandRunner(suggestion.command, {
155
- cwd: effectivePlatform.cwd,
156
- stdio: "inherit"
157
- });
158
206
  }
159
207
  export async function runCli(argv = process.argv, dependencies = createDefaultDependencies()) {
160
208
  const program = new Command();
@@ -166,11 +214,7 @@ export async function runCli(argv = process.argv, dependencies = createDefaultDe
166
214
  .option("--yes", "Skip the standard confirmation prompt for low/medium-risk commands")
167
215
  .option("--explain", "Show the explanation alongside the generated command")
168
216
  .option("--json", "Emit machine-readable JSON")
169
- .addOption(new Option("--shell <shell>", "Shell hint for command generation").choices([
170
- "bash",
171
- "zsh",
172
- "sh"
173
- ]))
217
+ .addOption(new Option("--shell <shell>", "Shell hint for command generation").choices(["bash", "zsh", "sh"]))
174
218
  .option("-v, --version", "Show branded version information")
175
219
  .option("--copy", "Copy the generated command to the clipboard")
176
220
  .option("--no-color", "Disable colored output")
@@ -188,22 +232,23 @@ export async function runCli(argv = process.argv, dependencies = createDefaultDe
188
232
  if (!question) {
189
233
  ensureInteractiveFlagsAreValid(options);
190
234
  const logger = new Logger(options.debug);
191
- const config = await dependencies.loadConfig();
192
- const platform = await dependencies.detectPlatformContext();
193
- const provider = dependencies.createProvider(config);
194
- const effectivePlatform = options.shell
195
- ? { ...platform, shell: options.shell }
196
- : platform;
235
+ const runtime = await prepareRuntime(options, dependencies, "interactive");
197
236
  await startRepl({
198
- platform: effectivePlatform,
199
- provider,
237
+ platform: runtime.effectivePlatform,
238
+ provider: runtime.provider,
239
+ providerName: runtime.config.provider,
200
240
  prompt: dependencies.createPromptAdapter(),
241
+ analytics: runtime.analytics,
242
+ ...(runtime.workspaceContext
243
+ ? { workspaceContext: runtime.workspaceContext }
244
+ : {}),
201
245
  color: !options.noColor,
202
246
  logger
203
247
  });
204
248
  return;
205
249
  }
206
- await handleOneShot(question, options, dependencies);
250
+ const runtime = await prepareRuntime(options, dependencies, "one-shot");
251
+ await handleOneShot(question, options, dependencies, runtime);
207
252
  });
208
253
  await program.parseAsync(argv);
209
254
  }
@@ -212,8 +257,7 @@ export async function runCliAndHandleErrors(argv = process.argv, dependencies =
212
257
  await runCli(argv, dependencies);
213
258
  }
214
259
  catch (error) {
215
- const debug = argv.includes("--debug") ||
216
- argv.includes("-d");
260
+ const debug = argv.includes("--debug") || argv.includes("-d");
217
261
  process.stderr.write(`${getErrorMessage(error, debug)}\n`);
218
262
  process.exitCode = 1;
219
263
  }