agent-sh 0.12.27 → 0.13.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.md +13 -2
- package/dist/agent/agent-loop.d.ts +3 -5
- package/dist/agent/agent-loop.js +42 -98
- package/dist/agent/conversation-state.d.ts +9 -0
- package/dist/agent/conversation-state.js +16 -0
- package/dist/agent/history-file.d.ts +6 -0
- package/dist/agent/history-file.js +1 -1
- package/dist/agent/host-types.d.ts +125 -0
- package/dist/agent/index.d.ts +12 -4
- package/dist/agent/index.js +357 -6
- package/dist/agent/nuclear-form.d.ts +7 -0
- package/dist/{extensions → agent}/providers/deepseek.d.ts +2 -2
- package/dist/{extensions → agent}/providers/deepseek.js +5 -4
- package/dist/{extensions → agent}/providers/openai-compatible.d.ts +2 -2
- package/dist/{extensions → agent}/providers/openai.d.ts +2 -2
- package/dist/{extensions → agent}/providers/openai.js +3 -2
- package/dist/{extensions → agent}/providers/openrouter.d.ts +2 -2
- package/dist/{extensions → agent}/providers/openrouter.js +4 -3
- package/dist/agent/skills.js +51 -7
- package/dist/agent/subagent.d.ts +1 -1
- package/dist/agent/system-prompt.js +14 -17
- package/dist/agent/tool-protocol.d.ts +1 -1
- package/dist/agent/tool-protocol.js +5 -3
- package/dist/agent/tool-registry.d.ts +9 -4
- package/dist/agent/tool-registry.js +27 -4
- package/dist/agent/tools/bash.d.ts +1 -1
- package/dist/agent/tools/bash.js +3 -2
- package/dist/agent/tools/edit-file.js +0 -1
- package/dist/agent/tools/glob.js +1 -1
- package/dist/agent/tools/grep.js +1 -1
- package/dist/agent/tools/pwsh.d.ts +1 -1
- package/dist/agent/tools/pwsh.js +1 -2
- package/dist/agent/tools/read-file.js +7 -4
- package/dist/agent/tools/write-file.js +0 -1
- package/dist/agent/types.d.ts +17 -2
- package/dist/cli/auth/cli.d.ts +1 -0
- package/dist/cli/auth/cli.js +216 -0
- package/dist/cli/auth/keys.d.ts +31 -0
- package/dist/cli/auth/keys.js +102 -0
- package/dist/{index.js → cli/index.js} +29 -32
- package/dist/{init.js → cli/init.js} +1 -1
- package/dist/{install.js → cli/install.js} +31 -2
- package/dist/cli/subcommands.d.ts +1 -0
- package/dist/cli/subcommands.js +17 -0
- package/dist/{event-bus.d.ts → core/event-bus.d.ts} +7 -13
- package/dist/{extension-loader.d.ts → core/extension-loader.d.ts} +1 -1
- package/dist/{extension-loader.js → core/extension-loader.js} +62 -70
- package/dist/{core.d.ts → core/index.d.ts} +18 -15
- package/dist/{core.js → core/index.js} +18 -92
- package/dist/{settings.d.ts → core/settings.d.ts} +7 -0
- package/dist/{settings.js → core/settings.js} +1 -0
- package/dist/core/types.d.ts +49 -0
- package/dist/core/types.js +1 -0
- package/dist/extensions/file-autocomplete.d.ts +1 -1
- package/dist/extensions/index.d.ts +7 -14
- package/dist/extensions/index.js +2 -19
- package/dist/extensions/slash-commands.d.ts +1 -1
- package/dist/extensions/slash-commands.js +7 -2
- package/dist/shell/host-types.d.ts +114 -0
- package/dist/shell/host-types.js +1 -0
- package/dist/shell/index.d.ts +8 -7
- package/dist/shell/index.js +58 -9
- package/dist/shell/input-handler.d.ts +7 -1
- package/dist/shell/input-handler.js +5 -2
- package/dist/shell/output-parser.d.ts +1 -1
- package/dist/{extensions → shell}/shell-context.d.ts +1 -1
- package/dist/{extensions → shell}/shell-context.js +18 -12
- package/dist/shell/shell.d.ts +6 -4
- package/dist/shell/shell.js +33 -109
- package/dist/shell/strategies/bash.d.ts +2 -0
- package/dist/shell/strategies/bash.js +68 -0
- package/dist/shell/strategies/fish.d.ts +2 -0
- package/dist/shell/strategies/fish.js +65 -0
- package/dist/shell/strategies/index.d.ts +13 -0
- package/dist/shell/strategies/index.js +17 -0
- package/dist/shell/strategies/types.d.ts +50 -0
- package/dist/shell/strategies/types.js +9 -0
- package/dist/shell/strategies/zsh.d.ts +2 -0
- package/dist/shell/strategies/zsh.js +72 -0
- package/dist/shell/tui-input-view.js +14 -3
- package/dist/{extensions → shell}/tui-renderer.d.ts +1 -1
- package/dist/{extensions → shell}/tui-renderer.js +27 -55
- package/dist/utils/box-frame.d.ts +4 -0
- package/dist/utils/box-frame.js +17 -6
- package/dist/utils/compositor.d.ts +1 -1
- package/dist/utils/compositor.js +2 -1
- package/dist/{executor.js → utils/executor.js} +1 -1
- package/dist/utils/floating-panel.d.ts +1 -1
- package/dist/utils/floating-panel.js +9 -4
- package/dist/utils/llm-facade.d.ts +7 -3
- package/dist/utils/stream-transform.d.ts +1 -1
- package/dist/utils/terminal-buffer.d.ts +1 -1
- package/dist/utils/tool-display.js +4 -0
- package/dist/utils/tool-interactive.d.ts +1 -1
- package/dist/utils/tty.d.ts +7 -0
- package/dist/utils/tty.js +15 -0
- package/examples/extensions/ash-acp-bridge/README.md +4 -1
- package/examples/extensions/ash-acp-bridge/src/index.ts +654 -0
- package/examples/extensions/ash-mcp-bridge/index.ts +1 -1
- package/examples/extensions/ashi/README.md +250 -0
- package/examples/extensions/ashi/package.json +60 -0
- package/examples/extensions/ashi/src/autocomplete.ts +91 -0
- package/examples/extensions/ashi/src/capture.ts +34 -0
- package/examples/extensions/ashi/src/cli.ts +126 -0
- package/examples/extensions/ashi/src/commands.ts +82 -0
- package/examples/extensions/ashi/src/compaction.ts +157 -0
- package/examples/extensions/ashi/src/components.ts +332 -0
- package/examples/extensions/ashi/src/default-renderers.ts +153 -0
- package/examples/extensions/ashi/src/display-config.ts +62 -0
- package/examples/extensions/ashi/src/frontend.ts +735 -0
- package/examples/extensions/ashi/src/hooks.ts +136 -0
- package/examples/extensions/ashi/src/multi-session-store.ts +146 -0
- package/examples/extensions/ashi/src/session-commands.ts +76 -0
- package/examples/extensions/ashi/src/session-store.ts +264 -0
- package/examples/extensions/ashi/src/status-footer.ts +66 -0
- package/examples/extensions/ashi/src/theme.ts +151 -0
- package/examples/extensions/ashi/tsconfig.json +14 -0
- package/examples/extensions/emacs-buffer.ts +1 -1
- package/examples/extensions/interactive-prompts.ts +114 -69
- package/examples/extensions/latex-images.ts +3 -3
- package/examples/extensions/opencode-bridge/index.ts +1 -1
- package/examples/extensions/overlay-agent.ts +7 -5
- package/examples/extensions/peer-mesh.ts +1 -1
- package/examples/extensions/pi-bridge/index.ts +0 -1
- package/examples/extensions/questionnaire.ts +2 -1
- package/examples/extensions/rtk-proxy.ts +3 -3
- package/examples/extensions/solarized-theme.ts +3 -3
- package/examples/extensions/subagents.ts +6 -6
- package/examples/extensions/terminal-buffer.ts +1 -1
- package/examples/extensions/tmux-pane.ts +6 -4
- package/examples/extensions/tunnel-vision.ts +5 -5
- package/examples/extensions/user-shell.ts +1 -1
- package/examples/extensions/web-access.ts +5 -5
- package/package.json +26 -22
- package/dist/extensions/agent-backend.d.ts +0 -14
- package/dist/extensions/agent-backend.js +0 -307
- package/dist/types.d.ts +0 -227
- /package/dist/{types.js → agent/host-types.js} +0 -0
- /package/dist/{extensions → agent}/providers/openai-compatible.js +0 -0
- /package/dist/{index.d.ts → cli/index.d.ts} +0 -0
- /package/dist/{init.d.ts → cli/init.d.ts} +0 -0
- /package/dist/{install.d.ts → cli/install.d.ts} +0 -0
- /package/dist/{event-bus.js → core/event-bus.js} +0 -0
- /package/dist/{executor.d.ts → utils/executor.d.ts} +0 -0
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
# ashi
|
|
2
|
+
|
|
3
|
+
`ash` (agent-sh's built-in agent) running inside pi's TUI substrate, with no shell underneath.
|
|
4
|
+
|
|
5
|
+
A test of agent-sh's claim that the kernel is a frontend-agnostic communication layer: `ashi`
|
|
6
|
+
uses `createCore()` from agent-sh, skips `activateShell()`, disables the shell-coupled built-ins
|
|
7
|
+
(`shell-context`, `tui-renderer`, `file-autocomplete`), and mounts `@earendil-works/pi-tui` as
|
|
8
|
+
the only frontend. Backend, tools, slash commands, providers, and skills come along unchanged.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
agent-sh install ashi
|
|
14
|
+
export PATH="$HOME/.agent-sh/bin:$PATH"
|
|
15
|
+
ashi
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
`agent-sh install` copies this directory into `~/.agent-sh/extensions/ashi/`, runs
|
|
19
|
+
`npm install` and `npm run build` there, and symlinks the built bin into `~/.agent-sh/bin/`.
|
|
20
|
+
|
|
21
|
+
## Configure
|
|
22
|
+
|
|
23
|
+
Reads `~/.agent-sh/settings.json` for providers and defaults, same as `agent-sh` itself. The
|
|
24
|
+
quickest path is exporting `OPENROUTER_API_KEY` or `OPENAI_API_KEY` and running `ashi`.
|
|
25
|
+
|
|
26
|
+
See the agent-sh [Usage Guide](https://github.com/guanyilun/agent-sh/blob/main/docs/usage.md)
|
|
27
|
+
for the full `settings.json` schema, provider profiles, and model selection details.
|
|
28
|
+
|
|
29
|
+
CLI flags mirror `agent-sh`:
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
--provider <name> Provider profile from ~/.agent-sh/settings.json
|
|
33
|
+
--model <id> Override model
|
|
34
|
+
--api-key <key> Direct API key
|
|
35
|
+
--base-url <url> OpenAI-compatible base URL
|
|
36
|
+
--backend <name> Agent backend (default: ash)
|
|
37
|
+
-e, --extensions Extra extensions to load (comma-separated)
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Extensions loaded via `-e` follow the standard agent-sh extension contract — see
|
|
41
|
+
[Extensions](https://github.com/guanyilun/agent-sh/blob/main/docs/extensions.md) for the
|
|
42
|
+
`ExtensionContext` API, event bus, content transforms, and custom backends.
|
|
43
|
+
|
|
44
|
+
## Keybindings
|
|
45
|
+
|
|
46
|
+
Match pi-coding-agent's convention:
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
Esc Cancel active turn
|
|
50
|
+
Ctrl+C Clear editor
|
|
51
|
+
Ctrl+D Quit (when editor is empty)
|
|
52
|
+
Ctrl+T Toggle thinking-block visibility
|
|
53
|
+
Ctrl+O Expand/collapse the most recent tool result
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Sessions
|
|
57
|
+
|
|
58
|
+
ashi mirrors pi's session model: many sessions per cwd, fresh by default.
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
/resume Browse past sessions in this cwd (interactive picker)
|
|
62
|
+
/new Start a fresh session (discards in-memory context)
|
|
63
|
+
/name <text> Set a display name for the current session
|
|
64
|
+
/sessions Text dump of all sessions in this cwd
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Each session is its own tree (one JSONL file per session). Every entry has an `id` and
|
|
68
|
+
`parentId`; sibling branches stay on disk; you can rewind and branch within a session.
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
/fork Interactive in-session tree picker
|
|
72
|
+
/fork <id-prefix> Direct rewind to a specific entry
|
|
73
|
+
/branch Text dump of the active branch (root → leaf)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Storage: `~/.agent-sh/extensions/ashi/history/<cwd-slug>/sessions/<id>.jsonl`. Each line
|
|
77
|
+
is a `SessionEntry`:
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
type SessionEntry =
|
|
81
|
+
| { type: "session"; id; parentId: null; cwd; timestamp; version }
|
|
82
|
+
| { type: "message"; id; parentId; timestamp; message: AgentMessage }
|
|
83
|
+
| { type: "compaction"; id; parentId; timestamp; summary; firstKeptId; tokensBefore };
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Raw `AgentMessage` objects are stored verbatim (full tool call arguments, tool results,
|
|
87
|
+
etc.) so `/resume` and `/fork` faithfully reconstruct the original conversation — same
|
|
88
|
+
shape as pi's session format.
|
|
89
|
+
|
|
90
|
+
The kernel side adds three small handlers:
|
|
91
|
+
optional `parentSeq`/`getBranch`/`getTree`/`setLeaf` on `NuclearEntry`/`HistoryAdapter`
|
|
92
|
+
(useful for tree-aware HistoryAdapters in general — not used by this extension);
|
|
93
|
+
`conversation:allocate-seq` and `conversation:reset-for-session` so multi-session adapters
|
|
94
|
+
can swap context without nuclear-state bleed-through.
|
|
95
|
+
|
|
96
|
+
ashi itself bypasses agent-sh's `NuclearEntry` pipeline entirely by installing a
|
|
97
|
+
`NoopHistory` adapter — raw messages are captured directly via `agent:processing-done`
|
|
98
|
+
and the `conversation:get-messages` handler.
|
|
99
|
+
|
|
100
|
+
## Compaction
|
|
101
|
+
|
|
102
|
+
ashi replaces agent-sh's default deterministic two-tier-pin compaction with a pi-style
|
|
103
|
+
LLM-driven path:
|
|
104
|
+
|
|
105
|
+
1. Cut point: walk back from the newest message until ~20K tokens are kept; never cut at
|
|
106
|
+
tool results or in the middle of an assistant→tool call group.
|
|
107
|
+
2. LLM summarizes the older span into the pi structured format (Goal / Constraints /
|
|
108
|
+
Progress / Decisions / Next Steps / Critical Context).
|
|
109
|
+
3. The live message array becomes `[summary, ...kept messages]`.
|
|
110
|
+
4. The summary lands in the session as a `CompactionEntry` carrying `summary`,
|
|
111
|
+
`firstKeptId`, and `tokensBefore` — same shape as pi's compaction. Subsequent
|
|
112
|
+
compactions reference the previous one's summary so chains stay coherent.
|
|
113
|
+
|
|
114
|
+
Triggered automatically when prompt tokens cross agent-sh's threshold, or manually with
|
|
115
|
+
`/compact`. If the LLM call fails or the conversation is too short, falls through to the
|
|
116
|
+
default eviction.
|
|
117
|
+
|
|
118
|
+
The cut-point walker, prompt template, serialization, and LLM call all live in this
|
|
119
|
+
extension. The kernel side is just the advisable `conversation:compact` seam.
|
|
120
|
+
|
|
121
|
+
## What's intentionally missing
|
|
122
|
+
|
|
123
|
+
This is a spike, not a clone of pi's full UI. The MVP renders:
|
|
124
|
+
|
|
125
|
+
- User submissions, streaming assistant Markdown
|
|
126
|
+
- Tool invocations with start/complete state
|
|
127
|
+
- Slash commands with autocomplete (`/help`, `/model`, `/backend`, `/resume`, `/new`, `/fork`, …)
|
|
128
|
+
- Multi-session tree history with `/resume` and `/fork` pickers
|
|
129
|
+
- LLM compaction with summaries that survive across `/resume`
|
|
130
|
+
- Loader, errors, info messages
|
|
131
|
+
- Inline images via the `image` ContentBlock and the `render:image` handler — the
|
|
132
|
+
bundled `latex-images` extension works against ashi without modification
|
|
133
|
+
(terminal must support iTerm2 or Kitty graphics)
|
|
134
|
+
|
|
135
|
+
Out of scope for v0: branch summaries on `/fork` navigation (pi has this), `/clone`
|
|
136
|
+
(duplicate active branch into a new session), permission dialogs, diff renderer, file-path
|
|
137
|
+
autocomplete, session search/rename/delete inside the `/resume` picker, theme selector.
|
|
138
|
+
Each can be added by writing a pi-tui Component and subscribing to the corresponding
|
|
139
|
+
bus event.
|
|
140
|
+
|
|
141
|
+
## Extension surface
|
|
142
|
+
|
|
143
|
+
Other extensions can override how chat entries render without forking ashi.
|
|
144
|
+
Hooks are exposed via `ctx.define` (defaults) + `ctx.advise` (override).
|
|
145
|
+
|
|
146
|
+
### Chat hooks
|
|
147
|
+
|
|
148
|
+
| Hook | Args | Returns |
|
|
149
|
+
|---|---|---|
|
|
150
|
+
| `ashi:render-user-message` | `{ text, state, invalidate }` | `Component` |
|
|
151
|
+
| `ashi:render-assistant` | `{ text, state, invalidate }` | `Component` |
|
|
152
|
+
| `ashi:render-thinking` | `{ text, hidden, state, invalidate }` | `Component` |
|
|
153
|
+
|
|
154
|
+
### Tool hooks (per-tool)
|
|
155
|
+
|
|
156
|
+
Tool rendering is split into a call line (the input header) and a result body
|
|
157
|
+
(streaming output + final state). Each side is dispatched by tool name with a
|
|
158
|
+
`:default` fallback:
|
|
159
|
+
|
|
160
|
+
| Hook | Args | Returns |
|
|
161
|
+
|---|---|---|
|
|
162
|
+
| `ashi:render-tool-call:{name}` | `{ toolCallId, name, title, kind, displayDetail, rawInput, state, invalidate }` | `ToolCallView` |
|
|
163
|
+
| `ashi:render-tool-call:default` | (same) | `ToolCallView` |
|
|
164
|
+
| `ashi:render-tool-result:{name}` | `{ toolCallId, name, kind, rawInput, mode, previewLines, state, invalidate }` | `ToolResultView` |
|
|
165
|
+
| `ashi:render-tool-result:default` | (same) | `ToolResultView` |
|
|
166
|
+
|
|
167
|
+
`state` is a per-call mutable bag; `invalidate()` requests a re-render.
|
|
168
|
+
|
|
169
|
+
- `ToolCallView` extends `Component` with `setStatus({ exitCode, elapsedMs, summary })` — called once on completion.
|
|
170
|
+
- `ToolResultView` extends `Component` with `appendChunk(chunk)`, `setDiff(lines)`, `finalize({ exitCode, summary })`, and `toggleExpanded()` — ashi mutates the result view as output streams in and when the user hits `Ctrl+O`.
|
|
171
|
+
|
|
172
|
+
`mode` and `previewLines` on result args come from `ashi.display.{name}` config
|
|
173
|
+
(see below) so renderers can honor the user's compactness preference without
|
|
174
|
+
re-implementing the resolution logic.
|
|
175
|
+
|
|
176
|
+
Example: override how `bash` calls render.
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
export default function activate(ctx) {
|
|
180
|
+
ctx.advise("ashi:render-tool-call:bash", (next, args) => {
|
|
181
|
+
return new MyFancyBashLine(args); // must implement ToolCallView
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
For non-render concerns (commands, settings, tools, providers) use the standard
|
|
187
|
+
agent-sh extension API.
|
|
188
|
+
|
|
189
|
+
## Display configuration
|
|
190
|
+
|
|
191
|
+
Per-tool compactness lives under `ashi.display` in `~/.agent-sh/settings.json`:
|
|
192
|
+
|
|
193
|
+
```json
|
|
194
|
+
{
|
|
195
|
+
"ashi": {
|
|
196
|
+
"display": {
|
|
197
|
+
"default": { "result": "preview", "previewLines": 8 },
|
|
198
|
+
"read": { "result": "hidden" },
|
|
199
|
+
"ls": { "result": "hidden" },
|
|
200
|
+
"grep": { "result": "summary" },
|
|
201
|
+
"bash": { "result": "preview", "previewLines": 12 },
|
|
202
|
+
"edit": { "result": "preview" },
|
|
203
|
+
"write": { "result": "preview" }
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
`result` modes:
|
|
210
|
+
|
|
211
|
+
- `"hidden"` — call line only while streaming; line count (`↳ 42 lines`) after completion.
|
|
212
|
+
- `"summary"` — 2-line tail while streaming; line count after completion.
|
|
213
|
+
- `"preview"` — last `previewLines` lines of output (default 8), with a `... (N more lines)` hint when content overflows.
|
|
214
|
+
|
|
215
|
+
For `edit_file` / `write_file`, the diff frame is treated as the output and
|
|
216
|
+
follows the same gating: shown for `preview`, hidden for `hidden`/`summary`
|
|
217
|
+
(the call line already carries `+12 -3` stats). The line-count hint is
|
|
218
|
+
suppressed for diff-producing tools so edits stay quiet.
|
|
219
|
+
|
|
220
|
+
Hit `Ctrl+O` to expand the most recent tool result inline — shows the full
|
|
221
|
+
output buffer and the full diff regardless of mode. Press again to collapse.
|
|
222
|
+
|
|
223
|
+
Each tool inherits from `default` and is overridden by its own block. Unknown
|
|
224
|
+
tool names fall through to `default`. Built-in defaults aim for compactness
|
|
225
|
+
(`read`/`ls` hidden, `grep` summarized) — override under `default` to widen
|
|
226
|
+
everything, or under a specific tool to tune one.
|
|
227
|
+
|
|
228
|
+
## Development
|
|
229
|
+
|
|
230
|
+
`@guanyilun/ashi` depends on the published `agent-sh` package. To iterate against
|
|
231
|
+
the parent checkout instead, use `npm link`:
|
|
232
|
+
|
|
233
|
+
```bash
|
|
234
|
+
# one-time: register the local agent-sh checkout
|
|
235
|
+
cd /path/to/agent-sh
|
|
236
|
+
npm run build
|
|
237
|
+
npm link
|
|
238
|
+
|
|
239
|
+
# in ashi, point its agent-sh dependency at the linked checkout
|
|
240
|
+
cd examples/extensions/ashi
|
|
241
|
+
npm install
|
|
242
|
+
npm link agent-sh
|
|
243
|
+
|
|
244
|
+
npm run dev # tsx-driven, no compile step
|
|
245
|
+
# or: npm run build && node dist/cli.js
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
Rebuild agent-sh (`npm run build` at the repo root) whenever you change the
|
|
249
|
+
kernel — the link picks up `dist/` directly. To go back to the published
|
|
250
|
+
version, run `npm unlink agent-sh && npm install` inside `examples/extensions/ashi`.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@guanyilun/ashi",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Ash with a pi-tui frontend — agent-sh's kernel without the shell, rendered through pi's TUI library",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/cli.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"ashi": "dist/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"dev": "tsx src/cli.ts",
|
|
16
|
+
"clean": "rm -rf dist",
|
|
17
|
+
"build": "npm run clean && tsc",
|
|
18
|
+
"start": "node dist/cli.js",
|
|
19
|
+
"prepublishOnly": "npm run build"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"agent-sh",
|
|
23
|
+
"ashi",
|
|
24
|
+
"ash",
|
|
25
|
+
"pi-tui",
|
|
26
|
+
"tui",
|
|
27
|
+
"agent",
|
|
28
|
+
"ai",
|
|
29
|
+
"llm",
|
|
30
|
+
"cli"
|
|
31
|
+
],
|
|
32
|
+
"author": "Yilun Guan",
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "git+https://github.com/guanyilun/agent-sh.git",
|
|
37
|
+
"directory": "examples/extensions/ashi"
|
|
38
|
+
},
|
|
39
|
+
"homepage": "https://github.com/guanyilun/agent-sh/tree/main/examples/extensions/ashi",
|
|
40
|
+
"bugs": {
|
|
41
|
+
"url": "https://github.com/guanyilun/agent-sh/issues"
|
|
42
|
+
},
|
|
43
|
+
"engines": {
|
|
44
|
+
"node": ">=18"
|
|
45
|
+
},
|
|
46
|
+
"publishConfig": {
|
|
47
|
+
"access": "public"
|
|
48
|
+
},
|
|
49
|
+
"dependencies": {
|
|
50
|
+
"@earendil-works/pi-tui": "^0.74.0",
|
|
51
|
+
"agent-sh": "^0.12.27",
|
|
52
|
+
"chalk": "^5.5.0",
|
|
53
|
+
"cli-highlight": "^2.1.11"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@types/node": "^22.0.0",
|
|
57
|
+
"tsx": "^4.20.0",
|
|
58
|
+
"typescript": "^5.7.0"
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AutocompleteItem,
|
|
3
|
+
AutocompleteProvider,
|
|
4
|
+
AutocompleteSuggestions,
|
|
5
|
+
} from "@earendil-works/pi-tui";
|
|
6
|
+
import type { EventBus } from "agent-sh/event-bus";
|
|
7
|
+
|
|
8
|
+
/** Adapt pi-tui's AutocompleteProvider to agent-sh's autocomplete:request pipe.
|
|
9
|
+
* Reads the line up to the cursor, asks the bus for suggestions, returns them. */
|
|
10
|
+
export class BusAutocompleteProvider implements AutocompleteProvider {
|
|
11
|
+
constructor(private bus: EventBus) {}
|
|
12
|
+
|
|
13
|
+
async getSuggestions(
|
|
14
|
+
lines: string[],
|
|
15
|
+
cursorLine: number,
|
|
16
|
+
cursorCol: number,
|
|
17
|
+
): Promise<AutocompleteSuggestions | null> {
|
|
18
|
+
if (cursorLine !== 0) return null;
|
|
19
|
+
const line = lines[0] ?? "";
|
|
20
|
+
const before = line.slice(0, cursorCol);
|
|
21
|
+
|
|
22
|
+
const atSpan = findAtTrigger(before);
|
|
23
|
+
if (atSpan) {
|
|
24
|
+
const result = this.bus.emitPipe("autocomplete:request", {
|
|
25
|
+
buffer: atSpan.text, command: null, commandArgs: null, items: [],
|
|
26
|
+
});
|
|
27
|
+
if (result.items.length === 0) return null;
|
|
28
|
+
const items: AutocompleteItem[] = result.items.map((it) => ({
|
|
29
|
+
value: "@" + it.name,
|
|
30
|
+
label: "@" + it.name,
|
|
31
|
+
description: it.description,
|
|
32
|
+
}));
|
|
33
|
+
return { items, prefix: atSpan.text };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!before.startsWith("/")) return null;
|
|
37
|
+
|
|
38
|
+
const sp = before.indexOf(" ");
|
|
39
|
+
const command = sp === -1 ? null : before.slice(0, sp);
|
|
40
|
+
const commandArgs = sp === -1 ? null : before.slice(sp + 1);
|
|
41
|
+
|
|
42
|
+
const result = this.bus.emitPipe("autocomplete:request", {
|
|
43
|
+
buffer: before,
|
|
44
|
+
command,
|
|
45
|
+
commandArgs,
|
|
46
|
+
items: [],
|
|
47
|
+
});
|
|
48
|
+
if (result.items.length === 0) return null;
|
|
49
|
+
|
|
50
|
+
const items: AutocompleteItem[] = result.items.map((it) => ({
|
|
51
|
+
value: it.name,
|
|
52
|
+
label: it.name,
|
|
53
|
+
description: it.description,
|
|
54
|
+
}));
|
|
55
|
+
return { items, prefix: before };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
applyCompletion(
|
|
59
|
+
lines: string[],
|
|
60
|
+
cursorLine: number,
|
|
61
|
+
cursorCol: number,
|
|
62
|
+
item: AutocompleteItem,
|
|
63
|
+
prefix: string,
|
|
64
|
+
): { lines: string[]; cursorLine: number; cursorCol: number } {
|
|
65
|
+
const line = lines[cursorLine] ?? "";
|
|
66
|
+
// Replace the prefix span (the leading slash-word + args we sent) with
|
|
67
|
+
// the completion value. Anything after the cursor is preserved.
|
|
68
|
+
const head = line.slice(0, cursorCol - prefix.length);
|
|
69
|
+
const tail = line.slice(cursorCol);
|
|
70
|
+
const newLine = `${head}${item.value}${tail}`;
|
|
71
|
+
const out = lines.slice();
|
|
72
|
+
out[cursorLine] = newLine;
|
|
73
|
+
return {
|
|
74
|
+
lines: out,
|
|
75
|
+
cursorLine,
|
|
76
|
+
cursorCol: (head + item.value).length,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Locate an active `@` file-trigger in the text preceding the cursor.
|
|
82
|
+
* Matches when `@` is at start or after whitespace and the chars from
|
|
83
|
+
* `@` to cursor are path-friendly (letters, digits, `.`, `/`, `_`, `-`). */
|
|
84
|
+
function findAtTrigger(before: string): { text: string } | null {
|
|
85
|
+
const at = before.lastIndexOf("@");
|
|
86
|
+
if (at < 0) return null;
|
|
87
|
+
if (at > 0 && !/\s/.test(before[at - 1]!)) return null;
|
|
88
|
+
const query = before.slice(at + 1);
|
|
89
|
+
if (!/^[a-zA-Z0-9_./-]*$/.test(query)) return null;
|
|
90
|
+
return { text: before.slice(at) };
|
|
91
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { ExtensionContext } from "agent-sh/types";
|
|
2
|
+
import type { MultiSessionStore } from "./multi-session-store.js";
|
|
3
|
+
import type { AgentMessage } from "./session-store.js";
|
|
4
|
+
|
|
5
|
+
/** Maintains an `(entryId | null)[]` parallel to the live messages array;
|
|
6
|
+
* null slots are synthetics like compaction summaries that have no entry. */
|
|
7
|
+
export interface Capture {
|
|
8
|
+
flush(): Promise<void>;
|
|
9
|
+
getEntryIdAt(messageIndex: number): string | null;
|
|
10
|
+
resetTo(ids: (string | null)[]): void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function registerCapture(
|
|
14
|
+
ctx: ExtensionContext,
|
|
15
|
+
getStore: () => MultiSessionStore,
|
|
16
|
+
): Capture {
|
|
17
|
+
let liveEntryIds: (string | null)[] = [];
|
|
18
|
+
|
|
19
|
+
const flush = async (): Promise<void> => {
|
|
20
|
+
const messages = ctx.call("conversation:get-messages") as AgentMessage[] | undefined;
|
|
21
|
+
if (!messages || messages.length <= liveEntryIds.length) return;
|
|
22
|
+
const newMessages = messages.slice(liveEntryIds.length);
|
|
23
|
+
const newIds = await getStore().current().appendMessages(newMessages);
|
|
24
|
+
liveEntryIds = [...liveEntryIds, ...newIds];
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
ctx.bus.on("agent:processing-done", () => { void flush(); });
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
flush,
|
|
31
|
+
getEntryIdAt: (i) => liveEntryIds[i] ?? null,
|
|
32
|
+
resetTo: (ids) => { liveEntryIds = [...ids]; },
|
|
33
|
+
};
|
|
34
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* ashi — agent-sh's ash backend with a pi-tui frontend, no shell.
|
|
4
|
+
*
|
|
5
|
+
* Boots the agent-sh kernel directly, skips the PTY shell and the
|
|
6
|
+
* default streaming tui-renderer, and mounts pi-tui as the sole
|
|
7
|
+
* frontend. Demonstrates that the kernel is frontend-agnostic — same
|
|
8
|
+
* backend, tools, slash commands, providers; different presentation.
|
|
9
|
+
*/
|
|
10
|
+
import { createCore, NoopHistory } from "agent-sh/core";
|
|
11
|
+
import { loadBuiltinExtensions } from "agent-sh/extensions";
|
|
12
|
+
import { loadExtensions } from "agent-sh/extension-loader";
|
|
13
|
+
import { activateAgent } from "agent-sh/agent";
|
|
14
|
+
import { getSettings } from "agent-sh/settings";
|
|
15
|
+
import type { AppConfig } from "agent-sh/types";
|
|
16
|
+
|
|
17
|
+
import { mountAshi } from "./frontend.js";
|
|
18
|
+
import { MultiSessionStore } from "./multi-session-store.js";
|
|
19
|
+
import { registerForkCommands } from "./commands.js";
|
|
20
|
+
import { registerSessionCommands } from "./session-commands.js";
|
|
21
|
+
import { registerCompaction } from "./compaction.js";
|
|
22
|
+
import { registerCapture } from "./capture.js";
|
|
23
|
+
import { registerRenderDefaults } from "./hooks.js";
|
|
24
|
+
import { registerDefaultToolRenderers } from "./default-renderers.js";
|
|
25
|
+
import * as os from "node:os";
|
|
26
|
+
import * as path from "node:path";
|
|
27
|
+
|
|
28
|
+
function parseArgs(argv: string[]): AppConfig & { extensions?: string[] } {
|
|
29
|
+
let model: string | undefined;
|
|
30
|
+
let apiKey: string | undefined = process.env.OPENAI_API_KEY ?? process.env.OPENROUTER_API_KEY;
|
|
31
|
+
let baseURL: string | undefined = process.env.OPENAI_BASE_URL;
|
|
32
|
+
let provider: string | undefined;
|
|
33
|
+
let backend: string | undefined;
|
|
34
|
+
const extensions: string[] = [];
|
|
35
|
+
|
|
36
|
+
for (let i = 0; i < argv.length; i++) {
|
|
37
|
+
const a = argv[i];
|
|
38
|
+
if (a === "--model" && argv[i + 1]) model = argv[++i];
|
|
39
|
+
else if (a === "--api-key" && argv[i + 1]) apiKey = argv[++i];
|
|
40
|
+
else if (a === "--base-url" && argv[i + 1]) baseURL = argv[++i];
|
|
41
|
+
else if (a === "--provider" && argv[i + 1]) provider = argv[++i];
|
|
42
|
+
else if (a === "--backend" && argv[i + 1]) backend = argv[++i];
|
|
43
|
+
else if ((a === "-e" || a === "--extensions") && argv[i + 1]) {
|
|
44
|
+
extensions.push(...argv[++i]!.split(",").map(s => s.trim()).filter(Boolean));
|
|
45
|
+
} else if (a === "-h" || a === "--help") {
|
|
46
|
+
process.stdout.write(`ashi — ash backend, pi-tui frontend\n\n` +
|
|
47
|
+
`Usage: ashi [--provider <name>] [--model <id>] [--api-key <key>] [--base-url <url>]\n` +
|
|
48
|
+
` [--backend <name>] [-e <ext>[,<ext>...]]\n\n` +
|
|
49
|
+
`Reads ~/.agent-sh/settings.json for providers and defaults.\n`);
|
|
50
|
+
process.exit(0);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return { shell: "/bin/sh", model, apiKey, baseURL, provider, backend, extensions };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function main(): Promise<void> {
|
|
58
|
+
const config = parseArgs(process.argv.slice(2));
|
|
59
|
+
|
|
60
|
+
if (!process.stdin.isTTY) {
|
|
61
|
+
process.stderr.write("ashi requires a TTY for interactive rendering.\n");
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const cwd = process.cwd();
|
|
66
|
+
const cwdSlug = cwd.replace(/\//g, "-").replace(/^-/, "");
|
|
67
|
+
const sessionsDir = path.join(os.homedir(), ".agent-sh", "ashi", "history", cwdSlug, "sessions");
|
|
68
|
+
const store = new MultiSessionStore(sessionsDir, cwd);
|
|
69
|
+
const getStore = (): MultiSessionStore => store;
|
|
70
|
+
|
|
71
|
+
const core = createCore({ ...config, history: new NoopHistory() });
|
|
72
|
+
|
|
73
|
+
let stopFrontend: (() => void) | null = null;
|
|
74
|
+
|
|
75
|
+
const cleanup = (): void => {
|
|
76
|
+
try { stopFrontend?.(); } catch { /* ignore */ }
|
|
77
|
+
try { core.kill(); } catch { /* ignore */ }
|
|
78
|
+
if (process.stdin.isTTY) {
|
|
79
|
+
try { process.stdin.setRawMode(false); } catch { /* ignore */ }
|
|
80
|
+
}
|
|
81
|
+
process.exit(0);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const ctx = core.extensionContext({ quit: cleanup });
|
|
85
|
+
|
|
86
|
+
activateAgent(ctx);
|
|
87
|
+
await loadBuiltinExtensions(ctx);
|
|
88
|
+
|
|
89
|
+
const loaded = await loadExtensions(ctx, config.extensions);
|
|
90
|
+
core.bus.emit("core:extensions-loaded", { names: loaded });
|
|
91
|
+
|
|
92
|
+
const { names: backendNames } = core.bus.emitPipe("config:get-backends", {
|
|
93
|
+
names: [] as string[], active: null as string | null,
|
|
94
|
+
});
|
|
95
|
+
if (backendNames.length === 0) {
|
|
96
|
+
process.stderr.write("ashi: no agent backend registered. Set OPENAI_API_KEY or OPENROUTER_API_KEY, or configure ~/.agent-sh/settings.json.\n");
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const capture = registerCapture(ctx, getStore);
|
|
101
|
+
registerCompaction(ctx, getStore, capture);
|
|
102
|
+
registerRenderDefaults(ctx);
|
|
103
|
+
registerDefaultToolRenderers(ctx);
|
|
104
|
+
|
|
105
|
+
ctx.advise("conversation:format-prior-history", () => null);
|
|
106
|
+
ctx.advise("system-prompt:build", (base) => `${base}\n\n<cwd>${process.cwd()}</cwd>`);
|
|
107
|
+
|
|
108
|
+
const handle = mountAshi(ctx, getStore, capture);
|
|
109
|
+
stopFrontend = handle.stop;
|
|
110
|
+
|
|
111
|
+
registerForkCommands(ctx, getStore, handle.openTreePicker, handle.rebuildChat, capture);
|
|
112
|
+
registerSessionCommands(ctx, getStore, capture, {
|
|
113
|
+
openSessionPicker: handle.openSessionPicker,
|
|
114
|
+
rebuildChat: handle.rebuildChat,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
await core.activateBackend(config.backend ?? getSettings().defaultBackend);
|
|
118
|
+
|
|
119
|
+
process.on("SIGTERM", cleanup);
|
|
120
|
+
process.on("SIGHUP", cleanup);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
main().catch((err) => {
|
|
124
|
+
process.stderr.write(`ashi fatal: ${err?.stack ?? err}\n`);
|
|
125
|
+
process.exit(1);
|
|
126
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { ExtensionContext } from "agent-sh/types";
|
|
2
|
+
import type { MultiSessionStore } from "./multi-session-store.js";
|
|
3
|
+
import type { AgentMessage } from "./session-store.js";
|
|
4
|
+
import type { Capture } from "./capture.js";
|
|
5
|
+
|
|
6
|
+
export function registerForkCommands(
|
|
7
|
+
ctx: ExtensionContext,
|
|
8
|
+
getStore: () => MultiSessionStore,
|
|
9
|
+
openTreePicker: () => Promise<void>,
|
|
10
|
+
rebuildChat: () => Promise<void>,
|
|
11
|
+
capture: Capture,
|
|
12
|
+
): void {
|
|
13
|
+
const { bus } = ctx;
|
|
14
|
+
|
|
15
|
+
ctx.registerCommand("fork", "Rewind and branch: /fork (interactive picker) or /fork <id-prefix>", async (args) => {
|
|
16
|
+
const arg = args.trim();
|
|
17
|
+
if (arg === "") {
|
|
18
|
+
await openTreePicker();
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const branch = getStore().current().getBranch();
|
|
22
|
+
const matches = branch.filter((e) => e.id.startsWith(arg));
|
|
23
|
+
if (matches.length === 0) {
|
|
24
|
+
bus.emit("ui:error", { message: `fork: no entry matches "${arg}"` });
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (matches.length > 1) {
|
|
28
|
+
bus.emit("ui:error", { message: `fork: ambiguous prefix "${arg}" matches ${matches.length} entries` });
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const target = matches[0]!;
|
|
32
|
+
getStore().current().setActiveLeaf(target.id);
|
|
33
|
+
applyBranchMessages(ctx, getStore, capture);
|
|
34
|
+
bus.emit("ui:info", { message: `fork: rewound to ${target.id}` });
|
|
35
|
+
await rebuildChat();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
ctx.registerCommand("branch", "Show the active branch (root → leaf)", async () => {
|
|
39
|
+
const branch = getStore().current().getBranch();
|
|
40
|
+
if (branch.length === 0) {
|
|
41
|
+
bus.emit("ui:info", { message: "branch: empty" });
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const lines = branch.map((e) => {
|
|
45
|
+
if (e.type === "session") return `[${e.id}] session start (${e.cwd})`;
|
|
46
|
+
if (e.type === "compaction") return `[${e.id}] compaction (firstKept=${e.firstKeptId})`;
|
|
47
|
+
const msg = (e as { message: AgentMessage }).message;
|
|
48
|
+
const text = typeof msg.content === "string" ? msg.content : "";
|
|
49
|
+
return `[${e.id}] ${msg.role}: ${text.slice(0, 60)}`;
|
|
50
|
+
});
|
|
51
|
+
bus.emit("ui:info", { message: `branch (${branch.length} entries):\n${lines.join("\n")}` });
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function applyBranchMessages(
|
|
56
|
+
ctx: ExtensionContext,
|
|
57
|
+
getStore: () => MultiSessionStore,
|
|
58
|
+
capture: Capture,
|
|
59
|
+
): void {
|
|
60
|
+
const store = getStore().current();
|
|
61
|
+
ctx.call("conversation:replace-messages", store.buildMessages());
|
|
62
|
+
|
|
63
|
+
const branch = store.getBranch();
|
|
64
|
+
let compaction: { firstKeptId: string } | null = null;
|
|
65
|
+
for (let i = branch.length - 1; i >= 0; i--) {
|
|
66
|
+
if (branch[i]!.type === "compaction") {
|
|
67
|
+
compaction = branch[i] as { firstKeptId: string };
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const ids: (string | null)[] = [];
|
|
72
|
+
if (compaction) {
|
|
73
|
+
ids.push(null);
|
|
74
|
+
const startIdx = branch.findIndex((e) => e.id === compaction!.firstKeptId);
|
|
75
|
+
for (let i = Math.max(0, startIdx); i < branch.length; i++) {
|
|
76
|
+
if (branch[i]!.type === "message") ids.push(branch[i]!.id);
|
|
77
|
+
}
|
|
78
|
+
} else {
|
|
79
|
+
for (const e of branch) if (e.type === "message") ids.push(e.id);
|
|
80
|
+
}
|
|
81
|
+
capture.resetTo(ids);
|
|
82
|
+
}
|