@fifthrevision/axle-cli 0.30.2 → 0.31.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 ADDED
@@ -0,0 +1,295 @@
1
+ # Axle CLI
2
+
3
+ An AI task runner built on [Axle](https://www.npmjs.com/package/@fifthrevision/axle).
4
+ Chat from the terminal, save recurring jobs as checked-in YAML recipes, fan a
5
+ recipe out over a folder of inputs — and resume any run, because every run is
6
+ a session.
7
+
8
+ A recipe is a saved partial application of an invocation: anything the
9
+ command line could say has a home in the YAML, and the command line
10
+ overrides selectively. The normative design lives in
11
+ [docs/architecture/cli.md](../../docs/architecture/cli.md).
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ npm install -g @fifthrevision/axle-cli
17
+ ```
18
+
19
+ ## Usage
20
+
21
+ Bare `axle` starts an interactive chat using the default provider and model
22
+ from `~/.axle/cli.yaml` (`defaults.provider`, `defaults.models`). Running a
23
+ YAML job file with `-j` is the non-interactive path.
24
+
25
+ On first run with no configuration anywhere — no credentials, no `cli.yaml`
26
+ providers or defaults, no inline provider in the recipe — `axle` launches a
27
+ setup wizard:
28
+ pick a provider, paste a key (written to `~/.axle/credentials`, chmod 600),
29
+ and pick a default model. Re-run it anytime with `axle setup`. A run that
30
+ can't resolve a model drops into the same model picker.
31
+
32
+ Sessions accumulate under `~/.axle/sessions/cli/` with no automatic
33
+ retention; `axle cleanup` deletes them by age window (24h/7d/30d/all).
34
+
35
+ ```bash
36
+ axle # interactive chat from configured defaults
37
+ axle -m "one question" # one-shot message, prints and exits
38
+ axle -j path/to/job.yaml # run a job file and exit
39
+ axle -j path/to/job.yaml -i # run the task, then continue interactively
40
+ axle -j path/to/job.yaml --args key=value other=thing
41
+ axle batch -j recipe.yaml 'data/*.md' # fan a recipe out over inputs
42
+ axle resume <id> # re-enter any saved session
43
+ axle resume <id> -m "follow up" # one-shot continuation
44
+ axle setup # (re)configure providers and defaults
45
+ axle cleanup # delete old sessions by age window
46
+ ```
47
+
48
+ Verbs select the machine; flags parameterize it. A session id prefix works
49
+ anywhere a full id does (`axle resume 3a2f` finds the unique match).
50
+
51
+ In the chat, `/quit` (or Ctrl-C / Ctrl-D at the prompt) exits. Ctrl-C during
52
+ a turn asks the agent to stop at the next tool boundary; a second Ctrl-C
53
+ cancels immediately. The session is saved on every exit path.
54
+
55
+ `--renderer` picks the screen renderer for the run: `ink` (default — terminal
56
+ UI with a live streaming region and input line) or `plain` (line-oriented).
57
+ Piped input or output always gets plain. `--no-log` disables the run log
58
+ (otherwise written to `~/.axle/logs/cli/<timestamp>.log`), `-d`/`--debug`
59
+ prints debug detail, and `--args key=value` supplies `{{variables}}` to the
60
+ recipe's task template.
61
+
62
+ Every run persists a resumable session to `~/.axle/sessions/cli/<id>.json`
63
+ (the id is printed at run start and exit). Resuming restores the saved
64
+ provider, model, tools, and conversation — no job file needed.
65
+
66
+ A job file specifies the provider, task prompt, and optional tools/files:
67
+
68
+ ```yaml
69
+ # job.yaml
70
+ provider: anthropic
71
+ model: anthropic/claude-sonnet-5
72
+
73
+ task: |
74
+ Summarize the attached document.
75
+
76
+ tools:
77
+ - calculator
78
+
79
+ providerTools:
80
+ - web_search
81
+
82
+ files:
83
+ - ./data/report.txt
84
+ ```
85
+
86
+ `provider` says where requests go. A string names a provider — a built-in
87
+ type (`anthropic`, `openai`, `gemini`, `chatcompletions`) or a provider
88
+ profile from `cli.yaml` — and an object is inline endpoint configuration.
89
+ Both `provider` and `model` are optional; anything the job leaves out
90
+ resolves through the config chain:
91
+
92
+ - provider: job → `defaults.provider` in `cli.yaml`
93
+ - model: job → `defaults.models.<provider name>` → `<TYPE>_MODEL` env or
94
+ credentials → interactive model picker
95
+
96
+ So a model-only job runs on the configured default provider, and a job with
97
+ neither runs entirely on defaults. `model` is a publisher-qualified registry
98
+ id (e.g. `anthropic/claude-sonnet-5`, `openai/gpt-5.5`) or a bare
99
+ provider-native id:
100
+
101
+ ```yaml
102
+ # Ollama, or any OpenAI-compatible endpoint
103
+ provider:
104
+ type: chatcompletions
105
+ baseUrl: http://localhost:11434/v1
106
+ model: gemma3
107
+ ```
108
+
109
+ Optional `system` sets the system prompt, and an optional `request` block
110
+ sets provider-portable request options:
111
+
112
+ ```yaml
113
+ system: You are a terse analyst.
114
+
115
+ request:
116
+ reasoning: on
117
+ temperature: 0.2
118
+ maxOutputTokens: 16000
119
+ ```
120
+
121
+ `reasoning` takes `default`, `off`, `on`, or `{ effort: low | medium | high }`.
122
+ Leave it unset (or `default`) and the model runs at its provider's own
123
+ default, which is always safe, including for models that cannot disable
124
+ thinking. `on` is medium effort; `off` sends the provider's explicit disable
125
+ and is rejected by models that cannot turn thinking off. On models that only
126
+ take a thinking budget (Claude Haiku, Opus, and Sonnet 4.5; Gemini 2.5), a
127
+ `maxOutputTokens` you set must exceed the budget: 8,192 for `on`, 16,384 for
128
+ `high`. Leave it unset and Axle's default already does.
129
+
130
+ ```yaml
131
+ request:
132
+ reasoning:
133
+ effort: high
134
+ ```
135
+
136
+ Long sessions compact automatically: when the conversation approaches the
137
+ model's context window (~80%), the next send first replaces the history with
138
+ a ~1000-word summary plus a slice of recent user messages kept verbatim (up
139
+ to a tenth of the threshold), summarized by the session's own provider, model, and
140
+ `reasoning` setting; the transcript records a `✔ Compacted context` line.
141
+ Compacted sessions snapshot and resume like any other. Opt out per recipe
142
+ with:
143
+
144
+ ```yaml
145
+ compaction: false
146
+ ```
147
+
148
+ `AXLE_CONTEXT_WINDOW=<tokens>` overrides the resolved window when the
149
+ registry gets a model wrong — the usage bar, compaction threshold, and
150
+ summary target all scale with it. A small value (e.g. `3000`) forces a
151
+ compaction within a few exchanges, which is also the way to see one without
152
+ filling a real context window.
153
+
154
+ CLI job files can use these local tool names:
155
+
156
+ - `calculator`
157
+ - `exec`
158
+ - `patch-file`
159
+ - `read-file`
160
+ - `write-file`
161
+
162
+ ## Batch
163
+
164
+ Batch is map(recipe, inputs): one isolated session per input. Inputs resolve
165
+ as positional arguments to the `batch` verb, then the recipe's `batch:`
166
+ block, then an interactive prompt:
167
+
168
+ ```bash
169
+ axle batch -j summarize.yml 'data/*.txt' # inputs from the command line
170
+ axle batch -j summarize.yml # inputs from the recipe, or prompted
171
+ axle -j summarize.yml # batch: block present → batch run
172
+ ```
173
+
174
+ For a recurring job, put the inputs in the recipe — it stays
175
+ self-documenting and runs with plain `-j`:
176
+
177
+ ```yaml
178
+ # job.yaml
179
+ provider: anthropic
180
+
181
+ task: |
182
+ Summarize this file ({{file}}).
183
+
184
+ batch:
185
+ files: "./data/*.txt"
186
+ concurrency: 3
187
+ ```
188
+
189
+ Each matched file is attached to the instruct and available as `{{file}}`.
190
+ Every input runs as its own session, so a failed item is inspected or
191
+ continued like any other run: `axle resume <id>` (every settled item line
192
+ prints the id). A project-local ledger (`.axle/batch.jsonl`) indexes
193
+ input → session. Skipping is opt-in: `--incremental` (or
194
+ `incremental: true` in the block; `--no-incremental` overrides) skips
195
+ completed inputs whose content is unchanged — useful when a folder of
196
+ inputs grows over time. Recipe edits never auto-invalidate; a plain run is
197
+ the force-fresh gesture and re-runs everything.
198
+
199
+ On a terminal, batch shows test-runner-style progress: one spinner row per
200
+ in-flight item (current phase, elapsed) and a running totals line, with
201
+ settled items committed to scrollback. `--verbose` (or `concurrency: 1`)
202
+ streams each item's full transcript instead. Piped output prints one line
203
+ per settled item.
204
+
205
+ Batch runs are non-interactive; a batch job cannot be combined with
206
+ `--interactive`.
207
+
208
+ ## MCP Servers
209
+
210
+ Add an `mcps` key to connect to MCP servers. Both stdio and HTTP transports
211
+ are supported.
212
+
213
+ ```yaml
214
+ # job.yaml
215
+ provider:
216
+ type: anthropic
217
+
218
+ mcps:
219
+ - name: wc
220
+ transport: stdio
221
+ command: npx
222
+ args: ["tsx", "examples/mcps/wordcount-server.ts"]
223
+ - transport: http
224
+ url: http://localhost:3100/mcp
225
+
226
+ task: |
227
+ Count the words in "hello world"
228
+ ```
229
+
230
+ Each entry supports:
231
+
232
+ - `transport` — `"stdio"` or `"http"` (required)
233
+ - `name` — prefix for tool names from this server (optional)
234
+ - `command` / `args` / `env` — for stdio transport
235
+ - `url` / `headers` — for HTTP transport
236
+
237
+ ## Configuration
238
+
239
+ For CLI use, put provider secrets in your environment, a local `.env` file, or
240
+ a credentials file. Credentials files use the same key names as the
241
+ environment variables, one `KEY=value` per line, and are read in order —
242
+ environment first, then the project's `.axle/credentials`, then the
243
+ user-level `~/.axle/credentials`:
244
+
245
+ ```bash
246
+ OPENAI_API_KEY=...
247
+ ANTHROPIC_API_KEY=...
248
+ GEMINI_API_KEY=...
249
+ ```
250
+
251
+ Optional model overrides use provider-specific variables:
252
+
253
+ ```bash
254
+ OPENAI_MODEL=openai/gpt-5.5
255
+ ANTHROPIC_MODEL=anthropic/claude-sonnet-5
256
+ GEMINI_MODEL=google/gemini-3.5-pro
257
+ ```
258
+
259
+ For OpenAI-compatible endpoints:
260
+
261
+ ```bash
262
+ CHATCOMPLETIONS_BASE_URL=http://localhost:11434/v1
263
+ CHATCOMPLETIONS_MODEL=llama3
264
+ CHATCOMPLETIONS_API_KEY=...
265
+ ```
266
+
267
+ Provider-level keys in the job file override environment variables. To
268
+ reference a non-standard environment variable from a job file, use `apiKeyEnv`:
269
+
270
+ ```yaml
271
+ provider:
272
+ type: openai
273
+ apiKeyEnv: CUSTOM_OPENAI_KEY
274
+ ```
275
+
276
+ `cli.yaml` (user-level `~/.axle/cli.yaml`, overridden per-project by
277
+ `.axle/cli.yaml`) holds named provider profiles and defaults:
278
+
279
+ ```yaml
280
+ providers:
281
+ openrouter: # a profile: pure endpoint config, no model
282
+ type: chatcompletions
283
+ baseUrl: https://openrouter.ai/api/v1
284
+ apiKeyEnv: OPENROUTER_API_KEY
285
+
286
+ defaults:
287
+ provider: openrouter # used when a job names no provider
288
+ models: # per-provider default models
289
+ openrouter: z-ai/glm-4.6
290
+ anthropic: anthropic/claude-sonnet-5
291
+ ```
292
+
293
+ Profile names share a namespace with the built-in types and may shadow
294
+ them. Across the user and project layers, `defaults` merge per key while
295
+ profiles replace wholesale.
@@ -0,0 +1 @@
1
+ import{a as e,i as t}from"./format-CVXITc-j.js";import{a as n,i as r,t as i}from"./store-yk8NwV-f.js";import{Static as a,Text as o,render as s,useInput as c}from"ink";import{useSyncExternalStore as l}from"react";import{Fragment as u,jsx as d,jsxs as f}from"react/jsx-runtime";function p({store:e}){let i=l(e.subscribe,e.getSnapshot),s=n();c((e,t)=>{t.ctrl&&e===`c`&&i.onInterrupt?.()});let p=Date.now();return f(u,{children:[d(a,{items:i.staticItems,children:(e,t)=>d(r,{level:e.level,text:e.text},t)}),!i.closed&&i.rows.map(e=>f(o,{children:[d(o,{color:`cyan`,children:s}),` `,h(e.input),` `,f(o,{dimColor:!0,children:[`· `,e.phase,` · `,t(p-e.startedAt)]})]},e.input)),!i.closed&&i.totals&&d(m,{totals:i.totals})]})}function m({totals:t}){let n=t.completed+t.skipped+t.failed;return f(o,{dimColor:!0,children:[` `,n,`/`,t.total,` settled`,t.failed>0?` · ${t.failed} failed`:``,` · ↑ `,e(t.tokensIn),` ↓`,` `,e(t.tokensOut)]})}function h(e){let t=e.split(`/`);return t.length>2?`…/${t.slice(-2).join(`/`)}`:e}var g=class{store=new i({staticItems:[],rows:[]});instance;constructor(){this.instance=s(d(p,{store:this.store}),{exitOnCtrlC:!1,patchConsole:!1})}batchStarted(e){this.store.update(t=>({...t,totals:e}))}itemStarted(e){this.store.update(t=>({...t,rows:[...t.rows,{input:e,phase:`starting`,startedAt:Date.now()}]}))}itemPhase(e,t){this.store.update(n=>({...n,rows:n.rows.map(n=>n.input===e?{...n,phase:t}:n)}))}itemFinished(e,t){this.store.update(n=>({...n,rows:n.rows.filter(t=>t.input!==e),totals:t}))}renderPriorTurns(e){}onEvent(e,t){}info(e){this.host(`info`,e)}success(e){this.host(`success`,e)}warn(e){this.host(`warn`,e)}error(e){this.host(`error`,e)}promptInput(){return Promise.resolve(null)}updateUsage(e){}setInterruptHandler(e){this.store.update(t=>({...t,onInterrupt:e}))}async close(){this.store.update(e=>({...e,closed:!0})),await new Promise(e=>setTimeout(e,20)),this.instance.unmount()}host(e,t){this.store.update(n=>({...n,staticItems:[...n.staticItems,{level:e,text:t}]}))}};export{g as InkBatchRenderer};
@@ -0,0 +1 @@
1
+ import{n as e,r as t,t as n}from"./store-yk8NwV-f.js";import{render as r}from"ink";import{jsx as i}from"react/jsx-runtime";var a=class{store=new n({staticItems:[],awaitingInput:!1,queuedInputs:[]});committed=new Set;instance;waiter;constructor(e){this.instance=r(i(t,{store:this.store,onSubmit:this.handleSubmit,statusBar:e?.statusBar??!0}),{exitOnCtrlC:!1,patchConsole:!1})}handleSubmit=e=>{let t=this.waiter;t?(this.waiter=void 0,this.store.update(e=>({...e,awaitingInput:!1})),t(e)):e!==null&&this.store.update(t=>({...t,queuedInputs:[...t.queuedInputs,e]}))};renderPriorTurns(e){if(e.length===0)return;let t=e.map(e=>({kind:`turn`,turn:e}));for(let t of e)this.committed.add(t.id);this.store.update(e=>({...e,staticItems:[...e.staticItems,...t]}))}onEvent(t,n){let{newlyFinished:r,liveTurn:i}=e(n.turns,this.committed);this.store.update(e=>({...e,staticItems:r.length?[...e.staticItems,...r]:e.staticItems,liveTurn:i}))}info(e){this.host(`info`,e)}success(e){this.host(`success`,e)}warn(e){this.host(`warn`,e)}error(e){this.host(`error`,e)}updateUsage(e){this.store.update(t=>({...t,usage:e}))}setInterruptHandler(e){this.store.update(t=>({...t,onInterrupt:e}))}promptInput(){let e=this.store.getSnapshot().queuedInputs;if(e.length>0){let[t,...n]=e;return this.store.update(e=>({...e,queuedInputs:n})),Promise.resolve(t)}return new Promise(e=>{this.waiter=e,this.store.update(e=>({...e,awaitingInput:!0}))})}async close(){this.handleSubmit(null),this.store.update(e=>({...e,closed:!0})),this.store.update(e=>e.liveTurn?{...e,staticItems:[...e.staticItems,{kind:`turn`,turn:e.liveTurn}],liveTurn:void 0}:e),await new Promise(e=>setTimeout(e,20)),this.instance.unmount()}host(e,t){this.store.update(n=>({...n,staticItems:[...n.staticItems,{kind:`host`,level:e,text:t}]}))}};export{a as InkRenderer};
package/dist/cli.d.ts CHANGED
@@ -1 +1 @@
1
- export { };
1
+ export {}
package/dist/cli.js CHANGED
@@ -1,6 +1,17 @@
1
1
  #!/usr/bin/env node
2
- import{a as e,i as t,n,r,t as i}from"./tools-DTZhd_3g.js";import{Command as a}from"@commander-js/extra-typings";import{Agent as o,Instruct as s,MCP as c,SimpleWriter as l,Tracer as u,addStats as d,anthropic as f,chatCompletions as p,createAgentConfig as m,createStats as h,gemini as g,loadFileContent as _,openai as v}from"@fifthrevision/axle";import{Models as y}from"@fifthrevision/axle/models";import{z as b}from"zod";import{appendFile as ee,mkdir as x,readFile as S}from"node:fs/promises";import{basename as te,dirname as ne,extname as C}from"node:path";import{config as w}from"dotenv";import T from"yaml";import{glob as re}from"glob";import{createInterface as ie}from"node:readline";import{createHash as ae}from"node:crypto";var oe=`0.30.2`;async function se(e,t){let n=[];for(let r of e){let e=new c(r);await e.connect({span:t}),n.push(e)}return n}async function ce(e,t){for(let n of e)try{await n.close({span:t})}catch{}}function le(a,o){switch(a){case`calculator`:return e;case`exec`:{let e=o?.exec;return e&&t.configure(e),t}case`patch-file`:return r;case`read-file`:return n;case`write-file`:return i;default:throw Error(`Unknown tool: ${a}`)}}function ue(e,t){return e.map(e=>le(e,t))}const E={anthropic:y.Anthropic.CLAUDE_HAIKU_4_5,gemini:y.Google.GEMINI_3_5_FLASH,openai:y.OpenAI.GPT_5_4_MINI};function de(e,t){let n=e.config??{};switch(e.type){case`openai`:{let e={...t.openai,...n},r=D(e);if(!r)throw Error(`The provider openai is not configured. Please check your configuration.`);return{provider:v(r,{maxRetries:e.maxRetries,timeoutMs:e.timeoutMs}),model:O(e,E.openai)}}case`anthropic`:{let e={...t.anthropic,...n},r=D(e);if(!r)throw Error(`The provider anthropic is not configured. Please check your configuration.`);return{provider:f(r,{maxRetries:e.maxRetries,timeoutMs:e.timeoutMs}),model:O(e,E.anthropic)}}case`gemini`:{let e={...t.gemini,...n},r=D(e);if(!r)throw Error(`The provider gemini is not configured. Please check your configuration.`);return{provider:g(r,{maxRetries:e.maxRetries,timeoutMs:e.timeoutMs}),model:O(e,E.gemini)}}case`chatcompletions`:{let e={...t.chatcompletions,...n},r=e[`base-url`],i=e.model;if(!r||!i)throw Error(`The provider chatcompletions is not configured. Please check your configuration.`);return{provider:p(r,{apiKey:D(e),maxRetries:e.maxRetries,timeoutMs:e.timeoutMs,vendor:e.vendor}),model:i}}default:throw Error(`Unknown provider type: ${e.type}`)}}function D(e){let t=e.apiKeyEnv??e[`api-key-env`];return typeof t==`string`&&t.length>0?process.env[t]:e[`api-key`]}function O(e,t){return typeof e.model==`string`&&e.model.length>0?e.model:t}function fe(e){let{type:t,...n}=e.provider,r=Object.keys(n).length>0?{type:t,config:n}:{type:t};return{version:1,name:e.name,provider:r,tools:e.tools?.map(e=>({name:e})),providerTools:e.provider_tools?.map(e=>({name:e})),mcps:e.mcps}}async function pe(e,t,n){let r=fe(e),i=r.mcps?.length?await se(r.mcps,n):[];return{agentConfig:await m(r,e=>{let n=de(e.provider,t);return{provider:n.provider,model:n.model,tools:e.tools?.length?ue(e.tools.map(e=>e.name)):void 0,mcps:i.length>0?i:void 0}}),mcps:i}}b.object({value:b.string()}),b.object({timeout:b.number().optional(),maxBuffer:b.number().optional(),cwd:b.string().optional()});const k={"api-key":b.string().optional(),apiKeyEnv:b.string().optional(),"api-key-env":b.string().optional()},A={maxRetries:b.number().int().nonnegative().optional(),timeoutMs:b.number().int().positive().optional()},j=b.strictObject({type:b.literal(`chatcompletions`),"base-url":b.string().optional(),model:b.string().optional(),vendor:b.enum([`openrouter`,`together`]).optional(),...k,...A}),M=b.strictObject({type:b.literal(`anthropic`),model:b.string().optional(),...k,...A}),N=b.strictObject({type:b.literal(`openai`),model:b.string().optional(),...k,...A}),P=b.strictObject({type:b.literal(`gemini`),model:b.string().optional(),...k,...A}),F=b.discriminatedUnion(`type`,[j,M,N,P]),I=b.object({transport:b.literal(`stdio`),name:b.string().optional(),command:b.string(),args:b.array(b.string()).optional(),env:b.record(b.string(),b.string()).optional()}),L=b.object({transport:b.literal(`http`),name:b.string().optional(),url:b.string(),headers:b.record(b.string(),b.string()).optional()}),R=b.discriminatedUnion(`transport`,[I,L]),z=b.object({files:b.string(),resume:b.boolean().default(!1),concurrency:b.number().int().positive().default(3)}),B=b.object({name:b.string().optional(),provider:F,task:b.string(),tools:b.array(b.string()).optional(),provider_tools:b.array(b.string()).optional(),files:b.array(b.string()).optional(),mcps:b.array(R).optional(),batch:z.optional()});async function me(e,t){let{span:n}=t,r=C(e).slice(1);if(r!==`yaml`&&r!==`yml`)throw Error(`Invalid job file format. Expected .yaml or .yml`);let i;try{i=await S(e,{encoding:`utf-8`})}catch{throw Error(`Job File not found, see --help for details`)}let a=T.parse(i);n?.debug(`Job config: `+JSON.stringify(a,null,2));let o=B.safeParse(a);if(!o.success)throw Error(`The job file is not valid:\n${ye(o.error)}`);return o.data.name||(o.data.name=te(e,C(e))),o.data}async function he(e){let{span:t}=e;w({quiet:!0});let n=ge();return t?.debug(`Service config: `+JSON.stringify(V(n),null,2)),n}function ge(){return _e({openai:process.env.OPENAI_API_KEY?{"api-key":process.env.OPENAI_API_KEY,model:process.env.OPENAI_MODEL}:void 0,anthropic:process.env.ANTHROPIC_API_KEY?{"api-key":process.env.ANTHROPIC_API_KEY,model:process.env.ANTHROPIC_MODEL}:void 0,gemini:process.env.GEMINI_API_KEY?{"api-key":process.env.GEMINI_API_KEY,model:process.env.GEMINI_MODEL}:void 0,chatcompletions:process.env.CHATCOMPLETIONS_BASE_URL&&process.env.CHATCOMPLETIONS_MODEL?{"base-url":process.env.CHATCOMPLETIONS_BASE_URL,model:process.env.CHATCOMPLETIONS_MODEL,"api-key":process.env.CHATCOMPLETIONS_API_KEY}:void 0})}function _e(e){return Object.fromEntries(Object.entries(e).filter(([,e])=>e&&Object.keys(e).length>0))}function V(e){return Array.isArray(e)?e.map(e=>V(e)):!e||typeof e!=`object`?e:Object.fromEntries(Object.entries(e).map(([e,t])=>[e,ve(e)&&t?`[redacted]`:V(t)]))}function ve(e){return e===`api-key`||e.toLowerCase().includes(`secret`)}function ye(e){return e.issues.map(e=>` - ${e.path.join(`.`)||`root`}: ${e.message}`).join(`
3
- `)}const H=`.axle/batch.jsonl`;function be(e,t){let n=ae(`sha256`);return n.update(e),n.update(`\0`),n.update(t),n.digest(`hex`)}async function xe(e=H){let t=new Map,n;try{n=await S(e,`utf-8`)}catch{return t}for(let e of n.split(`
4
- `)){let n=e.trim();if(n)try{let e=JSON.parse(n);e.file&&e.hash&&t.set(e.file,e)}catch{}}return t}async function Se(e,t=H){await x(ne(t),{recursive:!0}),await ee(t,JSON.stringify(e)+`
5
- `,`utf-8`)}async function Ce(e,t,n,r,i,a){let c=new s({prompt:e.task});if(e.files)for(let t of e.files)c.addFile(await _(t));let l=a.startSpan(`job`,{type:`workflow`}),u=new o({...t,observability:{trace:l}});try{let e=await u.send(c.withInputs(n)).final;if(d(i,e.usage),e.response){let t=e.response;a.info(t,{markdown:!0})}r.interactive&&await we(u,i,a),l.end()}catch(e){let t=e instanceof Error?e.message:String(e);throw l.error(t),l.end(`error`),e}}async function we(e,t,n){let r=ie({input:process.stdin,output:process.stdout});r.on(`SIGINT`,()=>{r.close()});let i=e=>new Promise(t=>{r.question(e,t),r.once(`close`,()=>t(null))});try{for(;;){let r=await i(`
6
- > `);if(r===null||r.trim()===``)break;try{let i=await e.send(r.trim()).final;if(d(t,i.usage),i.response){let e=i.response;n.info(e,{markdown:!0})}}catch(e){let t=e instanceof Error?e.message:String(e);n.error(t)}}}finally{r.close()}}async function Te(e,t,n,r,i,a,c){let l=await re(t.files);if(l.length===0){c.warn(`No files matched pattern: ${t.files}`);return}c.info(`Batch: ${l.length} file(s) matched "${t.files}"`);let u=t.resume?await xe():new Map,f=e.files?await Promise.all(e.files.map(e=>_(e))):[],p=0,m=0,h=0;await Ee(t.concurrency??3,l,async i=>{let l=c.startSpan(`batch:${i}`,{type:`workflow`});try{let c=await S(i),h=be(e.task,c),g=u.get(i);if(t.resume&&g&&g.hash===h){l.info(`Skipped (already completed)`),l.end(),m++;return}let v=new s({prompt:e.task});for(let e of f)v.addFile(e);v.addFile(await _(i));let y={...r,file:i};d(a,(await new o({...n,observability:{trace:l}}).send(v.withInputs(y)).final).usage),await Se({file:i,hash:h,timestamp:Date.now()}),l.end(),p++}catch(e){let t=e instanceof Error?e.message:String(e);l.error(`Failed: ${t}`),l.end(`error`),h++}}),c.info(`Batch complete: ${p} completed, ${m} skipped, ${h} failed`)}async function Ee(e,t,n){let r=0;async function i(){for(;r<t.length;)await n(t[r++])}let a=Array.from({length:Math.min(e,t.length)},()=>i());await Promise.all(a)}const U=new a().name(`axle`).description(`Axle is a CLI tool for running AI workflows`).version(oe).requiredOption(`-j, --job <path>`,`Path to the YAML job file`).option(`--no-log`,`Do not write the output to a log file`).option(`-d, --debug`,`Print additional debug information`).option(`-i, --interactive`,`Continue the conversation interactively after the initial task`).option(`--args <args...>`,`Additional arguments in the form key=value`);U.parse(process.argv);const W=U.opts(),G={date:new Date().toISOString().split(`T`)[0],datetime:new Date().toISOString(),cwd:process.cwd()};W.args&&W.args.forEach(e=>{let[t,n]=e.split(`=`);t&&n&&(G[t.trim()]=n.trim())});const K=new u;W.debug&&(K.minLevel=`debug`);const De=new l({minLevel:W.debug?`debug`:`info`,showInternal:W.debug,showTimestamp:!0,markdown:!0});if(K.addWriter(De),W.log){let e=new l({minLevel:`debug`,showInternal:!0,showTimestamp:!0,output:e=>{}});K.addWriter(e)}const q=K.startSpan(`cli`,{type:`root`});process.on(`uncaughtException`,async e=>{console.error(`Uncaught exception:`),console.error(e),q.error(`Uncaught exception:`),q.error(e.message),q.error(e.stack||``),q.end(`error`),await K.flush(),process.exit(1)}),W.debug&&(q.debug(`Options: `+JSON.stringify(W,null,2)),q.debug(`Additional Arguments: `+JSON.stringify(G,null,2)));let J,Y;try{J=await he({span:q}),Y=await me(W.job,{span:q})}catch(e){let t=e instanceof Error?e:Error(String(e));q.error(t.message),q.debug(t.stack??``),q.end(`error`),await K.flush(),U.outputHelp(),process.exit(1)}let X=[],Z;try{let e=await pe(Y,J,q);Z=e.agentConfig,X=e.mcps}catch(e){let t=e instanceof Error?e:Error(String(e));q.error(t.message),q.error(t.stack??``),q.end(`error`),await K.flush(),U.outputHelp(),process.exit(1)}if(!Z)throw Error(`Failed to create agent config.`);q.info(`All systems operational. Running job...`);const Q=h(),Oe=performance.now(),$={task:Y.task,files:Y.files};try{Y.batch?await Te($,Y.batch,Z,G,W,Q,q):await Ce($,Z,G,W,Q,q)}catch(e){let t=e instanceof Error?e:Error(String(e));q.error(t.message),q.debug(t.stack??``)}finally{X.length>0&&await ce(X,q)}const ke=performance.now()-Oe;q.info(`Total run time: ${Math.round(ke)}ms`),q.info(`Input tokens: ${Q.in}`),q.info(`Output tokens: ${Q.out}`),Q.cachedIn!==void 0&&q.info(`Cached input tokens: ${Q.cachedIn}`),Q.cacheWriteIn!==void 0&&q.info(`Cache write input tokens: ${Q.cacheWriteIn}`),Q.reasoningOut!==void 0&&q.info(`Reasoning output tokens: ${Q.reasoningOut}`),q.info(`Complete. Goodbye`),q.end(),await K.flush();export{};
2
+ import{a as e,i as t,n,r,t as i}from"./tools-gv3b9sAQ.js";import{n as a,o,r as s,t as c}from"./format-CVXITc-j.js";import{Command as l}from"@commander-js/extra-typings";import{Agent as u,AxleAgentAbortError as ee,Instruct as d,MCP as f,PromptCompactor as p,SimpleWriter as m,Tracer as h,Transcript as g,addStats as te,anthropic as ne,chatCompletions as _,createAgentConfig as v,createStats as y,gemini as b,loadFileContent as re,openai as x}from"@fifthrevision/axle";import{mkdirSync as S,openSync as C,writeSync as ie}from"node:fs";import{dirname as ae,extname as oe,join as w,relative as se,resolve as ce}from"node:path";import{z as T}from"zod";import{appendFile as le,chmod as ue,mkdir as E,readFile as D,readdir as de,rename as fe,rm as pe,stat as me,writeFile as he}from"node:fs/promises";import{config as ge,parse as _e}from"dotenv";import ve from"yaml";import{homedir as ye}from"node:os";import*as O from"@clack/prompts";import{ModelInfo as k}from"@fifthrevision/axle/models";import{glob as be}from"glob";import{createHash as xe}from"node:crypto";import{createInterface as Se}from"node:readline";var Ce=`0.31.0`;async function we(e,t){let n=[];for(let r of e){let e=new f(r);await e.connect({span:t}),n.push(e)}return n}async function Te(e,t){for(let n of e)try{await n.close({span:t})}catch{}}function Ee(a){switch(a){case`calculator`:return e;case`exec`:return t;case`patch-file`:return r;case`read-file`:return n;case`write-file`:return i;default:throw Error(`Unknown tool: ${a}`)}}function De(e){return e.map(e=>Ee(e))}const Oe=[`anthropic`,`openai`,`gemini`,`chatcompletions`];function ke(e,t,n){let r=e?.provider,i,a;if(r&&!(`name`in r))i=r,a=r.type;else{let e=r&&`name`in r?r.name:t.defaults?.provider;if(!e)throw Error(`No provider specified and no default provider configured. Add provider: to the job, or set defaults.provider in ~/.axle/cli.yaml.`);a=e;let n=t.providers?.[e];if(n)i=n;else if(Oe.includes(e))i={type:e};else throw Error(`Provider "${e}" is not a provider profile in cli.yaml or a built-in provider type.`)}let o=e?.model??t.defaults?.models?.[a]??n[i.type]?.model,{type:s,...c}=i;return{provider:Object.keys(c).length>0?{type:s,config:c}:{type:s},model:o,providerName:a}}function Ae(e,t,n){let r=e.config??{},i=e.type,a={...t[i],...r},o=n??a.model;if(!o)throw Error(`No model resolved for provider ${i}. Add model: to the job, set defaults.models in ~/.axle/cli.yaml, or set ${i.toUpperCase()}_MODEL.`);switch(i){case`openai`:{let e=A(a);if(!e)throw Error(`The provider openai is not configured. Please check your configuration.`);return{provider:x(e,{maxRetries:a.maxRetries,timeoutMs:a.timeoutMs}),model:o}}case`anthropic`:{let e=A(a);if(!e)throw Error(`The provider anthropic is not configured. Please check your configuration.`);return{provider:ne(e,{maxRetries:a.maxRetries,timeoutMs:a.timeoutMs}),model:o}}case`gemini`:{let e=A(a);if(!e)throw Error(`The provider gemini is not configured. Please check your configuration.`);return{provider:b(e,{maxRetries:a.maxRetries,timeoutMs:a.timeoutMs}),model:o}}case`chatcompletions`:{let e=a.baseUrl;if(!e)throw Error(`The provider chatcompletions is not configured. Please check your configuration.`);return{provider:_(e,{apiKey:A(a),maxRetries:a.maxRetries,timeoutMs:a.timeoutMs,vendor:a.vendor}),model:o}}default:throw Error(`Unknown provider type: ${i}`)}}function A(e){let t=e.apiKeyEnv;return typeof t==`string`&&t.length>0?process.env[t]:e.apiKey}function je(e,t){let n=ke(void 0,e,t);return{version:1,provider:n.provider,model:n.model}}function Me(e,t,n){let r=ke(e,t,n);return{version:1,name:e.name,provider:r.provider,model:r.model,system:e.system,request:e.request,tools:e.tools?.map(e=>({name:e})),providerTools:e.providerTools?.map(e=>({name:e})),mcps:e.mcps}}async function Ne(e,t,n){let r=e.mcps?.length?await we(e.mcps,n):[];return{agentConfig:await v(e,e=>{let n=Ae(e.provider,t,e.model);return{provider:n.provider,model:n.model,tools:e.tools?.length?De(e.tools.map(e=>e.name)):void 0,mcps:r.length>0?r:void 0}}),definition:e,mcps:r}}const Pe=`credentials`,Fe=`cli.yaml`;function j(e){return{project:w(e?.cwd??process.cwd(),`.axle`),user:w(e?.home??ye(),`.axle`)}}const M={apiKey:T.string().optional(),apiKeyEnv:T.string().optional()},N={maxRetries:T.number().int().nonnegative().optional(),timeoutMs:T.number().int().positive().optional()},Ie=T.strictObject({type:T.literal(`chatcompletions`),baseUrl:T.string().optional(),vendor:T.enum([`openrouter`,`together`]).optional(),...M,...N}),Le=T.strictObject({type:T.literal(`anthropic`),...M,...N}),Re=T.strictObject({type:T.literal(`openai`),...M,...N}),ze=T.strictObject({type:T.literal(`gemini`),...M,...N}),Be=T.discriminatedUnion(`type`,[Ie,Le,Re,ze]),Ve=T.union([T.string().min(1).transform(e=>({name:e})),Be]),He=T.object({providers:T.record(T.string(),Be).optional(),defaults:T.object({provider:T.string().optional(),models:T.record(T.string(),T.string()).optional()}).optional()}),Ue=T.object({transport:T.literal(`stdio`),name:T.string().optional(),command:T.string(),args:T.array(T.string()).optional(),env:T.record(T.string(),T.string()).optional()}),We=T.object({transport:T.literal(`http`),name:T.string().optional(),url:T.string(),headers:T.record(T.string(),T.string()).optional()}),Ge=T.discriminatedUnion(`transport`,[Ue,We]),Ke=T.strictObject({files:T.string(),concurrency:T.number().int().positive().default(3),incremental:T.boolean().default(!1)}),qe=T.strictObject({reasoning:T.union([T.enum([`default`,`off`,`on`]),T.strictObject({effort:T.enum([`low`,`medium`,`high`])})]).optional(),maxOutputTokens:T.number().int().positive().optional(),temperature:T.number().optional(),topP:T.number().optional(),stop:T.union([T.string(),T.array(T.string())]).optional(),toolChoice:T.union([T.enum([`auto`,`none`,`required`]),T.strictObject({type:T.literal(`tool`),name:T.string()})]).optional(),parallelToolCalls:T.boolean().optional(),providerOptions:T.record(T.string(),T.any()).optional()}),Je=T.strictObject({name:T.string().optional(),provider:Ve.optional(),model:T.string().optional(),system:T.string().optional(),request:qe.optional(),task:T.string(),tools:T.array(T.string()).optional(),providerTools:T.array(T.string()).optional(),files:T.array(T.string()).optional(),mcps:T.array(Ge).optional(),batch:Ke.optional(),compaction:T.boolean().optional()});async function Ye(e,t){let{span:n}=t,r=oe(e).slice(1);if(r!==`yaml`&&r!==`yml`)throw Error(`Invalid job file format. Expected .yaml or .yml`);let i;try{i=await D(e,{encoding:`utf-8`})}catch{throw Error(`Job File not found, see --help for details`)}let a=ve.parse(i);n?.debug(`Job config: `+JSON.stringify(a,null,2));let o=Je.safeParse(a);if(!o.success)throw Error(`The job file is not valid:\n${rt(o.error)}`);return o.data}async function P(e){let{span:t}=e;ge({quiet:!0});let n=j(e),r=[process.env];for(let e of[n.project,n.user]){let t=await $e(w(e,Pe));t&&r.push(t)}let i=et(e=>{for(let t of r)if(t[e])return t[e]});return t?.debug(`Service config: `+JSON.stringify(F(i),null,2)),i}async function Xe(e){let{span:t}=e,n=j(e),r={};for(let e of[n.user,n.project]){let t=w(e,Fe),n=await Qe(t);if(n===null)continue;let i;try{i=ve.parse(n)??{}}catch(e){let n=e instanceof Error?e.message:String(e);throw Error(`Invalid config file at ${t}:\n ${n}`)}let a=He.safeParse(i);if(!a.success)throw Error(`Invalid config file at ${t}:\n${rt(a.error)}`);r=Ze(r,a.data)}return t?.debug(`CLI config: `+JSON.stringify(r,null,2)),r}function Ze(e,t){let n={},r={...e.providers,...t.providers};if(Object.keys(r).length>0&&(n.providers=r),e.defaults||t.defaults){n.defaults={...e.defaults,...t.defaults};let r={...e.defaults?.models,...t.defaults?.models};Object.keys(r).length>0&&(n.defaults.models=r)}return n}async function Qe(e){try{return await D(e,{encoding:`utf-8`})}catch(e){if(e.code===`ENOENT`)return null;throw e}}async function $e(e){let t=await Qe(e);return t===null?null:_e(t)}function et(e){return tt({openai:e(`OPENAI_API_KEY`)?{apiKey:e(`OPENAI_API_KEY`),model:e(`OPENAI_MODEL`)}:void 0,anthropic:e(`ANTHROPIC_API_KEY`)?{apiKey:e(`ANTHROPIC_API_KEY`),model:e(`ANTHROPIC_MODEL`)}:void 0,gemini:e(`GEMINI_API_KEY`)?{apiKey:e(`GEMINI_API_KEY`),model:e(`GEMINI_MODEL`)}:void 0,chatcompletions:e(`CHATCOMPLETIONS_BASE_URL`)?{baseUrl:e(`CHATCOMPLETIONS_BASE_URL`),model:e(`CHATCOMPLETIONS_MODEL`),apiKey:e(`CHATCOMPLETIONS_API_KEY`)}:void 0})}function tt(e){return Object.fromEntries(Object.entries(e).filter(([,e])=>e&&Object.keys(e).length>0))}function F(e){return Array.isArray(e)?e.map(e=>F(e)):!e||typeof e!=`object`?e:Object.fromEntries(Object.entries(e).map(([e,t])=>[e,nt(e)&&t?`[redacted]`:F(t)]))}function nt(e){return e===`apiKey`||e.toLowerCase().includes(`secret`)}function rt(e){return e.issues.flatMap(e=>it(e,e.path)).map(({path:e,message:t})=>` - ${e.join(`.`)||`root`}: ${t}`).join(`
3
+ `)}function it(e,t){if(e.code===`invalid_union`){let n=e.errors.flatMap(e=>e.flatMap(e=>it(e,[...t,...e.path])));if(!n.length)return[{path:t,message:e.message}];let r=n.filter(e=>e.path.length>t.length);return r.length?r:n}return[{path:t,message:e.message}]}async function I(e,t,n){let r=`${e}.tmp`;try{await he(r,t,n?.mode===void 0?{}:{mode:n.mode}),n?.mode!==void 0&&await ue(r,n.mode),await fe(r,e)}catch(e){throw await pe(r,{force:!0}).catch(()=>{}),e}}function L(e){return w(j({home:e}).user,`sessions`,`cli`)}function at(e,t){return w(L(t),`${e}.json`)}async function ot(e,t){let n;try{n=await de(L(t))}catch{return e}let r=n.filter(e=>e.endsWith(`.json`)).map(e=>e.slice(0,-5));if(r.includes(e))return e;let i=r.filter(t=>t.startsWith(e));if(i.length===1)return i[0];if(i.length>1)throw Error(`Session id prefix "${e}" is ambiguous (${i.length} matches). Use more characters.`);return e}async function st(e){let t=L(e),n;try{n=await de(t)}catch(e){if(e.code===`ENOENT`)return[];throw e}let r=await Promise.all(n.filter(e=>e.endsWith(`.json`)).map(async e=>{let n=w(t,e),r=e.slice(0,-5),i=await me(n),a={sessionId:r,path:n,sizeBytes:i.size,updatedAt:i.mtime.toISOString()};try{let e=JSON.parse(await D(n,`utf-8`));return{...a,updatedAt:e.updatedAt??a.updatedAt,corrupt:!1}}catch{return{...a,corrupt:!0}}}));return r.sort((e,t)=>t.updatedAt.localeCompare(e.updatedAt)),r}async function ct(e,t){let n=at(await ot(e,t),t),r;try{r=await D(n,`utf-8`)}catch(t){throw t.code===`ENOENT`?Error(`No session found with id ${e}`):t}let i;try{i=JSON.parse(r)}catch{throw Error(`Invalid session file at ${n}`)}let a=i;if(a?.version!==1||!a.definition||!a.session)throw Error(`Unsupported or corrupt session file at ${n}`);return a}var R=class{definition;cwd;home;compaction;createdAt;constructor(e,t){this.definition=e,this.cwd=t?.cwd??process.cwd(),this.home=t?.home,this.compaction=t?.compaction,this.createdAt=t?.createdAt}async save(e,t){this.createdAt??=new Date().toISOString();let n={version:1,createdAt:this.createdAt,updatedAt:new Date().toISOString(),cwd:this.cwd,definition:this.definition,compaction:this.compaction,session:e,turns:[...t]};await E(L(this.home),{recursive:!0});let r=at(e.sessionId,this.home);return await I(r,JSON.stringify(n,null,2)),r}};async function lt(e,t){let n=j({home:t}).user,r=w(n,Pe),i=``;try{i=await D(r,`utf-8`)}catch(e){if(e.code!==`ENOENT`)throw e}let a=i.length>0?i.split(`
4
+ `):[];a.at(-1)===``&&a.pop();let o=new Map(Object.entries(e)),s=new Set,c=[];for(let e of a){let t=e.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=/)?.[1];t&&o.has(t)?(c.push(`${t}=${o.get(t)}`),o.delete(t),s.add(t)):t&&s.has(t)||c.push(e)}for(let[e,t]of o)c.push(`${e}=${t}`);return await E(n,{recursive:!0}),await I(r,c.join(`
5
+ `)+`
6
+ `,{mode:384}),r}async function ut(e,t){let n=j({home:t}).user,r=w(n,Fe),i=``;try{i=await D(r,`utf-8`)}catch(e){if(e.code!==`ENOENT`)throw e}let a=ve.parseDocument(i);e.provider!==void 0&&a.setIn([`defaults`,`provider`],e.provider);for(let[t,n]of Object.entries(e.models??{}))a.setIn([`defaults`,`models`,t],n);return await E(n,{recursive:!0}),await I(r,a.toString()),r}const z=[{value:`anthropic`,label:`Anthropic`,keyName:`ANTHROPIC_API_KEY`,publisher:`anthropic/`},{value:`openai`,label:`OpenAI`,keyName:`OPENAI_API_KEY`,publisher:`openai/`},{value:`gemini`,label:`Google Gemini`,keyName:`GEMINI_API_KEY`,publisher:`google/`},{value:`chatcompletions`,label:`OpenAI-compatible endpoint (Ollama, OpenRouter, …)`,keyName:`CHATCOMPLETIONS_API_KEY`}];function dt(e){return!!(e.anthropic?.apiKey??e.openai?.apiKey??e.gemini?.apiKey??e.chatcompletions?.baseUrl)}function ft(e,t,n){return!(dt(e)||Object.keys(t.providers??{}).length>0||t.defaults?.provider||n?.provider&&!(`name`in n.provider))}function B(e){return O.isCancel(e)&&(O.cancel(`Setup cancelled.`),process.exit(1)),e}async function pt(e){let t=z.find(t=>t.value===e)?.publisher;if(t){let e=Object.keys(k).filter(e=>e.startsWith(t)).sort();if(e.length>0){let t=`__other__`,n=B(await O.select({message:`Pick a model`,options:[...e.map(e=>{let t=k[e];return{value:e,label:e,hint:t.contextWindow?`${Math.round(t.contextWindow/1e3)}k context`:void 0}}),{value:t,label:`Other (type a model id)`}]}));if(n!==t)return n}}return B(await O.text({message:`Model id`,placeholder:t?`${t}model-name`:`e.g. qwen/qwen-3-coder`,validate:e=>(e??``).trim().length===0?`A model id is required`:void 0})).trim()}async function mt(){return B(await O.text({message:`Input files (glob or path)`,placeholder:`data/*.md`,validate:e=>(e??``).trim().length===0?`Inputs are required`:void 0})).trim()}async function ht(e,t){let n=t?.saveAs??e;O.log.warn(`No model configured for provider ${n}.`);let r=await pt(e);if(t?.offerSave!==!1&&B(await O.confirm({message:`Save ${r} as the default model for ${n}?`,initialValue:!0}))){let e=await ut({models:{[n]:r}});O.log.success(`Saved to ${e}`)}return r}async function gt(e){O.intro(`axle setup`);let t=B(await O.select({message:`Which provider should axle use by default?`,options:z.map(e=>({value:e.value,label:e.label}))})),n=z.find(e=>e.value===t),r={};t===`chatcompletions`&&(r.CHATCOMPLETIONS_BASE_URL=B(await O.text({message:`Base URL of the endpoint`,placeholder:`http://localhost:11434/v1`,initialValue:e.chatcompletions?.baseUrl??``,validate:e=>(e??``).trim().length===0?`A base URL is required`:void 0})).trim());let i=e[t]?.apiKey!==void 0,a=!0;if(i&&(a=B(await O.confirm({message:`A key for ${n.label} is already configured replace it?`,initialValue:!1}))),a){let e=B(await O.password({message:t===`chatcompletions`?`API key (leave empty if the endpoint needs none)`:`API key for ${n.label}`,validate:e=>t!==`chatcompletions`&&(e??``).trim().length===0?`An API key is required`:void 0})).trim();e.length>0&&(r[n.keyName]=e)}if(Object.keys(r).length>0){let e=await lt(r);O.log.success(`Credentials written to ${e}`)}let o=await pt(t),s=await ut({provider:t,models:{[t]:o}});O.log.success(`Defaults saved to ${s}`),O.outro(`Ready — run axle to chat with ${o}. Re-run anytime with: axle setup`)}function _t(e){let t={};for(let n of e){let e=n.indexOf(`=`);if(e<1)continue;let r=n.slice(0,e).trim(),i=n.slice(e+1).trim();r&&i&&(t[r]=i)}return t}async function vt(e){let{invocation:t,cliConfig:n,serviceConfig:r,jobConfig:i,jobScope:a,variables:o,interactiveTerminal:s}=e,c;if(t.kind===`resume`){let e=await ct(t.id);c={kind:`session`,definition:e.definition,spec:{spanName:`resume`,session:e.session,priorTurns:e.turns,resumedFromCwd:e.cwd,initial:t.message,interactive:t.message===void 0,compaction:e.compaction},sessionStore:new R(e.definition,{cwd:e.cwd,createdAt:e.createdAt,compaction:e.compaction})}}else if(i){let e=Me(i,n,r);if(t.kind===`batch`||i.batch){if(t.kind===`kernel`&&t.interactive)throw Error(`A batch run cannot be combined with --interactive.`);let n;if(t.kind===`batch`&&t.inputs.length>0)n=t.inputs;else if(i.batch)n=[i.batch.files];else if(s)n=[await mt()];else throw Error(`batch needs inputs: pass globs/paths after the recipe, or add a batch: block to it.`);let r=t.kind===`batch`&&t.verbose||(i.batch?.concurrency??3)===1;c={kind:`batch`,definition:e,spec:{task:i.task,files:i.files,inputs:n,concurrency:r?1:i.batch?.concurrency??3,jobName:a,incremental:(t.kind===`batch`?t.incremental:void 0)??i.batch?.incremental??!1,verbose:r,compaction:i.compaction}}}else{let n=new d({prompt:i.task});for(let e of i.files??[])n.addFile(await re(e));c={kind:`session`,definition:e,spec:{spanName:`job`,initial:n.withInputs(o),interactive:t.kind===`kernel`&&t.interactive,compaction:i.compaction},sessionStore:new R(e,{compaction:i.compaction})}}}else{let e=je(n,r);c={kind:`session`,definition:e,spec:{spanName:`chat`,initial:t.kind===`kernel`?t.message:void 0,interactive:t.kind!==`kernel`||t.message===void 0},sessionStore:new R(e)}}if(!c.definition.model&&s){let e=c.kind!==`session`||c.spec.spanName!==`resume`;c.definition.model=await ht(c.definition.provider.type,{offerSave:e,saveAs:e?ke(i,n,r).providerName:void 0})}return c}const yt=`.axle/batch.jsonl`;function bt(e){return xe(`sha256`).update(e).digest(`hex`)}function xt(e,t){return`${e}\u0000${t}`}async function St(e=yt){let t=new Map,n;try{n=await D(e,`utf-8`)}catch{return t}for(let e of n.split(`
7
+ `)){let n=e.trim();if(n)try{let e=JSON.parse(n);e.job&&e.file&&e.hash&&e.sessionId&&e.status&&t.set(xt(e.job,e.file),e)}catch{}}return t}async function Ct(e,t=yt){await E(ae(t),{recursive:!0}),await le(t,JSON.stringify(e)+`
8
+ `,`utf-8`)}function wt(e){switch(e.kind){case`model`:return`Model error: ${e.message}`;case`tool`:return`Tool error (${e.error.name}): ${e.message}`;case`parse`:return`Parse error: ${e.message}`}}function Tt(e,t){let n=Number(process.env.AXLE_CONTEXT_WINDOW);if(Number.isInteger(n)&&n>0)return n;if(t)return t;let r=e.provider.name.toLowerCase()===`gemini`?`google`:e.provider.name.toLowerCase();return k[e.model]?.contextWindow??k[`${r}/${e.model}`]?.contextWindow??2e5}const Et=[`You summarize an agent conversation so it can continue in a smaller context.`,`Preserve durable facts, decisions, constraints, file paths, tool outcomes,`,`completed work, and open tasks. Prefer concrete identifiers over prose.`].join(` `);function Dt(e){let t=Tt(e),n=new p({provider:e.provider,model:e.model,prompt:Et,thresholdTokens:Math.floor(t*.8),summaryWords:1e3,reasoning:e.requestOptions.reasoning});return{compact:n.compact,shouldCompactOnTrigger:n.shouldCompactOnTrigger,triggers:{beforeTurn:!0}}}var Ot=class{agent;transcript;sessionStore;span;persisted;constructor(e){this.agent=new u({...e.agentConfig,observability:{trace:e.span}},e.session),e.compaction!==!1&&this.agent.setCompaction(Dt(this.agent)),this.transcript=new g(e.priorTurns??[]),this.sessionStore=e.sessionStore,this.span=e.span,this.persisted=!!e.session,this.agent.on(t=>{this.transcript.apply(t),e.onEvent(t,this.transcript)})}async save(){if(!this.sessionStore)return!1;try{return await this.sessionStore.save(await this.agent.snapshot(),this.transcript.turns),this.persisted=!0,!0}catch(e){let t=e instanceof Error?e.message:String(e);return this.span.warn(`Failed to save session: ${t}`),!1}}resumeCommand(e=!1){if(this.persisted)return`axle resume ${e?this.agent.sessionId.slice(0,8):this.agent.sessionId}`}};async function kt(e,t,n,r,i){let a=n.startSpan(e.spanName,{type:`workflow`}),o=new Ot({agentConfig:e.agentConfig,span:a,compaction:e.compaction,session:e.session,priorTurns:e.priorTurns,sessionStore:i,onEvent:(e,t)=>r.onEvent(e,t)}),{agent:s}=o,c=new AbortController,l=0,u=()=>{l+=1,l===1&&s.stop()?r.warn(`Finishing the current step — Ctrl-C again to cancel now`):c.abort()};process.on(`SIGINT`,u),r.setInterruptHandler(u);let d=!e.session,f=async()=>{!i||!d||await o.save()&&(d=!1)};if(e.session){let t=`Resuming session ${s.sessionId}`;if(r.info(t),n.info(t),e.resumedFromCwd&&e.resumedFromCwd!==process.cwd()){let t=`Session was started in ${e.resumedFromCwd}; resuming from ${process.cwd()}`;r.warn(t),n.warn(t)}}else if(i){let e=`Session ${s.sessionId}`;r.info(e),n.info(e)}e.priorTurns?.length&&r.renderPriorTurns(e.priorTurns);let p=()=>{let e=s.context(),n=Tt(s,e.limit);r.updateUsage({in:t.in,out:t.out,contextTokens:e.total,contextLimit:n})};p(),s.on(e=>{e.type===`compaction:complete`&&p()});let m=async e=>{d=!0;try{let i=await s.send(e,{signal:c.signal}).final;if(te(t,i.usage),p(),!i.ok){let e=wt(i.error);return r.error(e),n.error(e),a.error(e),!1}return n.info(i.response,{markdown:!0}),!0}finally{l=0}};try{if(e.initial!==void 0){let t=await m(e.initial);if(await f(),!t)return a.end(`error`),!1}if(e.interactive)for(;;){let e=await r.promptInput();if(e===null)break;let t=e.trim();if(t!==``){if(t===`/quit`)break;try{await m(t)}catch(e){if(e instanceof ee){r.warn(`Interrupted`),n.warn(`Interrupted`);break}let t=e instanceof Error?e.message:String(e);r.error(t),n.error(t)}await f()}}return a.end(),!0}catch(e){if(e instanceof ee)return r.warn(`Interrupted`),n.warn(`Interrupted`),a.end(`cancelled`),!1;let t=e instanceof Error?e.message:String(e);throw a.error(t),a.end(`error`),e}finally{process.removeListener(`SIGINT`,u),r.setInterruptHandler(void 0),await f();let e=o.resumeCommand();if(i&&e){let t=`Resume this session:\n${e}`;r.info(t),n.info(t)}}}async function At(e,t,n,r,i,a){let o=await Promise.all(e.inputs.map(e=>be(e))),s=[...new Set(o.flat())].sort();if(s.length===0){let t=`No files matched: ${e.inputs.join(` `)}`;return i.warn(t),r.warn(t),!0}let l=`Batch: ${s.length} input(s) matched "${e.inputs.join(` `)}"`;i.info(l),r.info(l);let u=e.incremental?await St():new Map,f=e.files?await Promise.all(e.files.map(e=>re(e))):[],p=0,m=0,h=0,g=0,ne=0,_=()=>({total:s.length,completed:p,skipped:m,failed:h,tokensIn:g,tokensOut:ne});a?.batchStarted(_());let v=new AbortController,y=()=>{i.warn(`Cancelling batch…`),v.abort()};process.on(`SIGINT`,y),i.setInterruptHandler(y);try{await jt(e.concurrency,s,async o=>{if(v.signal.aborted)return;let s=r.startSpan(`batch:${o}`,{type:`workflow`}),l;try{l=bt(await D(o))}catch(e){let t=e instanceof Error?e.message:String(e);i.error(`${o}: failed — ${t}`),s.error(`Failed: ${t}`),s.end(`error`),h++,a?.itemFinished(o,_());return}let y=u.get(xt(e.jobName,o));if(e.incremental&&y?.status===`completed`&&y.hash===l){i.info(`${o}: unchanged — skipped`),s.info(`Skipped (already completed)`),s.end(),m++,a?.itemFinished(o,_());return}let b=new R(e.definition,{home:e.home,compaction:e.compaction});a?.itemStarted(o);let x=new Ot({agentConfig:e.agentConfig,span:s,compaction:e.compaction,sessionStore:b,onEvent:(t,n)=>{if(e.verbose)i.onEvent(t,n);else if(a&&t.type===`part:start`){let e=t.part,n=e.type===`action`?c(e.detail.name):e.type===`thinking`?`Thinking`:e.type===`text`?`Writing`:void 0;n&&a.itemPhase(o,n)}}}),{agent:S}=x,C=async(t,n)=>{await Ct({job:e.jobName,file:o,hash:t,sessionId:S.sessionId,status:`failed`,timestamp:Date.now()});let r=x.resumeCommand(!0);i.error(`${o}: failed — ${n}${r?` (${r})`:``}`),s.error(`Failed: ${n}`),s.end(`error`),h++,a?.itemFinished(o,_())};try{let r=new d({prompt:e.task});for(let e of f)r.addFile(e);r.addFile(await re(o));let c=await S.send(r.withInputs({...t,file:o}),{signal:v.signal}).final;if(te(n,c.usage),g+=c.usage.in,ne+=c.usage.out,await x.save(),!c.ok){await C(l,wt(c.error));return}await Ct({job:e.jobName,file:o,hash:l,sessionId:S.sessionId,status:`completed`,timestamp:Date.now()});let u=x.resumeCommand(!0);i.success(`${o}: done${u?` (${u})`:``}`),s.end(),p++,a?.itemFinished(o,_())}catch(e){if(await x.save(),e instanceof ee){s.end(`cancelled`),h++,a?.itemFinished(o,_());return}await C(l,e instanceof Error?e.message:String(e))}})}finally{process.removeListener(`SIGINT`,y),i.setInterruptHandler(void 0)}let b=v.signal.aborted,x=`Batch complete: ${p} completed, ${m} skipped, ${h} failed${b?` (cancelled)`:``}`;return i.info(x),r.info(x),h===0&&!b}async function jt(e,t,n){let r=0;async function i(){for(;r<t.length;)await n(t[r++])}let a=Array.from({length:Math.min(e,t.length)},()=>i());await Promise.all(a)}const Mt=[{label:`Older than 24 hours`,ms:864e5},{label:`Older than 7 days`,ms:6048e5},{label:`Older than 30 days`,ms:2592e6},{label:`Everything`,ms:0}];async function Nt(e){O.intro(`axle cleanup`);let t=await st(e);if(t.length===0){O.outro(`No saved sessions.`);return}let n=Date.now(),r=(e,t)=>{if(t===0||e.corrupt)return!0;let r=Date.parse(e.updatedAt);return!Number.isFinite(r)||n-r>t},i=Mt.map(e=>{let n=t.filter(t=>r(t,e.ms)),i=n.reduce((e,t)=>e+t.sizeBytes,0);return{window:e,matched:n,label:`${e.label} (${n.length} session${n.length===1?``:`s`} · ${Pt(i)})`}}).filter(e=>e.matched.length>0);if(i.length===0){O.outro(`Nothing old enough to clean up (${t.length} recent sessions).`);return}let a=`__cancel__`,o=await O.select({message:`What should be cleaned up?`,options:[...i.map((e,t)=>({value:String(t),label:e.label})),{value:a,label:`Nothing, cancel`}]});if(O.isCancel(o)||o===a){O.outro(`Nothing deleted.`);return}let s=i[Number(o)],c=await O.confirm({message:`Delete ${s.matched.length} session(s)? This cannot be undone.`,initialValue:!1});if(O.isCancel(c)||!c){O.outro(`Nothing deleted.`);return}for(let e of s.matched)await pe(e.path,{force:!0});O.outro(`Deleted ${s.matched.length} session(s).`)}function Pt(e){return e>=1048576?`${(e/1048576).toFixed(1)} MB`:e>=1024?`${(e/1024).toFixed(1)} kB`:`${e} B`}var Ft=class{active;ended=!1;prompt(){if(this.ended||process.stdin.readableEnded)return Promise.resolve(null);let e=Se({input:process.stdin,output:process.stdout});return this.active=e,e.on(`SIGINT`,()=>e.close()),new Promise(t=>{let n=()=>{this.ended=!0,this.active=void 0,t(null)};e.once(`close`,n),e.question(`
9
+ > `,r=>{e.removeListener(`close`,n),e.close(),this.active=void 0,t(r)})})}close(){this.active?.close(),this.active=void 0}},It=class{write;atLineStart=!0;readline=new Ft;constructor(e){this.write=e?.write??(e=>process.stdout.write(e))}promptInput(){return this.endLine(),this.atLineStart=!0,this.readline.prompt()}renderPriorTurns(e){for(let t of e)for(let e of t.parts)this.renderStaticPart(t.owner,e)}onEvent(e,t){switch(e.type){case`text:delta`:this.emit(e.delta);break;case`part:end`:{let n=Lt(t,e.turnId,e.partId);n?.type===`thinking`?this.renderStaticPart(`agent`,n):this.endLine();break}case`action:complete`:case`action:error`:{let n=Lt(t,e.turnId,e.partId);n?.type===`action`&&this.renderStaticPart(`agent`,n);break}case`compaction:complete`:case`compaction:error`:{let n=Lt(t,e.turnId,e.partId);n?.type===`compaction`&&this.renderStaticPart(`agent`,n);break}case`error`:this.line(`✖ ${e.error.message}`)}}info(e){this.line(`ℹ ${o(e)}`)}success(e){this.line(`✔ ${o(e)}`)}warn(e){this.line(`⚠ ${o(e)}`)}error(e){this.line(`✖ ${o(e)}`)}updateUsage(e){}setInterruptHandler(e){}close(){this.readline.close(),this.endLine()}renderStaticPart(e,t){switch(t.type){case`text`:{if(!t.text.trim())return;let n=t.text.trim();this.line(e===`user`?`❯ ${o(n)}`:n);return}case`thinking`:{let e=s(t.timing);this.line(`✔ Thinking${e?` (${e})`:``}`);return}case`action`:{let e=t.status===`error`?`✖`:t.status===`cancelled`?`⚠`:`✔`,n=a(t),r=t.status===`complete`?s(t.timing):void 0;this.line(`${e} ${c(t.detail.name)}${n?` ${n}`:``}${r?` (${r})`:``}${t.status===`cancelled`?` (cancelled)`:``}`);return}case`compaction`:{if(t.status===`error`){this.line(`✖ Compaction failed: ${t.error}`);return}let e=s(t.timing);this.line(`✔ Compacted context${e?` (${e})`:``}`);return}default:return}}emit(e){e.length!==0&&(this.write(e),this.atLineStart=e.endsWith(`
10
+ `))}endLine(){this.atLineStart||this.emit(`
11
+ `)}line(e){this.endLine(),this.emit(e+`
12
+ `)}};function Lt(e,t,n){return e.getTurn(t)?.parts.find(e=>e.id===n)}function Rt(e){return typeof e==`object`&&!!e&&`itemStarted`in e}async function zt(e,t){if(e===`ink`&&process.stdout.isTTY&&process.stdin.isTTY){if(t?.batchProgress){let{InkBatchRenderer:e}=await import(`./InkBatchRenderer-DPV54ovW.js`);return new e}let{InkRenderer:e}=await import(`./InkRenderer-C6KY_Kfj.js`);return new e({statusBar:t?.statusBar})}return new It}const V=new l().name(`axle`).description(`Axle is a CLI tool for running AI workflows`).version(Ce).enablePositionalOptions().helpCommand(!0);function Bt(e){let t=e.renderer??`ink`;return t!==`plain`&&t!==`ink`&&V.error(`error: unknown renderer "${t}" (expected plain or ink)`),{renderer:t,log:e.log,debug:!!e.debug}}let H;V.option(`-j, --job <path>`,`Run a YAML job file instead of starting a chat`).option(`-m, --message <text>`,`Send one message and exit`).option(`-i, --interactive`,`With --job: continue the conversation interactively after the task`).option(`--args <args...>`,`Template variables in the form key=value`).option(`--renderer <mode>`,`Screen renderer: ink or plain (pipes always get plain)`,`ink`).option(`--no-log`,`Do not write the output to a log file`).option(`-d, --debug`,`Print additional debug information`).addHelpText(`after`,`
13
+ Run a session (default):
14
+ axle Interactive chat from configured defaults
15
+ axle -m "..." One-shot message
16
+ axle -j <recipe> Run a job file (batch if the recipe has a batch block)`).action(e=>{e.job&&e.message!==void 0&&V.error(`error: --message cannot be combined with --job`),H={kind:`kernel`,job:e.job,message:e.message,interactive:!!e.interactive,args:e.args??[],common:Bt(e)}}),V.command(`batch`).description(`Run a recipe once per input, one isolated session each`).argument(`[inputs...]`,`Globs or paths; defaults to the recipe's batch block, else prompts`).requiredOption(`-j, --job <path>`,`Recipe to run`).option(`--incremental`,`Skip completed inputs whose content is unchanged`).option(`--no-incremental`,`Run every input even if the recipe sets incremental`).option(`--verbose`,`Stream full item transcripts instead of progress rows (concurrency 1)`).option(`--args <args...>`,`Template variables in the form key=value`).option(`--renderer <mode>`,`Screen renderer: ink or plain (pipes always get plain)`,`ink`).option(`--no-log`,`Do not write the output to a log file`).option(`-d, --debug`,`Print additional debug information`).action((e,t)=>{H={kind:`batch`,job:t.job,inputs:e,incremental:t.incremental,verbose:!!t.verbose,args:t.args??[],common:Bt(t)}}),V.command(`resume`).description(`Re-enter a saved session`).argument(`<id>`,`Session id (unique prefixes accepted)`).option(`-m, --message <text>`,`Send one message and exit`).option(`--renderer <mode>`,`Screen renderer: ink or plain (pipes always get plain)`,`ink`).option(`--no-log`,`Do not write the output to a log file`).option(`-d, --debug`,`Print additional debug information`).action((e,t)=>{H={kind:`resume`,id:e,message:t.message,common:Bt(t)}}),V.command(`setup`).description(`Configure providers, credentials, and defaults`).action(async()=>{await gt(await P({})),process.exit(0)}),V.command(`cleanup`).description(`Delete saved sessions by age window`).action(async()=>{await Nt(),process.exit(0)}),await V.parseAsync(process.argv),H||process.exit(0);const U=H,W=U.common,G={date:new Date().toISOString().split(`T`)[0],datetime:new Date().toISOString(),cwd:process.cwd()};`args`in U&&Object.assign(G,_t(U.args));const K=new h;if(W.debug&&(K.minLevel=`debug`),W.debug){let e=new m({minLevel:`debug`,showInternal:!0,showTimestamp:!0,markdown:!0});K.addWriter(e)}if(W.log){let e=w(j().user,`logs`,`cli`);S(e,{recursive:!0});let t=w(e,`${new Date().toISOString().replace(/:/g,`-`)}.log`),n=C(t,`a`),r=new m({minLevel:`debug`,showInternal:!0,showTimestamp:!0,output:e=>ie(n,e+`
17
+ `)});K.addWriter(r)}const q=K.startSpan(`cli`,{type:`root`});let J;async function Vt(){try{await J?.close()}finally{await K.flush()}}process.on(`uncaughtException`,async e=>{console.error(`Uncaught exception:`),console.error(e),q.error(`Uncaught exception:`),q.error(e.message),q.error(e.stack||``),q.end(`error`),await Vt(),process.exit(1)}),W.debug&&(q.debug(`Invocation: `+JSON.stringify(U,null,2)),q.debug(`Additional Arguments: `+JSON.stringify(G,null,2)));async function Y(e){let t=e instanceof Error?e:Error(String(e));(J??console).error(t.message),q.error(t.message),q.debug(t.stack??``),q.end(`error`),await Vt(),J||V.outputHelp(),process.exit(1)}let Ht=await Xe({span:q}).catch(Y),X=await P({span:q}).catch(Y);const Z=U.kind!==`resume`&&U.job?await Ye(U.job,{span:q}).catch(Y):void 0,Ut=U.kind!==`resume`&&U.job&&Z?Z.name??se(process.cwd(),ce(U.job)):`job`,Wt=!!(process.stdin.isTTY&&process.stdout.isTTY);U.kind!==`resume`&&Wt&&ft(X,Ht,Z)&&(await gt(X),Ht=await Xe({span:q}),X=await P({span:q}));const Q=await vt({invocation:U,cliConfig:Ht,serviceConfig:X,jobConfig:Z,jobScope:Ut,variables:G,interactiveTerminal:Wt}).catch(Y),$=await zt(W.renderer,{batchProgress:Q.kind===`batch`&&!Q.spec.verbose,statusBar:Q.kind===`session`&&Q.spec.interactive});J=$;const Gt=()=>process.exit(130);$.setInterruptHandler(Gt),Q.definition.mcps?.length&&$.info(`Connecting MCP servers…`);const{mcps:Kt,agentConfig:qt}=await Ne(Q.definition,X,q).catch(Y);try{q.info(`All systems operational. Running job...`);let e=y(),t=performance.now(),n=!1;try{n=Q.kind===`batch`?await At({...Q.spec,definition:Q.definition,agentConfig:qt},G,e,q,$,Rt($)?$:void 0):await kt({...Q.spec,agentConfig:qt},e,q,$,Q.sessionStore)}catch(e){let t=e instanceof Error?e:Error(String(e));$.error(t.message),q.error(t.message),q.debug(t.stack??``)}finally{$.setInterruptHandler(Gt),Kt.length>0&&await Te(Kt,q)}let r=performance.now()-t;q.info(`Total run time: ${Math.round(r)}ms`),q.info(`Input tokens: ${e.in}`),q.info(`Output tokens: ${e.out}`),e.cachedIn!==void 0&&q.info(`Cached input tokens: ${e.cachedIn}`),e.cacheWriteIn!==void 0&&q.info(`Cache write input tokens: ${e.cacheWriteIn}`),e.reasoningOut!==void 0&&q.info(`Reasoning output tokens: ${e.reasoningOut}`);let i=`in ${(r/1e3).toFixed(1)}s · ↑ ${e.in} ↓ ${e.out} tokens`;n?($.success(`Done ${i}`),q.info(`Complete. Goodbye`),q.end()):($.error(`Failed ${i}`),q.error(`Job failed`),q.end(`error`),process.exitCode=1)}finally{await Vt()}export{};
@@ -0,0 +1,3 @@
1
+ function e(e){return e.charAt(0).toUpperCase()+e.slice(1)}function t(e){if(!e?.end)return;let t=Date.parse(e.end)-Date.parse(e.start);if(!(!Number.isFinite(t)||t<0))return n(t)}function n(e){return e<1e3?`${Math.round(e)}ms`:`${(e/1e3).toFixed(1)}s`}function r(e){return e>=1e6?`${(e/1e6).toFixed(1)}M`:e>=1e3?`${(e/1e3).toFixed(1)}k`:String(e)}function i(e,t){return e.length>t?e.slice(0,t-1)+`…`:e}function a(e){return e.split(`
2
+ `).join(`
3
+ `)}function o(e){if(e.kind!==`tool`)return;let t=e.detail.parameters??{};if(Object.keys(t).length!==0)return i(JSON.stringify(t),80)}export{r as a,n as i,o as n,a as o,t as r,i as s,e as t};
@@ -0,0 +1,5 @@
1
+ import{a as e,n as t,o as n,r,s as i,t as a}from"./format-CVXITc-j.js";import{Box as o,Static as s,Text as c,useInput as l}from"ink";import{useEffect as u,useState as d,useSyncExternalStore as f}from"react";import{Fragment as p,jsx as m,jsxs as h}from"react/jsx-runtime";const g=[`⠋`,`⠙`,`⠹`,`⠸`,`⠼`,`⠴`,`⠦`,`⠧`,`⠇`,`⠏`];function _({store:e,onSubmit:t,statusBar:n}){let r=f(e.subscribe,e.getSnapshot);return h(p,{children:[m(s,{items:r.staticItems,children:(e,t)=>m(C,{item:e},t)}),r.liveTurn&&m(w,{turn:r.liveTurn}),r.queuedInputs.map((e,t)=>h(c,{dimColor:!0,children:[`❯ `,e,` (queued)`]},t)),!r.closed&&m(b,{onSubmit:t,awaitingInput:r.awaitingInput,onInterrupt:r.onInterrupt}),n&&!r.closed&&r.usage&&m(v,{usage:r.usage})]})}function v({usage:t}){let n=t.contextLimit?`${y(t.contextTokens/t.contextLimit)} ~${e(t.contextTokens)}tok`:`ctx ~${e(t.contextTokens)}`;return h(c,{dimColor:!0,children:[` `,`↑ `,e(t.in),` ↓ `,e(t.out),` · `,n]})}function y(e){let t=Math.min(8,Math.round(e*8));return`█`.repeat(t)+`░`.repeat(8-t)}function b({onSubmit:e,awaitingInput:t,onInterrupt:r}){let[i,a]=d(``),s=t=>{a(``),e(t)};return l((n,o)=>{if(o.ctrl&&n===`c`){t?e(null):r?.();return}if(o.ctrl&&n===`d`){t&&i===``&&e(null);return}if(o.return){if(o.shift||o.meta){a(e=>e+`
2
+ `);return}s(i);return}if(o.backspace||o.delete){a(e=>e.slice(0,-1));return}if(o.ctrl&&n===`u`){a(``);return}n&&!o.ctrl&&!o.meta&&a(e=>e+n.replace(/\r\n?/g,`
3
+ `))}),!t&&i===``?null:m(o,{marginTop:1,children:h(c,{children:[m(c,{color:`cyan`,children:`❯ `}),n(i),m(c,{inverse:!0,children:` `})]})})}const x={info:{glyph:`ℹ`,color:`cyan`},success:{glyph:`✔`,color:`green`},warn:{glyph:`⚠`,color:`yellow`},error:{glyph:`✖`,color:`red`}};function S({level:e,text:t}){let r=x[e];return h(c,{children:[m(c,{color:r.color,children:r.glyph}),` `,n(t)]})}function C({item:e}){return e.kind===`host`?m(S,{level:e.level,text:e.text}):m(E,{turn:e.turn})}function w({turn:e}){let t=T();return e.parts.length===0?m(c,{color:`cyan`,children:t}):m(E,{turn:e,live:!0,spinnerFrame:t})}function T(){let[e,t]=d(0);return u(()=>{let e=setInterval(()=>t(e=>e+1),80);return()=>clearInterval(e)},[]),g[e%g.length]}function E({turn:e,live:t,spinnerFrame:n}){return h(o,{flexDirection:`column`,children:[e.parts.map((r,i)=>m(D,{part:r,owner:e.owner,live:t,spinner:i===e.parts.length-1?n:void 0},r.id)),e.error&&h(c,{color:`red`,children:[`✖ `,e.error.message]})]})}function D({part:e,owner:t,live:i,spinner:a}){switch(e.type){case`text`:{if(!e.text)return a?m(c,{color:`cyan`,children:a}):null;let r=i?A(e.text,6):e.text.trim();return t===`user`?h(c,{children:[`❯ `,n(r)]}):m(c,{children:r})}case`thinking`:{if(a)return h(c,{children:[m(c,{color:`cyan`,children:a}),` `,m(c,{dimColor:!0,children:`Thinking…`})]});let t=r(e.timing),n=e.summary?.trim();return h(c,{dimColor:!0,children:[`✔ Thinking`,t?` (${t})`:``,n?` — ${n}`:``]})}case`action`:return m(O,{part:e,live:i,spinner:a});case`citation`:return m(o,{flexDirection:`column`,children:e.citations.map((e,t)=>{let n=e.source;return h(c,{dimColor:!0,children:[`※ `,n.title??n.url??n.uri??e.source.type]},t)})});case`file`:return h(c,{dimColor:!0,children:[`▣ `,e.file.name,` (`,e.file.mimeType,`)`]});case`compaction`:{if(e.status===`error`)return h(c,{color:`red`,children:[`✖ Compaction failed: `,e.error]});if(e.status===`running`){let t=e.progress===void 0?``:` ${Math.round(e.progress*100)}%`;return h(c,{children:[m(c,{color:`cyan`,children:a??g[0]}),` `,h(c,{dimColor:!0,children:[`Compacting…`,t]})]})}let t=r(e.timing);return h(c,{children:[m(c,{color:`green`,children:`✔`}),` Compacted context`,t&&h(c,{dimColor:!0,children:[` (`,t,`)`]})]})}}}function O({part:e,live:n,spinner:i}){let s=e.status===`pending`||e.status===`running`,l=s?i??`⠋`:e.status===`complete`?`✔`:e.status===`error`?`✖`:`⚠`,u=s?`cyan`:e.status===`complete`?`green`:e.status===`error`?`red`:`yellow`,d=t(e),f=s||e.status===`cancelled`?void 0:r(e.timing);return h(o,{flexDirection:`column`,children:[h(c,{children:[m(c,{color:u,children:l}),` `,a(e.detail.name),d&&h(c,{dimColor:!0,children:[` `,d]}),f&&h(c,{dimColor:!0,children:[` (`,f,`)`]}),e.status===`cancelled`&&m(c,{dimColor:!0,children:` (cancelled)`})]}),m(k,{result:e.detail.result}),e.kind===`agent`&&e.detail.children.length>0&&m(o,{flexDirection:`column`,paddingLeft:2,children:e.detail.children.map(e=>m(E,{turn:e,live:n},e.id))})]})}function k({result:e}){if(!e)return null;if(e.type===`error`&&e.error)return h(c,{color:`red`,children:[` `,i(e.error.message,200)]});let t=typeof e.content==`string`?e.content:void 0;return t?.trim()?h(c,{dimColor:!0,children:[` `,i(j(t),200)]}):null}function A(e,t){let n=e.trimEnd(),r=n.length;for(let e=0;e<t;e++){let e=n.lastIndexOf(`
4
+ `,r-1);if(e===-1)return n;r=e}return n.slice(r+1)}function j(e){return e.trim().split(`
5
+ `,1)[0]}var M=class{state;listeners=new Set;constructor(e){this.state=e}getSnapshot=()=>this.state;subscribe=e=>(this.listeners.add(e),()=>{this.listeners.delete(e)});update(e){this.state=e(this.state);for(let e of this.listeners)e()}};function N(e,t){let n=[],r;for(let i of e)t.has(i.id)||(i.status===`streaming`?r=i:(n.push({kind:`turn`,turn:i}),t.add(i.id)));return{newlyFinished:n,liveTurn:r}}export{T as a,S as i,N as n,_ as r,M as t};
@@ -1,17 +1,6 @@
1
1
  import { ExecutableTool, ProviderTool, ToolContext, ToolDefinition } from "@fifthrevision/axle";
2
2
  import * as z$1 from "zod";
3
3
  import { z } from "zod";
4
-
5
- //#region src/tools/types.d.ts
6
- interface ExecProviderConfig {
7
- timeout?: number;
8
- maxBuffer?: number;
9
- cwd?: string;
10
- }
11
- interface ToolProviderConfig {
12
- exec?: ExecProviderConfig;
13
- }
14
- //#endregion
15
4
  //#region src/tools/calculator.d.ts
16
5
  declare const calculatorSchema: z.ZodObject<{
17
6
  operation: z.ZodEnum<{
@@ -38,8 +27,11 @@ declare class ExecTool implements ExecutableTool<typeof execSchema> {
38
27
  private timeout;
39
28
  private maxBuffer;
40
29
  private cwd?;
41
- constructor(config?: ExecProviderConfig);
42
- configure(config: ExecProviderConfig): void;
30
+ constructor(options?: {
31
+ timeout?: number;
32
+ maxBuffer?: number;
33
+ cwd?: string;
34
+ });
43
35
  summarize(params: z$1.infer<typeof execSchema>): string;
44
36
  execute(params: z$1.infer<typeof execSchema>, ctx: ToolContext): Promise<string>;
45
37
  }
@@ -68,4 +60,4 @@ declare const writeFileSchema: z.ZodObject<{
68
60
  }, z.core.$strip>;
69
61
  declare const writeFileTool: ExecutableTool<typeof writeFileSchema>;
70
62
  //#endregion
71
- export { type ExecProviderConfig, type ExecutableTool, type ProviderTool, type ToolContext, type ToolDefinition, type ToolProviderConfig, calculatorTool, execTool, patchFileTool, readFileTool, writeFileTool };
63
+ export { type ExecutableTool, type ProviderTool, type ToolContext, type ToolDefinition, calculatorTool, execTool, patchFileTool, readFileTool, writeFileTool };
@@ -1 +1 @@
1
- import{a as e,i as t,n,r,t as i}from"../tools-DTZhd_3g.js";export{e as calculatorTool,t as execTool,r as patchFileTool,n as readFileTool,i as writeFileTool};
1
+ import{a as e,i as t,n,r,t as i}from"../tools-gv3b9sAQ.js";export{e as calculatorTool,t as execTool,r as patchFileTool,n as readFileTool,i as writeFileTool};
@@ -0,0 +1,5 @@
1
+ import{dirname as e}from"node:path";import*as t from"zod";import{z as n}from"zod";import{spawn as r}from"node:child_process";import{mkdir as i,readFile as a,writeFile as o}from"node:fs/promises";const s={name:`calculator`,description:`Performs basic arithmetic operations`,schema:n.object({operation:n.enum([`add`,`subtract`,`multiply`,`divide`]).describe(`The operation to perform (add, subtract, multiply, divide)`),a:n.number().describe(`First operand`),b:n.number().describe(`Second operand`)}),execute:async({operation:e,a:t,b:n})=>{switch(e){case`add`:return`${t} + ${n} = ${t+n}`;case`subtract`:return`${t} - ${n} = ${t-n}`;case`multiply`:return`${t} * ${n} = ${t*n}`;case`divide`:if(n===0)throw Error(`Cannot divide by zero`);return`${t} / ${n} = ${t/n}`;default:throw Error(`Unknown operation: ${e}`)}}};async function c(e,t={}){let n=t.timeout??3e4,i=t.maxBuffer??1048576;return new Promise((a,o)=>{let s=r(e,[],{shell:!0,cwd:t.cwd}),c=``,l=``,u=!1,d=!1,f=!1,p=setTimeout(()=>{u=!0,s.kill(`SIGTERM`)},n),m=()=>{d=!0,s.kill(`SIGTERM`)};t.signal?.addEventListener(`abort`,m);let h=()=>{clearTimeout(p),t.signal?.removeEventListener(`abort`,m)};s.stdout?.setEncoding(`utf-8`),s.stderr?.setEncoding(`utf-8`),s.stdout?.on(`data`,e=>{if(c+=e,c.length+l.length>i){f=!0,s.kill(`SIGTERM`);return}t.onChunk?.(e)}),s.stderr?.on(`data`,e=>{if(l+=e,c.length+l.length>i){f=!0,s.kill(`SIGTERM`);return}t.onChunk?.(e)}),s.on(`error`,e=>{h(),o(e)}),s.on(`close`,e=>{if(h(),u){let e=Error(`Command timed out after ${n}ms`);e.stdout=c,e.stderr=l,o(e);return}if(d){let e=Error(`Command aborted`);e.stdout=c,e.stderr=l,o(e);return}if(f){let e=Error(`Command output exceeded maxBuffer (${i} bytes)`);e.stdout=c,e.stderr=l,o(e);return}if(e!==0){let t=Error(`Command failed with exit code ${e}`);t.stdout=c,t.stderr=l,t.code=e??-1,o(t);return}a({stdout:c,stderr:l})})})}function l(e){if(e instanceof Error){let t=e,n=`Error executing command: ${e.message}`;return t.stdout&&(n+=`\n[stdout]: ${t.stdout}`),t.stderr&&(n+=`\n[stderr]: ${t.stderr}`),n}return`Error executing command: ${String(e)}`}function u(e,t){return t&&t.trim()?`${e}\n[stderr]: ${t}`:e}const d=t.object({command:t.string().describe(`The shell command to execute`)}),f=new class{name=`exec`;description=`Execute a shell command and return the output.`;schema=d;timeout;maxBuffer;cwd;constructor(e){this.timeout=e?.timeout??3e4,this.maxBuffer=e?.maxBuffer??1048576,this.cwd=e?.cwd}summarize(e){return e.command}async execute(e,t){let{command:n}=e;try{let e=await c(n,{timeout:this.timeout,maxBuffer:this.maxBuffer,cwd:this.cwd,signal:t.signal,onChunk:e=>t.emit(e)});return u(e.stdout,e.stderr)}catch(e){return l(e)}}},p={name:`patch-file`,description:`Patch a file by replacing an exact string match within a specified line range`,schema:n.object({path:n.string().describe(`The file path to patch`),old_string:n.string().describe(`The exact text to find and replace`),new_string:n.string().describe(`The replacement text`),start_line:n.number().int().positive().describe(`1-indexed start line of the region to match within`),end_line:n.number().int().positive().describe(`1-indexed end line (inclusive) of the region to match within`)}),summarize:({path:e,start_line:t,end_line:n})=>`${e}:${t}:${n}`,execute:async({path:e,old_string:t,new_string:n,start_line:r,end_line:i})=>{if(i<r)throw Error(`end_line (${i}) must be >= start_line (${r})`);let s;try{s=await a(e,`utf-8`)}catch(t){throw t instanceof Error?Error(`Failed to read file "${e}": ${t.message}`):t}let c=s.split(`
2
+ `);if(r>c.length)throw Error(`start_line (${r}) exceeds file length (${c.length} lines)`);if(i>c.length)throw Error(`end_line (${i}) exceeds file length (${c.length} lines)`);let l=c.slice(r-1,i).join(`
3
+ `),u=l.indexOf(t);if(u===-1)throw Error(`old_string not found within lines ${r}-${i} of "${e}"`);if(l.indexOf(t,u+1)!==-1)throw Error(`old_string matches multiple times within lines ${r}-${i} of "${e}"`);let d=l.replace(t,n),f=[...c.slice(0,r-1),...d.split(`
4
+ `),...c.slice(i)].join(`
5
+ `);try{await o(e,f,`utf-8`)}catch(t){throw t instanceof Error?Error(`Failed to write file "${e}": ${t.message}`):t}return`Successfully patched "${e}" (lines ${r}-${i})`}},m={name:`read-file`,description:`Read the contents of a file from disk`,schema:n.object({path:n.string().describe(`The file path to read from`)}),summarize:({path:e})=>e,execute:async({path:e})=>{try{return await a(e,`utf-8`)}catch(t){throw t instanceof Error?Error(`Failed to read file "${e}": ${t.message}`):t}}},h={name:`write-file`,description:`Write content to a file on disk, creating directories if needed`,schema:n.object({path:n.string().describe(`The file path to write to`),content:n.string().describe(`The content to write to the file`)}),summarize:({path:e})=>e,execute:async({path:t,content:n})=>{try{return await i(e(t),{recursive:!0}),await o(t,n,`utf-8`),`Successfully wrote ${n.length} characters to "${t}"`}catch(e){throw e instanceof Error?Error(`Failed to write file "${t}": ${e.message}`):e}}};export{s as a,f as i,m as n,p as r,h as t};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fifthrevision/axle-cli",
3
- "version": "0.30.2",
3
+ "version": "0.31.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/johncch/axle.git"
@@ -14,10 +14,6 @@
14
14
  "./tools": {
15
15
  "types": "./dist/tools/index.d.ts",
16
16
  "import": "./dist/tools/index.js"
17
- },
18
- "./store": {
19
- "types": "./dist/store/index.d.ts",
20
- "import": "./dist/store/index.js"
21
17
  }
22
18
  },
23
19
  "files": [
@@ -26,23 +22,28 @@
26
22
  "author": "Chong Han Chua",
27
23
  "license": "ISC",
28
24
  "dependencies": {
29
- "@commander-js/extra-typings": "^14.0.0",
30
- "commander": "^14.0.3",
25
+ "@clack/prompts": "^1.7.0",
26
+ "@commander-js/extra-typings": "^15.0.0",
27
+ "commander": "^15.0.0",
31
28
  "dotenv": "^17.4.2",
32
29
  "glob": "^13.0.6",
30
+ "ink": "^7.1.1",
31
+ "react": "^19.2.8",
33
32
  "yaml": "^2.9.0",
34
33
  "zod": "^4.4.3",
35
- "@fifthrevision/axle": "0.30.2"
34
+ "@fifthrevision/axle": "0.31.0"
36
35
  },
37
36
  "devDependencies": {
38
- "@arethetypeswrong/core": "^0.18.2",
39
- "publint": "^0.3.21",
40
- "tsdown": "^0.22.0",
41
- "tsx": "^4.22.3",
37
+ "@arethetypeswrong/core": "^0.18.5",
38
+ "@types/react": "^19.2.18",
39
+ "publint": "^0.3.24",
40
+ "tsdown": "^0.22.14",
41
+ "tsx": "^4.23.12",
42
42
  "typescript": "^6.0.3"
43
43
  },
44
44
  "scripts": {
45
45
  "start": "tsx ./src/cli.ts",
46
+ "generate:job-schema": "tsx ./scripts/generate-job-schema.ts",
46
47
  "build": "tsdown --clean --minify",
47
48
  "build:watch": "tsdown --watch",
48
49
  "build-dev": "tsdown"
@@ -1,11 +0,0 @@
1
- import { FileStore } from "@fifthrevision/axle";
2
-
3
- //#region src/store/LocalFileStore.d.ts
4
- declare class LocalFileStore implements FileStore {
5
- readonly rootPath: string;
6
- constructor(rootPath: string);
7
- read(path: string): Promise<string | null>;
8
- write(path: string, content: string): Promise<void>;
9
- }
10
- //#endregion
11
- export { LocalFileStore };
@@ -1 +0,0 @@
1
- import e from"node:fs/promises";import t from"node:path";var n=class{rootPath;constructor(e){this.rootPath=e}async read(n){let r=t.join(this.rootPath,n);try{return await e.readFile(r,`utf-8`)}catch{return null}}async write(n,r){let i=t.join(this.rootPath,n);await e.mkdir(t.dirname(i),{recursive:!0}),await e.writeFile(i,r,`utf-8`)}};export{n as LocalFileStore};
@@ -1,5 +0,0 @@
1
- import*as e from"zod";import{z as t}from"zod";import{spawn as n}from"node:child_process";import{mkdir as r,readFile as i,writeFile as a}from"node:fs/promises";import{dirname as o}from"node:path";const s={name:`calculator`,description:`Performs basic arithmetic operations`,schema:t.object({operation:t.enum([`add`,`subtract`,`multiply`,`divide`]).describe(`The operation to perform (add, subtract, multiply, divide)`),a:t.number().describe(`First operand`),b:t.number().describe(`Second operand`)}),execute:async({operation:e,a:t,b:n})=>{switch(e){case`add`:return`${t} + ${n} = ${t+n}`;case`subtract`:return`${t} - ${n} = ${t-n}`;case`multiply`:return`${t} * ${n} = ${t*n}`;case`divide`:if(n===0)throw Error(`Cannot divide by zero`);return`${t} / ${n} = ${t/n}`;default:throw Error(`Unknown operation: ${e}`)}}};async function c(e,t={}){let r=t.timeout??3e4,i=t.maxBuffer??1048576;return new Promise((a,o)=>{let s=n(e,[],{shell:!0,cwd:t.cwd}),c=``,l=``,u=!1,d=!1,f=!1,p=setTimeout(()=>{u=!0,s.kill(`SIGTERM`)},r),m=()=>{d=!0,s.kill(`SIGTERM`)};t.signal?.addEventListener(`abort`,m);let h=()=>{clearTimeout(p),t.signal?.removeEventListener(`abort`,m)};s.stdout?.setEncoding(`utf-8`),s.stderr?.setEncoding(`utf-8`),s.stdout?.on(`data`,e=>{if(c+=e,c.length+l.length>i){f=!0,s.kill(`SIGTERM`);return}t.onChunk?.(e)}),s.stderr?.on(`data`,e=>{if(l+=e,c.length+l.length>i){f=!0,s.kill(`SIGTERM`);return}t.onChunk?.(e)}),s.on(`error`,e=>{h(),o(e)}),s.on(`close`,e=>{if(h(),u){let e=Error(`Command timed out after ${r}ms`);e.stdout=c,e.stderr=l,o(e);return}if(d){let e=Error(`Command aborted`);e.stdout=c,e.stderr=l,o(e);return}if(f){let e=Error(`Command output exceeded maxBuffer (${i} bytes)`);e.stdout=c,e.stderr=l,o(e);return}if(e!==0){let t=Error(`Command failed with exit code ${e}`);t.stdout=c,t.stderr=l,t.code=e??-1,o(t);return}a({stdout:c,stderr:l})})})}function l(e){if(e instanceof Error){let t=e,n=`Error executing command: ${e.message}`;return t.stdout&&(n+=`\n[stdout]: ${t.stdout}`),t.stderr&&(n+=`\n[stderr]: ${t.stderr}`),n}return`Error executing command: ${String(e)}`}function u(e,t){return t&&t.trim()?`${e}\n[stderr]: ${t}`:e}const d=e.object({command:e.string().describe(`The shell command to execute`)}),f=new class{name=`exec`;description=`Execute a shell command and return the output.`;schema=d;timeout=3e4;maxBuffer=1024*1024;cwd;constructor(e){e&&this.configure(e)}configure(e){this.timeout=e.timeout??3e4,this.maxBuffer=e.maxBuffer??1024*1024,this.cwd=e.cwd}summarize(e){return e.command}async execute(e,t){let{command:n}=e;try{let e=await c(n,{timeout:this.timeout,maxBuffer:this.maxBuffer,cwd:this.cwd,signal:t.signal,onChunk:e=>t.emit(e)});return u(e.stdout,e.stderr)}catch(e){return l(e)}}},p={name:`patch-file`,description:`Patch a file by replacing an exact string match within a specified line range`,schema:t.object({path:t.string().describe(`The file path to patch`),old_string:t.string().describe(`The exact text to find and replace`),new_string:t.string().describe(`The replacement text`),start_line:t.number().int().positive().describe(`1-indexed start line of the region to match within`),end_line:t.number().int().positive().describe(`1-indexed end line (inclusive) of the region to match within`)}),summarize:({path:e,start_line:t,end_line:n})=>`${e}:${t}:${n}`,execute:async({path:e,old_string:t,new_string:n,start_line:r,end_line:o})=>{if(o<r)throw Error(`end_line (${o}) must be >= start_line (${r})`);let s;try{s=await i(e,`utf-8`)}catch(t){throw t instanceof Error?Error(`Failed to read file "${e}": ${t.message}`):t}let c=s.split(`
2
- `);if(r>c.length)throw Error(`start_line (${r}) exceeds file length (${c.length} lines)`);if(o>c.length)throw Error(`end_line (${o}) exceeds file length (${c.length} lines)`);let l=c.slice(r-1,o).join(`
3
- `),u=l.indexOf(t);if(u===-1)throw Error(`old_string not found within lines ${r}-${o} of "${e}"`);if(l.indexOf(t,u+1)!==-1)throw Error(`old_string matches multiple times within lines ${r}-${o} of "${e}"`);let d=l.replace(t,n),f=[...c.slice(0,r-1),...d.split(`
4
- `),...c.slice(o)].join(`
5
- `);try{await a(e,f,`utf-8`)}catch(t){throw t instanceof Error?Error(`Failed to write file "${e}": ${t.message}`):t}return`Successfully patched "${e}" (lines ${r}-${o})`}},m={name:`read-file`,description:`Read the contents of a file from disk`,schema:t.object({path:t.string().describe(`The file path to read from`)}),summarize:({path:e})=>e,execute:async({path:e})=>{try{return await i(e,`utf-8`)}catch(t){throw t instanceof Error?Error(`Failed to read file "${e}": ${t.message}`):t}}},h={name:`write-file`,description:`Write content to a file on disk, creating directories if needed`,schema:t.object({path:t.string().describe(`The file path to write to`),content:t.string().describe(`The content to write to the file`)}),summarize:({path:e})=>e,execute:async({path:e,content:t})=>{try{return await r(o(e),{recursive:!0}),await a(e,t,`utf-8`),`Successfully wrote ${t.length} characters to "${e}"`}catch(t){throw t instanceof Error?Error(`Failed to write file "${e}": ${t.message}`):t}}};export{s as a,f as i,m as n,p as r,h as t};