@unscientificjszhai/howto 1.0.0-alpha.2 → 1.0.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.
package/README.CN.md ADDED
@@ -0,0 +1,206 @@
1
+ # howto
2
+
3
+ _在终端中用 AI 快速找到可执行命令。_
4
+
5
+ [![npm version](https://img.shields.io/npm/v/@unscientificjszhai/howto/latest)](https://www.npmjs.com/package/@unscientificjszhai/howto)
6
+ [![GitHub Actions Test Status](https://github.com/UnscientificJsZhai/HowTo/actions/workflows/test.yml/badge.svg?branch=master)](https://github.com/UnscientificJsZhai/HowTo/actions)
7
+ [![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg)](https://github.com/prettier/prettier)
8
+ [![License](https://img.shields.io/github/license/UnscientificJsZhai/HowTo)](LICENSE)
9
+
10
+ [English](README.md) | 简体中文
11
+
12
+ [功能](#功能) • [安装](#安装) • [使用](#使用) • [配置](#配置) • [开发](#开发) • [故障排查](#故障排查)
13
+
14
+ `howto` 是一个 TypeScript CLI工具,使用自然语言向它提问,它会给你候选命令供你选择。它不会直接运行 AI 输出:你需要在终端中选择候选命令,填写占位符(如有),检查最终命令,然后确认执行。
15
+
16
+ > [!IMPORTANT]
17
+ > `howto` 是命令生成助手,不是沙箱。执行前请检查每一条命令,尤其是会修改文件、安装包、变更权限或使用提权操作的命令。
18
+
19
+ ## 功能
20
+
21
+ - **自然语言生成命令** - 描述任务后获得简洁的 shell 命令候选项。
22
+ - **指定工具模式** - 使用 `howto use <command>` 要求候选项围绕某个 CLI 工具生成。
23
+ - **交互式确认流程** - 选择候选项、填写占位符,并在执行前确认最终命令。
24
+ - **非交互打印模式** - 使用 `--print` 输出候选命令,不进入 TTY 交互,也不执行命令。
25
+ - **支持 OpenAI 和 Gemini** - 可通过 CLI 参数、环境变量或 `~/.howto/config.json` 配置 provider。
26
+ - **本地优先校验** - 本地校验 AI JSON 输出、占位符引用、`use <command>` 候选项以及明显危险命令。
27
+
28
+ ## 安装
29
+
30
+ 全局安装 CLI 包:
31
+
32
+ ```bash
33
+ npm install -g @unscientificjszhai/howto
34
+ ```
35
+
36
+ 也可以从克隆的仓库中运行:
37
+
38
+ ```bash
39
+ npm install
40
+ npm run build
41
+ npm link
42
+ ```
43
+
44
+ 之后即可使用 `howto` 命令。
45
+
46
+ ## 快速开始
47
+
48
+ 初始化 AI provider 配置:
49
+
50
+ ```bash
51
+ howto --init
52
+ ```
53
+
54
+ 初始化程序会把用户级配置写入 `~/.howto/config.json`。
55
+
56
+ > [!NOTE]
57
+ > OpenAI API key 可以为空,以支持本地 OpenAI 兼容服务。Gemini 必须提供非空 API key。
58
+
59
+ 询问一个命令:
60
+
61
+ ```bash
62
+ howto 找到最近7天修改的文件 .
63
+ ```
64
+
65
+ 限制候选项必须使用某个工具:
66
+
67
+ ```bash
68
+ howto use git 看看上周的提交
69
+ ```
70
+
71
+ 不进入交互 UI,只打印候选命令:
72
+
73
+ ```bash
74
+ howto --print 列出最大的文件 /var/log
75
+ ```
76
+
77
+ ## 使用
78
+
79
+ ```text
80
+ howto [options] [use <command>] <question> [<argument>...]
81
+ howto --init
82
+ ```
83
+
84
+ 示例:
85
+
86
+ ```bash
87
+ howto 找到当前目录的package.json
88
+ howto use find 寻找特定文件名的文件 package.json
89
+ howto 解释这个命令 -- --force
90
+ howto --ai-provider openai --print 列出监听的端口
91
+ ```
92
+
93
+ 参数:
94
+
95
+ - `--init` - 启动交互式 provider 配置,并保存 `~/.howto/config.json`。
96
+ - `--print` - 打印已校验的命令候选项并退出,不执行命令。
97
+ - `--ai-provider <openai|gemini>` - 选择 AI provider。
98
+ - `--gemini-api-key <key>` - 提供 Gemini API key。
99
+ - `--gemini-model <model>` - 覆盖 Gemini 模型。
100
+ - `--openai-api-url <url>` - 使用自定义 OpenAI 兼容 base URL。
101
+ - `--openai-api-key <key>` - 提供 OpenAI API key。
102
+ - `--openai-model <model>` - 覆盖 OpenAI 模型。
103
+ - `--structured-output <true|false>` - 启用 SDK 级 schema 结构化输出;默认 `true`。
104
+
105
+ ### 参数解析
106
+
107
+ `question` 是一个 shell 参数。包含空格时需要加引号:
108
+
109
+ ```bash
110
+ howto "find recently changed files" /tmp
111
+ ```
112
+
113
+ `question` 后面的内容会作为 `argument[]` 传给 AI。如果参数以 `--` 开头,请使用 `--` 结束 option 解析:
114
+
115
+ ```bash
116
+ howto "explain this flag" -- --force
117
+ ```
118
+
119
+ ## 配置
120
+
121
+ 配置按以下优先级解析:
122
+
123
+ 1. CLI 参数
124
+ 2. 环境变量
125
+ 3. `~/.howto/config.json`
126
+ 4. 内置默认值
127
+
128
+ 对每个配置项,优先级中第一个已配置的来源生效:
129
+
130
+ - `--ai-provider` / `HOWTO_AI_PROVIDER` / `aiProvider` - `openai` 或 `gemini`;无默认值。
131
+ - `--gemini-api-key` / `HOWTO_GEMINI_API_KEY` / `geminiApiKey` - Gemini API key;Gemini 必填。
132
+ - `--gemini-model` / `HOWTO_GEMINI_MODEL` / `geminiModel` - Gemini 模型;默认 `gemini-3.1-flash-lite`。
133
+ - `--openai-api-url` / `HOWTO_OPENAI_API_URL` / `openaiApiUrl` - OpenAI 兼容 base URL;默认使用 OpenAI SDK 默认值。
134
+ - `--openai-api-key` / `HOWTO_OPENAI_API_KEY` / `openaiApiKey` - OpenAI API key;默认为空字符串以支持本地服务。
135
+ - `--openai-model` / `HOWTO_OPENAI_MODEL` / `openaiModel` - OpenAI 模型;默认 `gpt-5.4-mini`。
136
+ - `--structured-output` / `HOWTO_STRUCTURED_OUTPUT` / `structuredOutput` - 使用 provider schema 结构化输出;默认 `true`。
137
+
138
+ 示例:
139
+
140
+ ```bash
141
+ HOWTO_AI_PROVIDER=openai \
142
+ HOWTO_OPENAI_MODEL=gpt-5.4-mini \
143
+ howto --print "show current branch"
144
+ ```
145
+
146
+ ## 安全模型
147
+
148
+ `howto` 将 AI 输出视为不可信数据。在进入执行前,CLI 会检查:
149
+
150
+ - AI 响应是符合命令 schema 的有效 JSON;
151
+ - 响应包含一到三个候选项;
152
+ - 所有占位符使用 `{{name}}` 语法,并且声明与引用一致;
153
+ - `use <command>` 候选项在保守处理前缀后,明确以指定工具开头;
154
+ - 明显危险的命令需要输入 `EXECUTE` 才能继续执行;大小写不敏感。
155
+
156
+ 危险命令检测当前覆盖递归破坏性 `rm`、磁盘和文件系统操作、大范围递归权限变更、下载脚本后直接交给 shell 执行、高影响包管理器操作以及服务变更等高风险模式。
157
+
158
+ > [!WARNING]
159
+ > 未被标记为危险并不表示命令一定安全。本地检查只会对已知高风险模式增加确认步骤,并不能证明命令安全。
160
+
161
+ ## 开发
162
+
163
+ 本项目使用 TypeScript、React、Ink、OpenAI SDK、Gemini GenAI SDK 和 Node 内置测试运行器构建。
164
+
165
+ ```bash
166
+ npm install
167
+ npm run build
168
+ npm test
169
+ npm run lint
170
+ npm run format:check
171
+ ```
172
+
173
+ 常用路径:
174
+
175
+ - `src/index.tsx` - CLI 编排和执行流程。
176
+ - `src/cli.ts` - 参数解析。
177
+ - `src/config.ts` - 配置合并和 provider 校验。
178
+ - `src/prompt.ts` - prompt 和 AI 输出契约。
179
+ - `src/validation/` - AI 响应和命令工具校验。
180
+ - `src/safety/` - 危险命令规则。
181
+ - `src/ui/` - 基于 Ink 的终端 UI。
182
+ - `tests/unit/` - CLI、配置、校验、执行、UI 和安全逻辑的单元测试。
183
+
184
+ ## 故障排查
185
+
186
+ ### AI provider 未配置
187
+
188
+ 运行:
189
+
190
+ ```bash
191
+ howto --init
192
+ ```
193
+
194
+ `--print` 会有意跳过初始化流程。请先配置 provider,或传入对应 CLI 参数/环境变量。
195
+
196
+ ### 非交互终端错误
197
+
198
+ 默认模式需要交互式 TTY 来选择和确认命令。在脚本或 CI 中请使用 `--print`:
199
+
200
+ ```bash
201
+ howto --print "show disk usage"
202
+ ```
203
+
204
+ ### Gemini key 必填
205
+
206
+ Gemini 不能在没有 API key 的情况下运行。请设置 `HOWTO_GEMINI_API_KEY`,传入 `--gemini-api-key`,或重新运行 `howto --init`。
package/README.md ADDED
@@ -0,0 +1,206 @@
1
+ # howto
2
+
3
+ _Use AI to quickly find commands within the terminal._
4
+
5
+ [![npm version](https://img.shields.io/npm/v/@unscientificjszhai/howto/latest)](https://www.npmjs.com/package/@unscientificjszhai/howto)
6
+ [![GitHub Actions Test Status](https://github.com/UnscientificJsZhai/HowTo/actions/workflows/test.yml/badge.svg?branch=master)](https://github.com/UnscientificJsZhai/HowTo/actions)
7
+ [![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg)](https://github.com/prettier/prettier)
8
+ [![License](https://img.shields.io/github/license/UnscientificJsZhai/HowTo)](LICENSE)
9
+
10
+ English | [简体中文](README.CN.md)
11
+
12
+ [Features](#features) • [Install](#install) • [Usage](#usage) • [Configuration](#configuration) • [Development](#development) • [Troubleshooting](#troubleshooting)
13
+
14
+ `howto` is a TypeScript CLI that turns a natural-language question into up to three executable command candidates. It never runs AI output directly: you choose a command in the terminal, fill any placeholders, review the final command, and confirm before execution.
15
+
16
+ > [!IMPORTANT]
17
+ > `howto` is an assistant for generating shell commands, not a sandbox. Review every command before running it, especially commands that modify files, install packages, change permissions, or use elevated privileges.
18
+
19
+ ## Features
20
+
21
+ - **Natural language to commands** - ask for a task and get concise shell command candidates.
22
+ - **Tool-constrained mode** - use `howto use <command>` to require candidates around a specific CLI tool.
23
+ - **Interactive review flow** - select a candidate, resolve placeholders, and confirm the final command before execution.
24
+ - **Non-interactive print mode** - use `--print` to output candidates without TTY interaction or command execution.
25
+ - **OpenAI and Gemini support** - configure either provider from CLI flags, environment variables, or `~/.howto/config.json`.
26
+ - **Local validation first** - AI JSON output, placeholder references, `use <command>` candidates, and obvious dangerous commands are checked locally.
27
+
28
+ ## Install
29
+
30
+ Install the CLI package globally:
31
+
32
+ ```bash
33
+ npm install -g @unscientificjszhai/howto
34
+ ```
35
+
36
+ Or run it from a cloned repository:
37
+
38
+ ```bash
39
+ npm install
40
+ npm run build
41
+ npm link
42
+ ```
43
+
44
+ You can then call the executable as `howto`.
45
+
46
+ ## Getting Started
47
+
48
+ Initialize your AI provider configuration:
49
+
50
+ ```bash
51
+ howto --init
52
+ ```
53
+
54
+ The initializer writes a user-level config file at `~/.howto/config.json`.
55
+
56
+ > [!NOTE]
57
+ > OpenAI API keys may be empty for local OpenAI-compatible services. Gemini requires a non-empty API key.
58
+
59
+ Ask for a command:
60
+
61
+ ```bash
62
+ howto "find files modified in the last 7 days" .
63
+ ```
64
+
65
+ Limit candidates to a specific tool:
66
+
67
+ ```bash
68
+ howto use git "show commits from last week"
69
+ ```
70
+
71
+ Print command candidates without entering the interactive UI:
72
+
73
+ ```bash
74
+ howto --print "list the largest files" /var/log
75
+ ```
76
+
77
+ ## Usage
78
+
79
+ ```text
80
+ howto [options] [use <command>] <question> [<argument>...]
81
+ howto --init
82
+ ```
83
+
84
+ Examples:
85
+
86
+ ```bash
87
+ howto "find package.json under the current directory"
88
+ howto use find "find a filename" package.json
89
+ howto "explain this option" -- --force
90
+ howto --ai-provider openai --print "show listening ports"
91
+ ```
92
+
93
+ Options:
94
+
95
+ - `--init` - start interactive provider setup and save `~/.howto/config.json`.
96
+ - `--print` - print validated command candidates and exit without executing.
97
+ - `--ai-provider <openai|gemini>` - select the AI provider.
98
+ - `--gemini-api-key <key>` - provide a Gemini API key.
99
+ - `--gemini-model <model>` - override the Gemini model.
100
+ - `--openai-api-url <url>` - use a custom OpenAI-compatible base URL.
101
+ - `--openai-api-key <key>` - provide an OpenAI API key.
102
+ - `--openai-model <model>` - override the OpenAI model.
103
+ - `--structured-output <true|false>` - enable SDK-level schema structured output; default `true`.
104
+
105
+ ### Argument Parsing
106
+
107
+ `question` is one shell argument. Quote it when it contains spaces:
108
+
109
+ ```bash
110
+ howto "find recently changed files" /tmp
111
+ ```
112
+
113
+ Everything after the question is passed to the AI as `argument[]`. Use `--` when an argument starts with `--`:
114
+
115
+ ```bash
116
+ howto "explain this flag" -- --force
117
+ ```
118
+
119
+ ## Configuration
120
+
121
+ Configuration is resolved in this order:
122
+
123
+ 1. CLI options
124
+ 2. Environment variables
125
+ 3. `~/.howto/config.json`
126
+ 4. Built-in defaults
127
+
128
+ For each setting, the first configured source in that order wins:
129
+
130
+ - `--ai-provider` / `HOWTO_AI_PROVIDER` / `aiProvider` - `openai` or `gemini`; no default.
131
+ - `--gemini-api-key` / `HOWTO_GEMINI_API_KEY` / `geminiApiKey` - Gemini API key; required for Gemini.
132
+ - `--gemini-model` / `HOWTO_GEMINI_MODEL` / `geminiModel` - Gemini model; default `gemini-3.1-flash-lite`.
133
+ - `--openai-api-url` / `HOWTO_OPENAI_API_URL` / `openaiApiUrl` - OpenAI-compatible base URL; defaults to the OpenAI SDK default.
134
+ - `--openai-api-key` / `HOWTO_OPENAI_API_KEY` / `openaiApiKey` - OpenAI API key; defaults to an empty string for local services.
135
+ - `--openai-model` / `HOWTO_OPENAI_MODEL` / `openaiModel` - OpenAI model; default `gpt-5.4-mini`.
136
+ - `--structured-output` / `HOWTO_STRUCTURED_OUTPUT` / `structuredOutput` - use provider schema structured output; default `true`.
137
+
138
+ Example:
139
+
140
+ ```bash
141
+ HOWTO_AI_PROVIDER=openai \
142
+ HOWTO_OPENAI_MODEL=gpt-5.4-mini \
143
+ howto --print "show current branch"
144
+ ```
145
+
146
+ ## Safety Model
147
+
148
+ `howto` treats AI output as untrusted data. Before anything reaches execution, the CLI checks that:
149
+
150
+ - the AI response is valid JSON matching the required command schema;
151
+ - the response contains between one and three candidates;
152
+ - all placeholders use `{{name}}` syntax and are declared consistently;
153
+ - `use <command>` candidates clearly start with the requested tool after conservative prefix handling;
154
+ - obvious dangerous patterns require typing `EXECUTE` before they can run; matching is case-insensitive.
155
+
156
+ Dangerous-command detection currently covers high-risk patterns such as recursive destructive `rm`, disk and filesystem operations, broad recursive permission changes, downloaded scripts piped into a shell, high-impact package manager operations, and service changes.
157
+
158
+ > [!WARNING]
159
+ > A command not flagged as dangerous is not guaranteed to be safe. The local checks add friction for known high-risk patterns; they do not prove command safety.
160
+
161
+ ## Development
162
+
163
+ This project is built with TypeScript, React, Ink, OpenAI SDK, Gemini GenAI SDK, and Node's built-in test runner.
164
+
165
+ ```bash
166
+ npm install
167
+ npm run build
168
+ npm test
169
+ npm run lint
170
+ npm run format:check
171
+ ```
172
+
173
+ Useful paths:
174
+
175
+ - `src/index.tsx` - CLI orchestration and execution flow.
176
+ - `src/cli.ts` - argument parsing.
177
+ - `src/config.ts` - config merging and provider validation.
178
+ - `src/prompt.ts` - prompt and AI output contract.
179
+ - `src/validation/` - AI response and command-tool validation.
180
+ - `src/safety/` - dangerous command rules.
181
+ - `src/ui/` - Ink-based terminal UI.
182
+ - `tests/unit/` - unit tests for CLI, config, validation, execution, UI, and safety logic.
183
+
184
+ ## Troubleshooting
185
+
186
+ ### AI provider is not configured
187
+
188
+ Run:
189
+
190
+ ```bash
191
+ howto --init
192
+ ```
193
+
194
+ For `--print`, initialization is intentionally skipped. Configure the provider first or pass the relevant CLI flags/environment variables.
195
+
196
+ ### Non-interactive terminal error
197
+
198
+ The default mode needs an interactive TTY for selection and confirmation. Use `--print` in scripts or CI:
199
+
200
+ ```bash
201
+ howto --print "show disk usage"
202
+ ```
203
+
204
+ ### Gemini key is required
205
+
206
+ Gemini cannot run without an API key. Set `HOWTO_GEMINI_API_KEY`, pass `--gemini-api-key`, or rerun `howto --init`.
@@ -0,0 +1,17 @@
1
+ # Third-Party Notices
2
+
3
+ This project depends on third-party packages that are installed by npm as separate packages.
4
+ Each dependency is distributed under its own license.
5
+
6
+ ## Runtime Dependencies
7
+
8
+ | Package | Version | License |
9
+ | --------------- | -------- | ---------- |
10
+ | `@google/genai` | `2.6.0` | Apache-2.0 |
11
+ | `ink` | `7.0.4` | MIT |
12
+ | `openai` | `6.39.0` | Apache-2.0 |
13
+ | `react` | `19.2.6` | MIT |
14
+
15
+ The package versions above reflect the installed dependency set at the time this notice
16
+ was prepared. See each package's published npm artifact for its complete license text and
17
+ any package-specific notices.
@@ -0,0 +1,44 @@
1
+ export const COMMAND_GENERATION_SCHEMA = {
2
+ type: "object",
3
+ additionalProperties: false,
4
+ required: ["commands"],
5
+ properties: {
6
+ commands: {
7
+ type: "array",
8
+ minItems: 1,
9
+ maxItems: 3,
10
+ items: {
11
+ type: "object",
12
+ additionalProperties: false,
13
+ required: ["title", "command", "description", "placeholders"],
14
+ properties: {
15
+ title: {
16
+ type: "string",
17
+ },
18
+ command: {
19
+ type: "string",
20
+ },
21
+ description: {
22
+ type: "string",
23
+ },
24
+ placeholders: {
25
+ type: "array",
26
+ items: {
27
+ type: "object",
28
+ additionalProperties: false,
29
+ required: ["name", "description"],
30
+ properties: {
31
+ name: {
32
+ type: "string",
33
+ },
34
+ description: {
35
+ type: "string",
36
+ },
37
+ },
38
+ },
39
+ },
40
+ },
41
+ },
42
+ },
43
+ },
44
+ };
package/dist/ai/gemini.js CHANGED
@@ -8,6 +8,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
8
8
  });
9
9
  };
10
10
  import { GoogleGenAI } from "@google/genai";
11
+ import { COMMAND_GENERATION_SCHEMA } from "./command-schema.js";
11
12
  import { AiProviderError } from "./errors.js";
12
13
  export class GeminiCommandProvider {
13
14
  constructor(config) {
@@ -17,14 +18,7 @@ export class GeminiCommandProvider {
17
18
  generateCommands(request) {
18
19
  return __awaiter(this, void 0, void 0, function* () {
19
20
  try {
20
- const response = yield this.client.models.generateContent({
21
- model: this.model,
22
- contents: request.userPrompt,
23
- config: {
24
- systemInstruction: request.systemPrompt,
25
- responseMimeType: "application/json",
26
- },
27
- });
21
+ const response = yield this.client.models.generateContent(buildGeminiGenerateContentRequest(this.model, request));
28
22
  const rawText = response.text;
29
23
  if (rawText === undefined || rawText.trim() === "") {
30
24
  throw new Error("provider returned an empty response");
@@ -37,3 +31,10 @@ export class GeminiCommandProvider {
37
31
  });
38
32
  }
39
33
  }
34
+ export function buildGeminiGenerateContentRequest(model, request) {
35
+ return {
36
+ model,
37
+ contents: request.userPrompt,
38
+ config: Object.assign({ systemInstruction: request.systemPrompt, responseMimeType: "application/json" }, (request.structuredOutput ? { responseJsonSchema: COMMAND_GENERATION_SCHEMA } : {})),
39
+ };
40
+ }
package/dist/ai/openai.js CHANGED
@@ -8,27 +8,18 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
8
8
  });
9
9
  };
10
10
  import OpenAI from "openai";
11
+ import { COMMAND_GENERATION_SCHEMA } from "./command-schema.js";
11
12
  import { AiProviderError } from "./errors.js";
12
13
  export class OpenAiCommandProvider {
13
14
  constructor(config) {
14
15
  this.model = config.model;
15
- this.client = new OpenAI({
16
- apiKey: config.apiKey,
17
- baseURL: config.baseUrl,
18
- });
16
+ this.client = new OpenAI(buildOpenAiClientOptions(config));
19
17
  }
20
18
  generateCommands(request) {
21
19
  return __awaiter(this, void 0, void 0, function* () {
22
20
  var _a, _b;
23
21
  try {
24
- const response = yield this.client.chat.completions.create({
25
- model: this.model,
26
- messages: [
27
- { role: "system", content: request.systemPrompt },
28
- { role: "user", content: request.userPrompt },
29
- ],
30
- response_format: { type: "json_object" },
31
- });
22
+ const response = yield this.client.chat.completions.create(buildOpenAiChatCompletionRequest(this.model, request));
32
23
  const rawText = (_b = (_a = response.choices[0]) === null || _a === void 0 ? void 0 : _a.message) === null || _b === void 0 ? void 0 : _b.content;
33
24
  if (rawText === undefined || rawText === null || rawText.trim() === "") {
34
25
  throw new Error("provider returned an empty response");
@@ -41,3 +32,38 @@ export class OpenAiCommandProvider {
41
32
  });
42
33
  }
43
34
  }
35
+ export function buildOpenAiClientOptions(config) {
36
+ if (config.apiKey.trim() !== "") {
37
+ return {
38
+ apiKey: config.apiKey,
39
+ baseURL: config.baseUrl,
40
+ };
41
+ }
42
+ return {
43
+ apiKey: "howto-empty-api-key",
44
+ baseURL: config.baseUrl,
45
+ defaultHeaders: {
46
+ Authorization: null,
47
+ },
48
+ };
49
+ }
50
+ export function buildOpenAiChatCompletionRequest(model, request) {
51
+ return {
52
+ model,
53
+ messages: [
54
+ { role: "system", content: request.systemPrompt },
55
+ { role: "user", content: request.userPrompt },
56
+ ],
57
+ response_format: request.structuredOutput
58
+ ? {
59
+ type: "json_schema",
60
+ json_schema: {
61
+ name: "command_generation",
62
+ description: "Shell command candidates generated for howto.",
63
+ schema: COMMAND_GENERATION_SCHEMA,
64
+ strict: true,
65
+ },
66
+ }
67
+ : { type: "json_object" },
68
+ };
69
+ }
package/dist/cli.js CHANGED
@@ -11,6 +11,7 @@ const VALUE_OPTIONS = new Set([
11
11
  "--openai-api-url",
12
12
  "--openai-api-key",
13
13
  "--openai-model",
14
+ "--structured-output",
14
15
  ]);
15
16
  const BOOLEAN_OPTIONS = new Set(["--print", "--init"]);
16
17
  export const USAGE = `Usage: howto [options] [use <command>] <question> [<argument>...]
@@ -25,6 +26,7 @@ Options:
25
26
  --openai-api-url <url>
26
27
  --openai-api-key <key>
27
28
  --openai-model <model>
29
+ --structured-output <true|false>
28
30
 
29
31
  Try: howto "list files changed today"`;
30
32
  export function parseCliArgs(argv) {
@@ -89,6 +91,9 @@ function assignOptionValue(options, optionName, value) {
89
91
  case "--openai-model":
90
92
  options.openaiModel = value;
91
93
  return;
94
+ case "--structured-output":
95
+ options.structuredOutput = value;
96
+ return;
92
97
  default:
93
98
  throw new CliParseError(`unsupported option: ${optionName}`);
94
99
  }
@@ -19,6 +19,7 @@ const CONFIG_FILE_FIELDS = new Set([
19
19
  "openaiApiUrl",
20
20
  "openaiApiKey",
21
21
  "openaiModel",
22
+ "structuredOutput",
22
23
  ]);
23
24
  export function getConfigFilePath(env = process.env) {
24
25
  var _a;
@@ -44,6 +45,13 @@ export function readUserConfigFile() {
44
45
  if (!CONFIG_FILE_FIELDS.has(key)) {
45
46
  continue;
46
47
  }
48
+ if (key === "structuredOutput") {
49
+ if (typeof value !== "string" && typeof value !== "boolean") {
50
+ throw new ConfigError(`config file field ${key} must be a boolean or string`);
51
+ }
52
+ config.structuredOutput = value;
53
+ continue;
54
+ }
47
55
  if (typeof value !== "string") {
48
56
  throw new ConfigError(`config file field ${key} must be a string`);
49
57
  }
package/dist/config.js CHANGED
@@ -28,6 +28,7 @@ export function loadConfig(options, env, fileConfig = {}) {
28
28
  model: (_c = pickConfigValue(options.openaiModel, env.HOWTO_OPENAI_MODEL, fileConfig.openaiModel)) !== null && _c !== void 0 ? _c : DEFAULT_OPENAI_MODEL,
29
29
  baseUrl: pickConfigValue(options.openaiApiUrl, env.HOWTO_OPENAI_API_URL, fileConfig.openaiApiUrl),
30
30
  },
31
+ structuredOutput: parseStructuredOutput(pickConfigValue(options.structuredOutput, env.HOWTO_STRUCTURED_OUTPUT, fileConfig.structuredOutput)),
31
32
  };
32
33
  if (config.aiProvider === "gemini" && isBlank(config.gemini.apiKey)) {
33
34
  throw new ConfigError("Gemini provider requires --gemini-api-key or HOWTO_GEMINI_API_KEY");
@@ -47,3 +48,18 @@ function parseAiProvider(value) {
47
48
  function isBlank(value) {
48
49
  return value === undefined || value.trim() === "";
49
50
  }
51
+ function parseStructuredOutput(value) {
52
+ if (value === undefined) {
53
+ return true;
54
+ }
55
+ if (typeof value === "boolean") {
56
+ return value;
57
+ }
58
+ if (value === "true") {
59
+ return true;
60
+ }
61
+ if (value === "false") {
62
+ return false;
63
+ }
64
+ throw new ConfigError("invalid structuredOutput value; expected true or false");
65
+ }