laohuang 0.3.2 → 0.4.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 (86) hide show
  1. package/README.md +142 -16
  2. package/dist/agent.d.ts +166 -0
  3. package/dist/agent.js +858 -0
  4. package/dist/agent.js.map +1 -0
  5. package/dist/bash-runner.d.ts +79 -0
  6. package/dist/bash-runner.js +464 -0
  7. package/dist/bash-runner.js.map +1 -0
  8. package/dist/cancellation.d.ts +36 -0
  9. package/dist/cancellation.js +123 -0
  10. package/dist/cancellation.js.map +1 -0
  11. package/dist/cli.d.ts +117 -0
  12. package/dist/cli.js +1307 -0
  13. package/dist/cli.js.map +1 -0
  14. package/dist/client.d.ts +21 -0
  15. package/dist/client.js +17 -0
  16. package/dist/client.js.map +1 -0
  17. package/dist/commands.d.ts +123 -0
  18. package/dist/commands.js +660 -0
  19. package/dist/commands.js.map +1 -0
  20. package/dist/config.d.ts +51 -0
  21. package/dist/config.js +183 -0
  22. package/dist/config.js.map +1 -0
  23. package/dist/credentials.d.ts +12 -0
  24. package/dist/credentials.js +102 -0
  25. package/dist/credentials.js.map +1 -0
  26. package/dist/events.d.ts +288 -0
  27. package/dist/events.js +838 -0
  28. package/dist/events.js.map +1 -0
  29. package/dist/model-adapter.d.ts +138 -0
  30. package/dist/model-adapter.js +244 -0
  31. package/dist/model-adapter.js.map +1 -0
  32. package/dist/model-selection.d.ts +67 -0
  33. package/dist/model-selection.js +148 -0
  34. package/dist/model-selection.js.map +1 -0
  35. package/dist/model-stream.d.ts +128 -0
  36. package/dist/model-stream.js +582 -0
  37. package/dist/model-stream.js.map +1 -0
  38. package/dist/project-instructions.d.ts +99 -0
  39. package/dist/project-instructions.js +348 -0
  40. package/dist/project-instructions.js.map +1 -0
  41. package/dist/providers.d.ts +9 -0
  42. package/dist/providers.js +26 -0
  43. package/dist/providers.js.map +1 -0
  44. package/dist/routing.d.ts +170 -0
  45. package/dist/routing.js +669 -0
  46. package/dist/routing.js.map +1 -0
  47. package/dist/semantic-classifier.d.ts +66 -0
  48. package/dist/semantic-classifier.js +86 -0
  49. package/dist/semantic-classifier.js.map +1 -0
  50. package/dist/session.d.ts +162 -0
  51. package/dist/session.js +871 -0
  52. package/dist/session.js.map +1 -0
  53. package/dist/system-prompt.d.ts +16 -0
  54. package/dist/system-prompt.js +42 -0
  55. package/dist/system-prompt.js.map +1 -0
  56. package/dist/terminal/editor.d.ts +161 -0
  57. package/dist/terminal/editor.js +1060 -0
  58. package/dist/terminal/editor.js.map +1 -0
  59. package/dist/terminal/input.d.ts +61 -0
  60. package/dist/terminal/input.js +276 -0
  61. package/dist/terminal/input.js.map +1 -0
  62. package/dist/terminal/markdown.d.ts +20 -0
  63. package/dist/terminal/markdown.js +621 -0
  64. package/dist/terminal/markdown.js.map +1 -0
  65. package/dist/terminal/screen.d.ts +66 -0
  66. package/dist/terminal/screen.js +624 -0
  67. package/dist/terminal/screen.js.map +1 -0
  68. package/dist/terminal/theme.d.ts +23 -0
  69. package/dist/terminal/theme.js +101 -0
  70. package/dist/terminal/theme.js.map +1 -0
  71. package/dist/terminal/ui.d.ts +285 -0
  72. package/dist/terminal/ui.js +1815 -0
  73. package/dist/terminal/ui.js.map +1 -0
  74. package/dist/tools.d.ts +95 -0
  75. package/dist/tools.js +444 -0
  76. package/dist/tools.js.map +1 -0
  77. package/dist/ui-state.d.ts +51 -0
  78. package/dist/ui-state.js +194 -0
  79. package/dist/ui-state.js.map +1 -0
  80. package/dist/web.d.ts +56 -0
  81. package/dist/web.js +247 -0
  82. package/dist/web.js.map +1 -0
  83. package/package.json +24 -21
  84. package/bin/laohuang.js +0 -10
  85. package/lib/launcher.js +0 -133
  86. package/vendor/laohuangcode-0.3.2-py3-none-any.whl +0 -0
