@swifty.js/swifty 0.0.1 → 0.0.2

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,183 @@
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 |
@@ -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{q as a}from"./chunk-F6HLYUZ4.js";import"./chunk-FZPTNGTU.js";import"./chunk-4KVSJNS6.js";import"./chunk-MQ5XOYLD.js";import"./chunk-X3UA5OZL.js";export{a as Agent};
@@ -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 P}from"./chunk-H7VXSMJR.js";import{f as B,h as W,i as R}from"./chunk-RD3MICOU.js";import{b as O,c as j,d as M,e as U,f as $,g as K}from"./chunk-FZPTNGTU.js";import"./chunk-7MHXMDYC.js";import{H as w,J as v,K as S,L as I,M as k,N,e as L,l as E}from"./chunk-4KVSJNS6.js";import"./chunk-MQ5XOYLD.js";import"./chunk-X3UA5OZL.js";var x=L({module:"llm"});var H=3e3,Y=w.object({max_input_tokens:w.coerce.number()});async function ot(n){if(n.protocol!=="anthropic")return 0;let t=R(n),o=`${n.base_url.replace(/\/+$/,"")}/v1/models/${encodeURIComponent(n.model)}`,r=new AbortController,a=setTimeout(()=>{r.abort()},H);try{let l=await fetch(o,{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:f,data:d}=await E(Y,g);if(!h)return console.error(f.message),0;let y=d.max_input_tokens;return Math.max(y,0)}catch(l){return console.error(l),0}finally{clearTimeout(a)}}function q(){return!0}function D(n){let t=[];for(let e of n)if(e.role==="assistant"){let o=[];if(e.thinkingBlocks)for(let r of e.thinkingBlocks)o.push({type:"thinking",thinking:r.thinking,signature:r.signature});if(e.content&&o.push({type:"text",text:e.content}),e.toolUses)for(let r of e.toolUses)o.push({type:"tool_use",id:r.toolUseId,name:r.toolName,input:r.arguments});o.length===0&&o.push({type:"text",text:""}),t.push({role:"assistant",content:o})}else if(e.toolResults&&e.toolResults.length>0){let o=[];for(let r of e.toolResults)o.push({type:"tool_result",tool_use_id:r.toolUseId,is_error:r.isError,content:r.content});t.push({role:"user",content:o})}else{if(t.length===0){t.push({role:"user",content:[{type:"text",text:e.content}]});continue}let o=!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")&&(o=!0),o?(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 G=class{client;model;thinking;systemPrompt;maxOutputTokens;contextWindow;constructor(t,e){let o=R(t);if(!o)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 P({apiKey:o,baseURL:t.base_url}),this.model=t.model,this.thinking=t.thinking??!0,this.systemPrompt=e,this.maxOutputTokens=W(t),this.contextWindow=B(t)}setSystemPrompt(t){this.systemPrompt=t}setMaxOutputTokens(t){this.maxOutputTokens=t}async*stream(t,e,o){let r=D(O(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"}),F(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?q()&&(l.thinking={type:"enabled",budget_tokens:this.maxOutputTokens-1}):l.thinking={type:"enabled",budget_tokens:this.maxOutputTokens-1};let g=0,h=0,f=0,d=0,y="end_turn",b="",T="",A=!1,C=0;try{let c=this.client.messages.stream(l,{...o?{signal:o}:{}}),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):{[v]:u}}catch(m){x.error({err:m},"llm operation failed"),s={[v]: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&&(f=i.usage.cache_read_input_tokens),i.usage.cache_creation_input_tokens&&(d=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,f=i.message.usage.cache_read_input_tokens??0,d=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:f,cacheCreationInputTokens:d}}}catch(c){throw x.error({err:c},"llm operation failed"),J(c)}}};function F(n){for(let t=n.length-1;t>=0;t--){if(n[t].role!=="user")continue;let e=n[t].content;if(typeof e=="string"&&e.length===0||Array.isArray(e)&&e.length===0)return;typeof e=="string"&&(e=n[t].content=[{type:"text",text:e}]);let o=e[e.length-1];Reflect.set(o,"cache_control",{type:"ephemeral"})}}function J(n){if(n instanceof P.APIError){if(n.status===413||/prompts?\s+too\s+long/i.test(n.message))return new K(`Prompt too long: ${n.message}`);if(n.status===401)return new M(`Invalid API key: ${n.message}`);if(n.status===429){let t=I(n.headers)["retry-after"],e="Rate Limited";if(t){let o=Number.parseInt(k(t));Number.isNaN(o)&&(e+=", please wait."),e+=`, retry after ${k(o)}s.`}else e+=", please wait.";return new U(e,t?k(t):void 0)}return new j(`Anthropic API error (${k(n.status)}): ${n.message}`)}return new $(`Network error: ${N(n)}`)}export{G as AnthropicClient,D as buildAnthropicMessages,ot 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{a,b,c,d}from"./chunk-6ARDOHBL.js";import"./chunk-7MHXMDYC.js";import"./chunk-4KVSJNS6.js";import"./chunk-MQ5XOYLD.js";import"./chunk-X3UA5OZL.js";export{b as PathSandbox,d as PermissionChecker,c as RuleEngine,a as extractContent};