@swifty.js/swifty 0.0.1 → 0.0.2-alpha

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,302 @@
1
+ # Swifty
2
+
3
+ Swifty is a terminal-based AI coding agent. It provides an interactive TUI (terminal user interface) for conversing with large language models, executing code, manipulating files, and orchestrating multi-agent workflows, all from the command line.
4
+
5
+ ## Overview
6
+
7
+ Swifty runs as a single CLI binary that connects to configurable LLM providers (Anthropic, OpenAI, or any OpenAI-compatible endpoint). It renders a rich terminal interface using React and Ink, giving you streaming responses, tool execution feedback, permission prompts, and slash commands in a single pane.
8
+
9
+ Beyond interactive use, Swifty supports a non-interactive print mode for scripting, a remote mode that serves a browser-based chat UI over WebSocket, and a teammate mode that lets one lead agent coordinate multiple subagents working in parallel.
10
+
11
+ ## Features
12
+
13
+ ### Core Capabilities
14
+
15
+ - Multi-provider LLM support with Anthropic, OpenAI, and OpenAI-compatible protocols
16
+ - Interactive terminal UI with streaming text, thinking indicators, and tool execution display
17
+ - Built-in tool set: ReadFile, WriteFile, EditFile, Bash, Glob, Grep, ToolSearch, EnterWorktree, ExitWorktree, ExitPlanMode
18
+ - MCP (Model Context Protocol) server integration for extending the tool set with external services
19
+ - Permission system with four modes: default, acceptEdits, plan (read-only), and bypassPermissions
20
+ - Sandbox support via bwrap (Linux) and seatbelt (macOS) for isolated command execution
21
+ - Dangerous command pattern detection with human-in-the-loop approval dialogs
22
+
23
+ ### Conversation and Memory
24
+
25
+ - Session persistence with JSONL-based storage for cross-session resume
26
+ - Automatic context compaction when conversations approach the model's context window
27
+ - Long-term memory extraction and recall across sessions
28
+ - Instructions file support for persistent project-level guidance
29
+
30
+ ### Skills and Commands
31
+
32
+ - Skill catalog with three-tier loading: built-in, user-global (~/.swifty/skills/), and project-level (.swifty/skills/)
33
+ - Hot-reload support for skills edited on disk
34
+ - Inline and fork execution modes for skills
35
+ - Slash command system with built-in commands and user-defined commands from .swifty/commands/
36
+ - Skill installation from URLs
37
+
38
+ ### Agent Orchestration
39
+
40
+ - Subagent spawning with built-in agent types: general-purpose, plan (read-only architect), explore (read-only code explorer)
41
+ - Team coordination with file-based mailboxes and lead/member communication
42
+ - Coordinator mode for managing multi-agent workflows
43
+ - Git worktree isolation for parallel agent tasks
44
+
45
+ ### Hooks
46
+
47
+ - Event-driven hook engine supporting: session_start, session_end, turn_start, turn_end, pre_send, post_receive, pre_tool_use, post_tool_use, shutdown
48
+ - Hook actions: shell commands, HTTP requests, prompt injection
49
+ - Conditional execution, reject-on-failure, and async options
50
+
51
+ ### Remote Mode
52
+
53
+ - Koa HTTP server with WebSocket bridge for browser-based access
54
+ - React frontend served at a configurable address
55
+ - Bidirectional message streaming between browser and agent
56
+
57
+ ## Installation
58
+
59
+ ```bash
60
+ npm install -g @swifty.js/swifty
61
+ ```
62
+
63
+ Or run directly from the monorepo:
64
+
65
+ ```bash
66
+ pnpm dev
67
+ ```
68
+
69
+ ## Configuration
70
+
71
+ Swifty reads YAML configuration files from multiple locations, merged in order:
72
+
73
+ 1. ~/.swifty/config.yml or ~/.swifty/config.yaml
74
+ 2. .swifty/config.yml or .swifty/config.yaml (project root)
75
+ 3. .swifty/config.local.yml or .swifty/config.local.yaml (project root, gitignored)
76
+
77
+ At least one provider must be configured. Example config.yml:
78
+
79
+ ```yaml
80
+ providers:
81
+ - name: anthropic
82
+ protocol: anthropic
83
+ base_url: https://api.anthropic.com
84
+ model: claude-sonnet-4-20250514
85
+ # api_key defaults to $ANTHROPIC_API_KEY
86
+
87
+ permission_mode: default
88
+
89
+ mcp_servers:
90
+ - name: my-server
91
+ command: npx
92
+ args: ["-y", "my-mcp-server"]
93
+
94
+ hooks:
95
+ - event: pre_tool_use
96
+ condition: "Bash"
97
+ action:
98
+ type: command
99
+ command: "echo tool about to run"
100
+
101
+ sandbox:
102
+ enabled: false
103
+ auto_allow: false
104
+ network_enabled: true
105
+
106
+ enable_coordinator_mode: false
107
+ ```
108
+
109
+ Provider fields:
110
+
111
+ | Field | Required | Description |
112
+ | ----------------- | -------- | -------------------------------------------------------------------- |
113
+ | name | yes | Display name for the provider |
114
+ | protocol | yes | One of: anthropic, openai, openai-compat |
115
+ | base_url | yes | API base URL |
116
+ | model | yes | Model identifier |
117
+ | api_key | no | API key (falls back to environment variable) |
118
+ | thinking | no | Enable extended thinking mode (increases max_output_tokens to 64000) |
119
+ | context_window | no | Override auto-detected context window size |
120
+ | max_output_tokens | no | Override default max output tokens |
121
+
122
+ API keys are resolved in this order: explicit api_key field, then environment variables (ANTHROPIC_API_KEY for anthropic, OPENAI_API_KEY for openai and openai-compat).
123
+
124
+ ## Usage
125
+
126
+ ### Interactive TUI Mode
127
+
128
+ ```bash
129
+ swifty
130
+ ```
131
+
132
+ Launches the terminal interface. If multiple providers are configured, a provider selection screen appears first.
133
+
134
+ ### Print Mode (Non-Interactive)
135
+
136
+ ```bash
137
+ swifty -p "explain this codebase"
138
+ swifty -p "fix the failing test" --output-format stream-json
139
+ ```
140
+
141
+ The -p flag sends a single prompt, runs the agent loop, and prints the result to stdout. Useful for scripting and CI pipelines.
142
+
143
+ ### Remote Mode (Browser UI)
144
+
145
+ ```bash
146
+ swifty --remote # listens on :18888
147
+ swifty --remote :9000 # custom address
148
+ ```
149
+
150
+ Starts a Koa HTTP server and WebSocket bridge. The bundled React frontend is served at the configured address for browser-based interaction.
151
+
152
+ ### Slash Commands
153
+
154
+ Inside the TUI, these commands are available:
155
+
156
+ | Command | Description |
157
+ | ----------------------- | ---------------------------------------------------------------------------------- |
158
+ | /status | Show current session status (model, tokens, tools, sandbox, memories, skills, MCP) |
159
+ | /permission mode <mode> | Change permission mode (default, acceptEdits, plan, bypassPermissions) |
160
+ | /memory | List stored memories |
161
+ | /memory clear | Clear all memories |
162
+ | /skills | List available skills |
163
+ | /skills reload | Hot-reload skills from disk |
164
+ | /skill <name> [args] | Run a skill by name |
165
+ | /plan | Enter plan mode (read-only investigation) |
166
+ | /do | Exit plan mode and execute the approved plan |
167
+ | /compact | Force conversation compaction |
168
+ | /clear | Reset the session and clear the terminal |
169
+ | /resume [id] | List or restore a previous session |
170
+ | /rewind | Open checkpoint rewind dialog |
171
+ | /sandbox [1/2/3] | Configure sandbox (1=on+auto, 2=on+manual, 3=off) |
172
+ | /worktree | List git worktrees |
173
+ | /mcp | Show MCP server status |
174
+ | /quit | Exit the application |
175
+
176
+ ### Keyboard Shortcuts
177
+
178
+ | Key | Action |
179
+ | --------- | -------------------------------------------------------------------- |
180
+ | Ctrl+C | Interrupt streaming (first press), exit app (second press within 2s) |
181
+ | Ctrl+O | Toggle full vs. truncated tool output |
182
+ | Ctrl+T | Toggle Teams dialog overlay |
183
+ | Shift+Tab | Cycle permission modes |
184
+
185
+ ## Development
186
+
187
+ Requires Node.js 20+ and pnpm 10+.
188
+
189
+ ### Scripts
190
+
191
+ ```bash
192
+ pnpm dev # Run in development mode via tsx
193
+ pnpm build # Build production bundle with tsup (runs prebuild first)
194
+ pnpm test # Run tests with Vitest
195
+ pnpm test:watch # Run tests in watch mode
196
+ pnpm lint # Lint source with ESLint
197
+ pnpm lint:fix # Lint and auto-fix
198
+ pnpm format # Format with oxfmt
199
+ pnpm typecheck # Type-check with tsc --noEmit
200
+
201
+ pnpm dev:docs # Run documentation site (Rspress)
202
+ pnpm build:docs # Build documentation
203
+ pnpm preview:docs # Preview built documentation
204
+
205
+ pnpm fe:dev # Run remote frontend in dev mode (Rsbuild)
206
+ pnpm fe:build # Build remote frontend
207
+ pnpm fe:preview # Preview built frontend
208
+ ```
209
+
210
+ ### Build Output
211
+
212
+ The tsup build produces a single ESM bundle at dist/main.js with a #!/usr/bin/env node shebang. The prebuild step compiles the glob-wasm module and the glob-addon native binary. These are copied into dist/ along with built-in skills:
213
+
214
+ - dist/main.js: CLI entry point
215
+ - dist/release.wasm: WASM glob matcher
216
+ - dist/glob_addon.node: Native C++ addon for glob (platform-specific)
217
+ - dist/builtin/: Built-in skill markdown files
218
+
219
+ ### Testing
220
+
221
+ Tests live in tests/ and use Vitest with V8 coverage. The test timeout is set to 30 seconds.
222
+
223
+ ```bash
224
+ pnpm test
225
+ ```
226
+
227
+ ## Dependencies and Tech Stack
228
+
229
+ ### Runtime
230
+
231
+ | Package | Purpose |
232
+ | ------------------------- | -------------------------------------------- |
233
+ | @anthropic-ai/sdk | Anthropic Claude API client |
234
+ | openai | OpenAI API client |
235
+ | @modelcontextprotocol/sdk | MCP protocol support |
236
+ | ink + react | Terminal UI framework |
237
+ | zod | Schema validation (config, session, hooks) |
238
+ | js-yaml | YAML config parsing |
239
+ | koa + ws | Remote mode HTTP server and WebSocket bridge |
240
+ | pino | Structured logging |
241
+ | marked + dompurify | Markdown rendering and sanitization |
242
+ | fuse.js | Fuzzy search for tool/command lookup |
243
+ | chalk | Terminal color output |
244
+
245
+ ### Build Toolchain
246
+
247
+ | Tool | Purpose |
248
+ | -------------------------- | ------------------------------------- |
249
+ | tsup | Bundle to single ESM file for Node.js |
250
+ | tsx | Development runner |
251
+ | typescript | Type checking |
252
+ | vitest | Testing |
253
+ | eslint + typescript-eslint | Linting |
254
+ | oxfmt | Code formatting |
255
+ | rspress | Documentation site |
256
+ | rsbuild | Remote frontend bundler |
257
+ | biome | Remote frontend linter/formatter |
258
+ | tailwindcss | Remote frontend styling |
259
+
260
+ ## Project Structure
261
+
262
+ ```
263
+ apps/swifty/
264
+ src/
265
+ main.tsx CLI entry point (TUI, remote, print, teammate routing)
266
+ agent/ Agent loop (ReAct pattern, streaming executor)
267
+ compact/ Context window compaction
268
+ commands/ Slash command registry and loader
269
+ config/ YAML config loading and validation
270
+ conversation/ Message management and conversation state
271
+ file-history/ File snapshot checkpoints for rewind
272
+ hooks/ Event-driven hook engine
273
+ llm/ LLM clients (Anthropic, OpenAI, OpenAI-compatible)
274
+ logger/ Structured logging (pino)
275
+ mcp/ MCP client, manager, and tool wrapper
276
+ memory/ Long-term memory extraction, consolidation, recall
277
+ permissions/ Permission checker with dangerous pattern detection
278
+ plan-file/ Plan mode file management
279
+ print-mode/ Non-interactive print mode (-p flag)
280
+ prompt/ System prompt builder and environment detection
281
+ remote/
282
+ server.ts Koa + WebSocket remote server
283
+ fe/ React frontend for remote mode (Rsbuild)
284
+ sandbox/ Sandbox implementations (bwrap, seatbelt)
285
+ session/ Session persistence and resume
286
+ skills/ Skill catalog, executor, load/install tools
287
+ subagent/ Subagent spawning and task management
288
+ teams/ Team coordination, file mailboxes, progress tracking
289
+ teammate.ts Teammate process entry point
290
+ todo/ Task list management tools
291
+ tool-result/ Tool output budgeting and reconstruction
292
+ tools/ Built-in tools (ReadFile, Bash, Glob, Grep, etc.)
293
+ tui/ Terminal UI components (Ink/React)
294
+ utils/ Shared utilities
295
+ worktree/ Git worktree management
296
+ tests/ Vitest test suite (25 test files)
297
+ docs/ Rspress documentation (15 chapters)
298
+ ```
299
+
300
+ ## License
301
+
302
+ ISC
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire as __swiftyCreateRequire } from "node:module";
3
+ const require = __swiftyCreateRequire(import.meta.url);
4
+ import{g as v}from"./chunk-H7VXSMJR.js";import{$,L as w,N as P,O as S,P as I,Q as k,R as N,X as O,Y as M,Z as j,_ as U,aa as K,ca as B,da as R,e as L,p as E}from"./chunk-SLGPWJQF.js";import"./chunk-MQ5XOYLD.js";import"./chunk-X3UA5OZL.js";var x=L({module:"llm"});var G=3e3,H=w.object({max_input_tokens:w.coerce.number()});async function tt(o){if(o.protocol!=="anthropic")return 0;let t=R(o),n=`${o.base_url.replace(/\/+$/,"")}/v1/models/${encodeURIComponent(o.model)}`,r=new AbortController,a=setTimeout(()=>{r.abort()},G);try{let l=await fetch(n,{method:"GET",headers:{"anthropic-version":"2023-06-01",...t?{"x-api-key":t}:{}},signal:r.signal});if(!l.ok)return 0;let g=await l.json(),{success:h,error:d,data:f}=await E(H,g);if(!h)return console.error(d.message),0;let y=f.max_input_tokens;return Math.max(y,0)}catch(l){return console.error(l),0}finally{clearTimeout(a)}}function Y(){return!0}function q(o){let t=[];for(let e of o)if(e.role==="assistant"){let n=[];if(e.thinkingBlocks)for(let r of e.thinkingBlocks)n.push({type:"thinking",thinking:r.thinking,signature:r.signature});if(e.content&&n.push({type:"text",text:e.content}),e.toolUses)for(let r of e.toolUses)n.push({type:"tool_use",id:r.toolUseId,name:r.toolName,input:r.arguments});n.length===0&&n.push({type:"text",text:""}),t.push({role:"assistant",content:n})}else if(e.toolResults&&e.toolResults.length>0){let n=[];for(let r of e.toolResults)n.push({type:"tool_result",tool_use_id:r.toolUseId,is_error:r.isError,content:r.content});t.push({role:"user",content:n})}else{if(t.length===0){t.push({role:"user",content:[{type:"text",text:e.content}]});continue}let n=!1,r=t[t.length-1],a=r.content;r.role==="user"&&(typeof a=="string"||Array.isArray(a)&&a.length>0&&a[0].type==="text")&&(n=!0),n?(typeof a=="string"&&(a=r.content=a.trim().length>0?[{type:"text",text:a}]:[]),a.push({type:"text",text:e.content})):t.push({role:"user",content:[{type:"text",text:e.content}]})}return t}var W=class{client;model;thinking;systemPrompt;maxOutputTokens;contextWindow;constructor(t,e){let n=R(t);if(!n)throw new M("Anthropic API key not found, set ANTHROPIC_API_KEY in .swifty/config.y(a)ml, or via ANTHROPIC_API_KEY env variable.");this.client=new v({apiKey:n,baseURL:t.base_url}),this.model=t.model,this.thinking=t.thinking??!0,this.systemPrompt=e,this.maxOutputTokens=B(t),this.contextWindow=K(t)}setSystemPrompt(t){this.systemPrompt=t}setMaxOutputTokens(t){this.maxOutputTokens=t}async*stream(t,e,n){let r=q(t.getMessages()),a=e.map(c=>{let p=c.input_schema;return{name:c.name,description:c.description,input_schema:{type:"object",properties:p.properties,required:p.required??[]}}});a.length>0&&(a[a.length-1].cache_control={type:"ephemeral"}),D(r);let l={model:this.model,max_tokens:this.maxOutputTokens,stream:!0,system:[{type:"text",text:this.systemPrompt,cache_control:{type:"ephemeral"}}],messages:r,...a.length>0?{tools:a}:{}};this.thinking?Y()&&(l.thinking={type:"enabled",budget_tokens:this.maxOutputTokens-1}):l.thinking={type:"enabled",budget_tokens:this.maxOutputTokens-1};let g=0,h=0,d=0,f=0,y="end_turn",b="",T="",A=!1,C=0;try{let c=this.client.messages.stream(l,{...n?{signal:n}:{}}),p="",_="",u="";for await(let i of c)switch(i.type){case"content_block_start":{let s=i.content_block;s.type==="thinking"?(A=!0,b="",T=""):s.type==="tool_use"&&(_=s.id,p=s.name,u="",yield{type:"tool_call_start",toolName:p,toolId:_});break}case"content_block_delta":{let s=i.delta;s.type==="thinking_delta"?(b+=s.thinking,yield{type:"thinking_delta",text:s.thinking}):s.type==="signature_delta"?(x.debug({signature:s.signature},"thinking signature received"),T=s.signature):s.type==="text_delta"?yield{type:"text_delta",text:s.text}:s.type==="input_json_delta"&&(u+=s.partial_json,yield{type:"tool_call_delta",text:s.partial_json});break}case"content_block_stop":{if(A&&(yield{type:"thinking_complete",thinking:b,signature:T},A=!1),p){let s={};if(u)try{let m=JSON.parse(u);s=S(m)?I(m):{[P]:u}}catch(m){x.error({err:m},"llm operation failed"),s={[P]:u}}yield{type:"tool_call_complete",toolId:_,toolName:p,arguments:s},p="",_="",u=""}break}case"message_delta":{i.delta.stop_reason&&(y=i.delta.stop_reason),i.usage.output_tokens&&(h=i.usage.output_tokens,i.usage.input_tokens&&(g=i.usage.input_tokens),i.usage.cache_read_input_tokens&&(d=i.usage.cache_read_input_tokens),i.usage.cache_creation_input_tokens&&(f=i.usage.cache_creation_input_tokens));break}case"message_start":{C=performance.now(),g=i.message.usage.input_tokens,h=i.message.usage.output_tokens,d=i.message.usage.cache_read_input_tokens??0,f=i.message.usage.cache_creation_input_tokens??0;break}case"message_stop":{let m=performance.now()-C;x.debug({elapsedMs:m},"message stream complete");break}}yield{type:"stream_end",stopReason:y,usage:{inputTokens:g,outputTokens:h,cacheReadInputTokens:d,cacheCreationInputTokens:f}}}catch(c){throw x.error({err:c},"llm operation failed"),F(c)}}};function D(o){for(let t=o.length-1;t>=0;t--){if(o[t].role!=="user")continue;let e=o[t].content;if(typeof e=="string"&&e.length===0||Array.isArray(e)&&e.length===0)return;typeof e=="string"&&(e=o[t].content=[{type:"text",text:e}]);let n=e[e.length-1];Reflect.set(n,"cache_control",{type:"ephemeral"})}}function F(o){if(o instanceof v.APIError){if(o.status===413||/prompts?\s+too\s+long/i.test(o.message))return new $(`Prompt too long: ${o.message}`);if(o.status===401)return new M(`Invalid API key: ${o.message}`);if(o.status===429){let t=I(o.headers)["retry-after"],e="Rate Limited";if(t){let n=Number.parseInt(k(t));Number.isNaN(n)&&(e+=", please wait."),e+=`, retry after ${k(n)}s.`}else e+=", please wait.";return new j(e,t?k(t):void 0)}return new O(`Anthropic API error (${k(o.status)}): ${o.message}`)}return new U(`Network error: ${N(o)}`)}export{W as AnthropicClient,q as buildAnthropicMessages,tt as fetchModelContextWindow};
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire as __swiftyCreateRequire } from "node:module";
3
+ const require = __swiftyCreateRequire(import.meta.url);
4
+ import{readdir as c,stat as l,unlink as u,access as w}from"fs/promises";import{join as s}from"path";var y=30,g=y*24*60*60*1e3;async function f(t){try{return await w(t),!0}catch{return!1}}async function o(t){if(!await f(t))return 0;let n;try{n=(await c(t)).filter(a=>a.endsWith(".jsonl"))}catch{return 0}let r=Date.now(),e=0;for(let a of n){let i=s(t,a);try{let m=await l(i);r-m.mtimeMs>g&&(await u(i),e++)}catch{}}return e}async function p(t){let n=0;n+=await o(s(t,".swifty","logs"));let r=s(t,".swifty","teams");if(!await f(r))return n;let e;try{e=await c(r)}catch{return n}for(let a of e)n+=await o(s(r,a,"logs"));return n}export{p as a};