package/README.md CHANGED
@@ -1,27 +1,153 @@
1
- # laohuang
1
+ # laoHuangCode
2
2
 
3
- This npm package provides the `laohuang` command for
4
- [laoHuangCode](https://github.com/hxr223/laoHuangCode).
3
+ 一个最小的 coding agent,基于 TypeScript/Node.js、官方 `openai` npm SDK 和
4
+ Chat Completions 原生工具调用。
5
5
 
6
- It is a small Node.js launcher, not a second implementation of the agent. On
7
- first run it creates an isolated Python environment in the user cache and
8
- installs the bundled `laohuangcode` Python wheel. Python 3.11 or newer must
9
- already be available as `python3` or `python`. Third-party Python dependencies
10
- are downloaded by pip during this first launch.
6
+ 当前提供四个工具:`read`、`write`、`edit`、`bash`。所有工具均直接执行,当前原型
7
+ 暂不提供权限确认。
8
+
9
+ 同一次模型响应中的只读工具与多个 Bash 调用默认并发执行。实时事件按工具实际完成
10
+ 顺序发出,回传模型的 `tool` 消息保持原始调用顺序。只要一个批次包含 `write`
11
+ `edit`,Agent 会保守地串行执行整个批次,避免读写或多次修改之间出现竞态。
12
+
13
+ 交互终端采用后台 AgentSession:模型回复和 Bash 的 stderr/状态会实时显示,stdout
14
+ 会保留在工具结果中但默认不刷到终端,
15
+ Agent 运行时仍可继续输入。后续输入由事件路由器放入 pending/held 队列,并在安全点
16
+ 成批交给模型;当前任务可以通过 `/cancel` 或运行中的 `Ctrl+C` 协作式取消。
17
+ 连续重复的工具调用和 Token、耗时预算会触发安全保护;保护触发后
18
+ Agent 会禁用工具并尝试基于已有信息完成一次最终回答。
19
+
20
+ ## 快速开始
21
+
22
+ 需要 Node.js 18+。面向普通用户的安装方式:
11
23
 
12
24
  ```bash
13
25
  npm install --global laohuang
14
- laohuang --version
15
26
  laohuang
16
27
  ```
17
28
 
18
- The first interactive launch asks for DeepSeek or OpenAI, reads the API key
19
- with hidden terminal input, and then asks which model to use. No API key
20
- environment variable is needed.
29
+ 首次启动会在终端中依次选择 DeepSeek OpenAI、隐藏输入 API key、选择模型,
30
+ 不需要设置环境变量。配置完成后,进入任意项目目录直接运行 `laohuang`。
31
+
32
+ 从源码运行:
33
+
34
+ ```bash
35
+ git clone https://github.com/hxr223/laoHuangCode.git
36
+ cd laoHuangCode
37
+ npm ci
38
+ npm run build
39
+ node dist/cli.js
40
+ ```
41
+
42
+ ## 模型配置
43
+
44
+ 首次运行时,终端会提供两个供应商:
45
+
46
+ - DeepSeek:内置 `deepseek-v4-flash` 和 `deepseek-v4-pro`。
47
+ - OpenAI:使用输入的 API key 动态读取账户可用模型,也可手动输入模型名。
48
+
49
+ API key 使用隐藏输入,保存在独立的
50
+ `~/.config/laohuang/credentials.json` 中。普通模型配置保存在同目录的
51
+ `config.json`;两个文件均使用 `0600` 权限,目录使用 `0700` 权限。
52
+
53
+ ```text
54
+ /model 交互选择供应商和模型
55
+ /model current 查看当前模型
56
+ /model deepseek deepseek-v4-pro 直接切换模型
57
+ /model openai <model-name> 直接切换 OpenAI 模型
58
+ /login 交互选择供应商并登录
59
+ /login deepseek 输入或覆盖 DeepSeek API key
60
+ /logout openai 删除保存的 OpenAI 凭据
61
+ /cancel 取消当前运行任务
62
+ /queue 查看 pending/held/dead-letter 与估算 token
63
+ /queue resume 恢复取消后保留的消息
64
+ /queue clear 清空 pending/held/dead-letter
65
+ /clear 清空当前对话上下文
66
+ ```
67
+
68
+ 认证和模型选择相互独立:`/login`、`/logout` 管理凭据,`/model` 只切换模型。
69
+ 如果请求返回 401,Agent 会提示对应的 `/login <provider>` 命令。旧的
70
+ `/apikey set`、`/apikey remove` 暂时保留为兼容别名。
71
+
72
+ 常用配置命令:
73
+
74
+ ```bash
75
+ laohuang config list
76
+ laohuang config use default
77
+ laohuang doctor
78
+ ```
79
+
80
+ 完整规则见 [模型配置文档](docs/configuration.md)。
81
+
82
+ ## 运行
83
+
84
+ 进入希望 Agent 操作的项目目录后执行:
85
+
86
+ ```bash
87
+ laohuang
88
+ ```
89
+
90
+ 输入任务,使用 `/help` 查看命令,使用 `/exit` 或 `Ctrl+D` 退出。通过 `/model`
91
+ 切换供应商或模型时会保留当前对话上下文。
92
+
93
+ 交互终端使用简洁的上下边框标识输入区域,不显示常驻状态或快捷键底栏。输入 `/` 会
94
+ 立即显示命令、说明和参数补全;`Tab`/`Enter` 可接受当前补全,`/model` 会按
95
+ “供应商 → 模型”分层补全。终端还支持多行
96
+ 编辑和当前会话输入历史:
97
+
98
+ - `Enter`:发送任务。
99
+ - `Alt+Enter`:插入换行。
100
+ - `↑` / `↓`:浏览历史输入。
101
+ - Agent 运行中按 `Ctrl+C`:取消当前任务。
102
+ - Agent 空闲时按 `Ctrl+C`:清空输入;500ms 内再按一次:退出。
103
+ - `Ctrl+D`:退出。
104
+
105
+ 模型文本和 Bash stderr/状态采用 append-only inline 流式展示,工具输出按 tool call 分组;
106
+ 被取消或截断的半条模型回复会保留在屏幕上并标记“未加入上下文”。完整的逐轮事件仍可
107
+ 通过 Web 日志面板查看。输出被重定向或由程序调用 CLI 时,会自动回退到稳定的纯文本
108
+ 格式。
109
+
110
+ ### 验证交互终端
111
+
112
+ 1. 在真实 TTY 中运行 `laohuang`。
113
+ 2. 发送第一个问题并等待回答完成。
114
+ 3. 发送第二个问题;向上滚动确认第一个问题和回答仍保留且未被改写。
115
+ 4. 输入 `/` 和 `/e`,确认候选只占可见行数,`Tab` 可接受 `/exit`,继续编辑会移除补全层。
116
+ 5. 任务运行中按 `Ctrl+C` 取消;空闲且编辑器为空时按 `Ctrl+D` 退出。
117
+
118
+ 当前版本不会在工具执行前请求确认。请只在你信任的项目和环境中运行。
119
+
120
+ ### Web 日志面板
121
+
122
+ ```bash
123
+ laohuang --web
124
+ laohuang --web --web-port 9000
125
+ ```
126
+
127
+ 面板默认位于 <http://127.0.0.1:8765>,展示模型轮次、工具调用和最终
128
+ 回复。它只监听本机,数据只存在内存中,进程退出后清空。
129
+
130
+ ## 开发与发布检查
131
+
132
+ ```bash
133
+ npm ci
134
+ npm run build
135
+ npm test
136
+ ```
137
+
138
+ `npm run build` 通过 `tsc` 把 `src/` 编译到 `dist/`;`npm test` 使用 Node 自带的
139
+ `node:test` 运行 `test/` 下的离线测试套件,不需要网络访问。发布流程见
140
+ [发布流程](docs/publishing.md) 和 [npm 分发说明](docs/npm-distribution.md)。
141
+
142
+ ## 安全边界
143
+
144
+ 文件工具会限制在启动目录内并阻止符号链接逃逸;API key 不通过环境变量传递给
145
+ Bash。但 `bash` **没有操作系统级沙箱**,执行后仍能访问项目外文件、网络和其他
146
+ 系统资源。公开使用前请阅读 [安全模型](docs/security.md)。
21
147
 
22
- The launcher accepts two advanced environment variables:
148
+ 架构说明见 [docs/architecture.md](docs/architecture.md);设计文档归档在
149
+ [docs/superpowers/specs/](docs/superpowers/specs/)。
23
150
 
24
- - `LAOHUANG_PYTHON`: run a specific Python executable and skip bootstrapping.
25
- - `LAOHUANG_CACHE_HOME`: override the launcher's cache root.
151
+ ## License
26
152
 
27
- See the main project README for model configuration and security details.
153
+ MIT
@@ -0,0 +1,166 @@
1
+ /**
2
+ * The model/tool loop at the heart of laoHuangCode.
3
+ *
4
+ * One user turn streams model completions through the provider-neutral
5
+ * adapter boundary (model-adapter.ts), commits
6
+ * only fully validated attempts to history (atomically, via the owning
7
+ * session's commit hooks when present), executes tool-call batches
8
+ * (read-only tools and multiple bash calls concurrently; any write/edit or
9
+ * sequential-mode tool makes the whole batch serial), and enforces the
10
+ * runtime guard rails: repeated-identical-tool-call detection plus token and
11
+ * duration budgets. A triggered guard disables tools and asks the model for
12
+ * one final answer from the information already gathered.
13
+ */
14
+ import { CancelToken } from "./cancellation.ts";
15
+ import { EventKind, EventSource, type EventBus } from "./events.ts";
16
+ import { type ChatClientLike } from "./model-adapter.ts";
17
+ import type { ToolDefinition, ToolExecutionContextLike, ToolExecutionMode, ToolResult, ToolSpec } from "./tools.ts";
18
+ import { ProjectInstructionState } from "./project-instructions.ts";
19
+ export declare const FORCED_FINAL_PROMPT = "Tool use has been stopped by the runtime safety guard.\nDo not call any tools. Give the user the best concise answer possible from the\ninformation already available. Clearly state any limitation caused by stopping\ntool use, but do not mention internal implementation details unless useful.";
20
+ /** Raised when the model response cannot drive the agent loop. */
21
+ export declare class AgentError extends Error {
22
+ constructor(message: string, options?: {
23
+ cause?: unknown;
24
+ });
25
+ }
26
+ /** Raised when the active agent task is cooperatively cancelled. */
27
+ export declare class AgentCancelled extends AgentError {
28
+ constructor(message: string, options?: {
29
+ cause?: unknown;
30
+ });
31
+ }
32
+ export type AgentEventCallback = (eventType: string, payload: Record<string, unknown>) => void;
33
+ export type ToolEventCallback = (name: string, args: Record<string, unknown>, result: ToolResult) => void;
34
+ export type { ChatClientLike } from "./model-adapter.ts";
35
+ /** Structural minimum of ToolRegistry (tools.ts) the agent relies on. */
36
+ export interface AgentToolRegistry {
37
+ readonly definitions: readonly ToolDefinition[];
38
+ readonly orderedSpecs: readonly ToolSpec[];
39
+ executionMode(name: string): ToolExecutionMode | undefined;
40
+ execute(name: string, args: Record<string, unknown>, context?: ToolExecutionContextLike): Promise<ToolResult> | ToolResult;
41
+ }
42
+ /** Pending user input handed over at a session safe point. */
43
+ export interface PendingInputBatchLike {
44
+ content?: string | undefined;
45
+ eventIds?: readonly string[] | undefined;
46
+ }
47
+ /** Options for the session-owned publish hook. */
48
+ export interface AgentEventPublishOptions {
49
+ source: EventSource;
50
+ correlation_id: string | null;
51
+ payload: Record<string, unknown>;
52
+ }
53
+ /**
54
+ * Capabilities the owning session (session.ts) exposes to one agent run.
55
+ * Every member is optional; without a context the agent runs standalone and
56
+ * manages its own history commits and cancellation checks.
57
+ */
58
+ export interface AgentContext {
59
+ readonly sessionId?: string | null;
60
+ readonly taskId?: string | null;
61
+ readonly cancelToken?: CancelToken | null;
62
+ readonly eventBus?: EventBus | null;
63
+ publish?(kind: EventKind, options: AgentEventPublishOptions): unknown;
64
+ /** Return false to cancel before the model request is sent. */
65
+ modelStarted?(): boolean | void;
66
+ /** Called once the SDK acknowledged the streaming request. */
67
+ modelRequestOpened?(): boolean | void;
68
+ toolsStarted?(): void;
69
+ safePoint?(): PendingInputBatchLike | null | undefined;
70
+ /** Atomically commit the user message; rollback undoes a failed commit. */
71
+ commitInput?(append: () => void, rollback: () => void): boolean;
72
+ commitPending?(batch: PendingInputBatchLike, append: () => void, rollback: () => void): boolean;
73
+ /** Atomically reject history commits once cancellation has won. */
74
+ commitIfActive?(callback: () => void): boolean;
75
+ }
76
+ export interface RunOptions {
77
+ cancelToken?: CancelToken | null;
78
+ requestId?: string | null;
79
+ isRequestActive?: ((requestId: string) => boolean) | null;
80
+ }
81
+ export interface CodingAgentOptions {
82
+ client: ChatClientLike;
83
+ model: string;
84
+ tools: AgentToolRegistry;
85
+ maxTotalTokens?: number;
86
+ maxElapsedSeconds?: number;
87
+ repeatedToolCallLimit?: number;
88
+ onToolEvent?: ToolEventCallback | null;
89
+ onAgentEvent?: AgentEventCallback | null;
90
+ provider?: string | null;
91
+ toolExecution?: ToolExecutionMode;
92
+ /**
93
+ * Project root used only for project-instruction loading (both this and
94
+ * `startupCwd` must be set; omit both to disable instruction injection).
95
+ */
96
+ projectRoot?: string | null;
97
+ /** Startup cwd for project-instruction discovery. */
98
+ startupCwd?: string | null;
99
+ }
100
+ /** Execution context handed to every tool call in a batch. */
101
+ export interface AgentToolContext extends ToolExecutionContextLike {
102
+ readonly sessionId: string | null;
103
+ readonly taskId: string | null;
104
+ readonly toolCallId: string | null;
105
+ readonly cancelToken: CancelToken | null;
106
+ }
107
+ export declare class CodingAgent {
108
+ client: ChatClientLike;
109
+ model: string;
110
+ provider: string | null;
111
+ /** Provider-neutral model access; resolved from `provider`. */
112
+ private adapter;
113
+ readonly tools: AgentToolRegistry;
114
+ readonly maxTotalTokens: number;
115
+ readonly maxElapsedSeconds: number;
116
+ readonly repeatedToolCallLimit: number;
117
+ readonly toolExecution: ToolExecutionMode;
118
+ /** Conversation history in Chat Completions wire shape. */
119
+ messages: Array<Record<string, unknown>>;
120
+ private readonly onToolEvent;
121
+ private readonly onAgentEvent;
122
+ private readonly instructionRoot;
123
+ private readonly startupCwd;
124
+ private baselineInstructionsLoaded;
125
+ private instructionState;
126
+ private turn;
127
+ private activeContext;
128
+ private activeRequestId;
129
+ constructor(options: CodingAgentOptions);
130
+ /** Bookkeeping for loaded project instructions (never model-visible). */
131
+ get projectInstructionState(): ProjectInstructionState | null;
132
+ /** Swap the model client mid-conversation, keeping portable history. */
133
+ switchModel(options: {
134
+ client: unknown;
135
+ model: string;
136
+ provider: string;
137
+ }): void;
138
+ /** Run one user turn, committing only fully validated model attempts. */
139
+ run(userInput: string, context?: AgentContext | null, options?: RunOptions): Promise<string>;
140
+ private budgetGuardReason;
141
+ private guardPayload;
142
+ private recordRepeatedToolCalls;
143
+ private executeToolBatch;
144
+ private executeTool;
145
+ private finishToolEvent;
146
+ /**
147
+ * Append the rendered baseline project instructions once per session,
148
+ * right after the first direct user message and before the first model
149
+ * request. Append-only: the system prompt and committed history are never
150
+ * rebuilt, and nothing is appended when no instruction files exist.
151
+ */
152
+ private injectBaselineInstructions;
153
+ /**
154
+ * DSH-style dynamic descendant discovery: for every parent directory from
155
+ * the instruction root to each successfully touched path's directory,
156
+ * render instructions from scopes not already represented by the
157
+ * instruction state as one additional reminder message. Append-only and
158
+ * duplicate-suppressed by ProjectInstructionState; a no-op when
159
+ * instruction loading is not configured.
160
+ */
161
+ private discoverForTouchedPaths;
162
+ private commitContextMessage;
163
+ private emit;
164
+ private emitLegacy;
165
+ private publishRuntimeEvent;
166
+ }