heyio 0.1.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/LICENSE +21 -0
- package/README.md +212 -0
- package/dist/api/server.js +70 -0
- package/dist/config.js +28 -0
- package/dist/copilot/agents.js +267 -0
- package/dist/copilot/client.js +29 -0
- package/dist/copilot/orchestrator.js +354 -0
- package/dist/copilot/skills.js +132 -0
- package/dist/copilot/system-message.js +90 -0
- package/dist/copilot/tools.js +239 -0
- package/dist/daemon.js +145 -0
- package/dist/index.js +139 -0
- package/dist/paths.js +10 -0
- package/dist/store/db.js +101 -0
- package/dist/store/squads.js +52 -0
- package/dist/store/tasks.js +32 -0
- package/dist/telegram/bot.js +141 -0
- package/dist/telegram/handlers.js +4 -0
- package/dist/tui/index.js +79 -0
- package/dist/update.js +53 -0
- package/dist/wiki/fs.js +152 -0
- package/dist/wiki/search.js +49 -0
- package/package.json +54 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Michael Jolley
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# 🤖 IO
|
|
2
|
+
|
|
3
|
+
A personal AI assistant daemon built on the GitHub Copilot SDK. IO runs 24/7 on your machine, reachable via Telegram and a terminal TUI.
|
|
4
|
+
|
|
5
|
+
[](https://github.com/michaeljolley/io/actions/workflows/ci.yml)
|
|
6
|
+

|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
## ✨ Features
|
|
10
|
+
|
|
11
|
+
- **Copilot SDK Integration** — powered by GitHub's Copilot SDK for LLM conversations with tool calling
|
|
12
|
+
- **Multi-Interface** — Telegram bot + terminal TUI + HTTP API (future web UI)
|
|
13
|
+
- **Persistent Memory** — wiki-based knowledge base stored at `~/.io/wiki/`
|
|
14
|
+
- **Squad System** — persistent project teams that remember decisions, context, and history
|
|
15
|
+
- **Skills** — modular skill system; install from git repos or the [skills.sh](https://skills.sh) registry
|
|
16
|
+
- **Adaptive Sessions** — infinite sessions with automatic context compaction
|
|
17
|
+
- **Worker Agents** — delegated task execution through specialized agent sessions
|
|
18
|
+
- **Self-Updating** — checks for updates and can apply them automatically
|
|
19
|
+
|
|
20
|
+
## đź“‹ Prerequisites
|
|
21
|
+
|
|
22
|
+
- **Node.js** >= 18
|
|
23
|
+
- **GitHub Copilot subscription** — IO uses the Copilot SDK, which requires an active Copilot license
|
|
24
|
+
- **GitHub CLI** (`gh`) — authenticated via `gh auth login`
|
|
25
|
+
|
|
26
|
+
## 🚀 Quick Start
|
|
27
|
+
|
|
28
|
+
### Install
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npm install -g heyio
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Setup
|
|
35
|
+
|
|
36
|
+
Run the setup wizard to configure your Telegram bot token and user ID:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
io setup
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
This creates a config file at `~/.io/config.json`.
|
|
43
|
+
|
|
44
|
+
### Run
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
# Interactive TUI mode
|
|
48
|
+
io
|
|
49
|
+
|
|
50
|
+
# Background daemon (Telegram + HTTP API)
|
|
51
|
+
io --daemon
|
|
52
|
+
|
|
53
|
+
# Allow IO to modify its own source code
|
|
54
|
+
io --self-edit
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## đź’¬ CLI Usage
|
|
58
|
+
|
|
59
|
+
| Command | Description |
|
|
60
|
+
| --- | --- |
|
|
61
|
+
| `io` | Start interactive TUI mode |
|
|
62
|
+
| `io --daemon` | Run as background daemon (Telegram + API) |
|
|
63
|
+
| `io --self-edit` | Allow IO to modify its own source |
|
|
64
|
+
| `io setup` | Configure Telegram bot token and user ID |
|
|
65
|
+
| `io skill list` | List installed skills |
|
|
66
|
+
| `io skill add <repo-url>` | Install a skill from a git repository |
|
|
67
|
+
| `io skill remove <slug>` | Remove an installed skill |
|
|
68
|
+
| `io skill search <query>` | Search the skills.sh registry |
|
|
69
|
+
|
|
70
|
+
## ⚙️ Configuration
|
|
71
|
+
|
|
72
|
+
IO stores its configuration at `~/.io/config.json`. The setup wizard (`io setup`) handles initial configuration, but you can also edit the file directly.
|
|
73
|
+
|
|
74
|
+
```jsonc
|
|
75
|
+
{
|
|
76
|
+
// Telegram bot token from @BotFather
|
|
77
|
+
"telegramBotToken": "123456:ABC-DEF...",
|
|
78
|
+
|
|
79
|
+
// Your Telegram user ID (for authentication)
|
|
80
|
+
"telegramUserId": 123456789,
|
|
81
|
+
|
|
82
|
+
// Enable self-edit mode by default
|
|
83
|
+
"selfEdit": false
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
All persistent data is stored under `~/.io/`:
|
|
88
|
+
|
|
89
|
+
| Path | Purpose |
|
|
90
|
+
| --- | --- |
|
|
91
|
+
| `~/.io/config.json` | User configuration |
|
|
92
|
+
| `~/.io/wiki/` | Knowledge base (markdown files) |
|
|
93
|
+
| `~/.io/io.db` | SQLite database (squads, tasks) |
|
|
94
|
+
| `~/.io/skills/` | Installed skills |
|
|
95
|
+
|
|
96
|
+
## đź§© Skills System
|
|
97
|
+
|
|
98
|
+
Skills are modular extensions that add new tools and capabilities to IO. Each skill is a directory containing a `SKILL.md` manifest and tool definitions.
|
|
99
|
+
|
|
100
|
+
### Managing Skills
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
# Search the skills.sh registry
|
|
104
|
+
io skill search "github"
|
|
105
|
+
|
|
106
|
+
# Install from a git repo
|
|
107
|
+
io skill add https://github.com/user/my-skill.git
|
|
108
|
+
|
|
109
|
+
# List installed skills
|
|
110
|
+
io skill list
|
|
111
|
+
|
|
112
|
+
# Remove a skill
|
|
113
|
+
io skill remove my-skill
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Creating a Skill
|
|
117
|
+
|
|
118
|
+
A skill is a directory with a `SKILL.md` file that describes the skill and its tools. See the [Contributing Guide](CONTRIBUTING.md) for details on the skill format.
|
|
119
|
+
|
|
120
|
+
## 👥 Squad System
|
|
121
|
+
|
|
122
|
+
Squads are persistent project teams that IO manages. Each squad:
|
|
123
|
+
|
|
124
|
+
- Is associated with a specific project or domain
|
|
125
|
+
- Remembers decisions, context, and conversation history
|
|
126
|
+
- Can have multiple specialized agents working together
|
|
127
|
+
- Persists across sessions in the SQLite database
|
|
128
|
+
|
|
129
|
+
IO's orchestrator automatically creates and manages squads based on your conversations.
|
|
130
|
+
|
|
131
|
+
## 🏗️ Architecture
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
User → [TUI / Telegram / HTTP API]
|
|
135
|
+
↓
|
|
136
|
+
Orchestrator (Copilot SDK)
|
|
137
|
+
↕ ↕
|
|
138
|
+
Squad Manager Wiki/Memory
|
|
139
|
+
↓
|
|
140
|
+
Worker Agents
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
IO is built around the **Copilot SDK** which handles all LLM interactions, including tool calling and context management. The **Orchestrator** manages the primary conversation session with automatic context compaction for infinite-length sessions.
|
|
144
|
+
|
|
145
|
+
For complex tasks, the orchestrator delegates work to **Worker Agents** — short-lived agent sessions that execute specific tasks and report back.
|
|
146
|
+
|
|
147
|
+
The **Squad System** provides persistent project context, while the **Wiki** serves as a long-term knowledge base that spans all conversations.
|
|
148
|
+
|
|
149
|
+
## 🏗️ Project Structure
|
|
150
|
+
|
|
151
|
+
```
|
|
152
|
+
src/
|
|
153
|
+
├── index.ts # CLI entry (commander)
|
|
154
|
+
├── daemon.ts # Daemon startup/shutdown
|
|
155
|
+
├── config.ts # Config loading
|
|
156
|
+
├── paths.ts # Path constants
|
|
157
|
+
├── update.ts # Self-update checker
|
|
158
|
+
├── copilot/
|
|
159
|
+
│ ├── client.ts # CopilotClient singleton
|
|
160
|
+
│ ├── orchestrator.ts # Main session management
|
|
161
|
+
│ ├── agents.ts # Worker agent sessions
|
|
162
|
+
│ ├── tools.ts # Tool definitions
|
|
163
|
+
│ ├── skills.ts # Skills loader
|
|
164
|
+
│ └── system-message.ts # System prompt builder
|
|
165
|
+
├── store/
|
|
166
|
+
│ ├── db.ts # SQLite database
|
|
167
|
+
│ ├── squads.ts # Squad CRUD
|
|
168
|
+
│ └── tasks.ts # Agent task tracking
|
|
169
|
+
├── wiki/
|
|
170
|
+
│ ├── fs.ts # Wiki filesystem
|
|
171
|
+
│ └── search.ts # Wiki search
|
|
172
|
+
├── telegram/
|
|
173
|
+
│ ├── bot.ts # Grammy Telegram bot
|
|
174
|
+
│ └── handlers.ts # Command handlers
|
|
175
|
+
├── tui/
|
|
176
|
+
│ └── index.ts # Terminal UI
|
|
177
|
+
└── api/
|
|
178
|
+
└── server.ts # Express HTTP + SSE
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## 🛠️ Development
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
# Clone the repository
|
|
185
|
+
git clone https://github.com/michaeljolley/io.git
|
|
186
|
+
cd io
|
|
187
|
+
|
|
188
|
+
# Install dependencies
|
|
189
|
+
npm install
|
|
190
|
+
|
|
191
|
+
# Run in development mode (watch)
|
|
192
|
+
npm run dev
|
|
193
|
+
|
|
194
|
+
# Build for production
|
|
195
|
+
npm run build
|
|
196
|
+
|
|
197
|
+
# Run the TUI directly
|
|
198
|
+
npm run tui
|
|
199
|
+
|
|
200
|
+
# Run the daemon directly
|
|
201
|
+
npm run daemon
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for detailed development guidelines.
|
|
205
|
+
|
|
206
|
+
## đź“„ License
|
|
207
|
+
|
|
208
|
+
This project is licensed under the [MIT License](LICENSE).
|
|
209
|
+
|
|
210
|
+
## 🤝 Contributing
|
|
211
|
+
|
|
212
|
+
Contributions are welcome! Please read the [Contributing Guide](CONTRIBUTING.md) and [Code of Conduct](CODE_OF_CONDUCT.md) before submitting a pull request.
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import express from "express";
|
|
2
|
+
import { config } from "../config.js";
|
|
3
|
+
let messageHandler;
|
|
4
|
+
const sseConnections = new Set();
|
|
5
|
+
export function setMessageHandler(handler) {
|
|
6
|
+
messageHandler = handler;
|
|
7
|
+
}
|
|
8
|
+
export function broadcastToSSE(text) {
|
|
9
|
+
const payload = JSON.stringify({ type: "delta", text });
|
|
10
|
+
for (const res of sseConnections) {
|
|
11
|
+
res.write(`data: ${payload}\n\n`);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export async function startApiServer() {
|
|
15
|
+
const app = express();
|
|
16
|
+
app.use(express.json());
|
|
17
|
+
app.use((_req, res, next) => {
|
|
18
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
19
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
20
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
21
|
+
next();
|
|
22
|
+
});
|
|
23
|
+
app.get("/health", (_req, res) => {
|
|
24
|
+
res.json({ status: "ok" });
|
|
25
|
+
});
|
|
26
|
+
app.get("/status", (_req, res) => {
|
|
27
|
+
res.json({ version: "1.0.0", uptime: process.uptime() });
|
|
28
|
+
});
|
|
29
|
+
app.post("/message", async (req, res) => {
|
|
30
|
+
const { text } = req.body;
|
|
31
|
+
if (!text) {
|
|
32
|
+
res.status(400).json({ error: "Missing 'text' in request body" });
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
if (!messageHandler) {
|
|
36
|
+
res.status(503).json({ error: "No message handler registered" });
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const connectionId = crypto.randomUUID();
|
|
40
|
+
let fullResponse = "";
|
|
41
|
+
await messageHandler(text, connectionId, (chunk, done) => {
|
|
42
|
+
fullResponse += chunk;
|
|
43
|
+
const ssePayload = JSON.stringify({
|
|
44
|
+
type: done ? "done" : "delta",
|
|
45
|
+
text: chunk,
|
|
46
|
+
});
|
|
47
|
+
for (const conn of sseConnections) {
|
|
48
|
+
conn.write(`data: ${ssePayload}\n\n`);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
res.json({ response: fullResponse });
|
|
52
|
+
});
|
|
53
|
+
app.get("/events", (req, res) => {
|
|
54
|
+
res.setHeader("Content-Type", "text/event-stream");
|
|
55
|
+
res.setHeader("Cache-Control", "no-cache");
|
|
56
|
+
res.setHeader("Connection", "keep-alive");
|
|
57
|
+
res.flushHeaders();
|
|
58
|
+
sseConnections.add(res);
|
|
59
|
+
req.on("close", () => {
|
|
60
|
+
sseConnections.delete(res);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
return new Promise((resolve) => {
|
|
64
|
+
app.listen(config.apiPort, () => {
|
|
65
|
+
console.log(`[io] API server listening on port ${config.apiPort}`);
|
|
66
|
+
resolve();
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=server.js.map
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
|
|
2
|
+
import { CONFIG_PATH, IO_HOME } from "./paths.js";
|
|
3
|
+
const DEFAULT_CONFIG = {
|
|
4
|
+
telegramEnabled: false,
|
|
5
|
+
selfEditEnabled: false,
|
|
6
|
+
apiPort: 3170,
|
|
7
|
+
};
|
|
8
|
+
function loadConfig() {
|
|
9
|
+
mkdirSync(IO_HOME, { recursive: true });
|
|
10
|
+
if (!existsSync(CONFIG_PATH)) {
|
|
11
|
+
writeFileSync(CONFIG_PATH, JSON.stringify(DEFAULT_CONFIG, null, 2));
|
|
12
|
+
return { ...DEFAULT_CONFIG };
|
|
13
|
+
}
|
|
14
|
+
try {
|
|
15
|
+
const raw = readFileSync(CONFIG_PATH, "utf-8");
|
|
16
|
+
const parsed = JSON.parse(raw);
|
|
17
|
+
return { ...DEFAULT_CONFIG, ...parsed };
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return { ...DEFAULT_CONFIG };
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export const config = loadConfig();
|
|
24
|
+
export function saveConfig(updates) {
|
|
25
|
+
Object.assign(config, updates);
|
|
26
|
+
writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { randomUUID } from "crypto";
|
|
2
|
+
import { execSync } from "child_process";
|
|
3
|
+
import { readFileSync, writeFileSync, readdirSync, statSync, existsSync, mkdirSync, } from "fs";
|
|
4
|
+
import { join, dirname, resolve } from "path";
|
|
5
|
+
import { defineTool, approveAll } from "@github/copilot-sdk";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { getClient } from "./client.js";
|
|
8
|
+
import { getSquad, updateSquadSession, updateSquadStatus, getDecisionsSummary, logDecision, } from "../store/squads.js";
|
|
9
|
+
import { createTask, completeTask, failTask, getActiveTasks, } from "../store/tasks.js";
|
|
10
|
+
import { SESSIONS_DIR } from "../paths.js";
|
|
11
|
+
const agentSessions = new Map();
|
|
12
|
+
export function getAgentInfo() {
|
|
13
|
+
const activeTasks = getActiveTasks();
|
|
14
|
+
const tasksByAgent = new Map();
|
|
15
|
+
for (const task of activeTasks) {
|
|
16
|
+
tasksByAgent.set(task.agent_slug, task.description);
|
|
17
|
+
}
|
|
18
|
+
const agents = [];
|
|
19
|
+
for (const [slug, _session] of agentSessions) {
|
|
20
|
+
const squad = getSquad(slug);
|
|
21
|
+
const currentTask = tasksByAgent.get(slug);
|
|
22
|
+
let status = "idle";
|
|
23
|
+
if (currentTask) {
|
|
24
|
+
status = "working";
|
|
25
|
+
}
|
|
26
|
+
if (squad?.status === "error") {
|
|
27
|
+
status = "error";
|
|
28
|
+
}
|
|
29
|
+
agents.push({
|
|
30
|
+
slug,
|
|
31
|
+
name: squad?.name ?? slug,
|
|
32
|
+
status,
|
|
33
|
+
currentTask,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
return agents;
|
|
37
|
+
}
|
|
38
|
+
export async function delegateToAgent(squadSlug, task, onComplete) {
|
|
39
|
+
const squad = getSquad(squadSlug);
|
|
40
|
+
if (!squad) {
|
|
41
|
+
throw new Error(`Squad not found: ${squadSlug}`);
|
|
42
|
+
}
|
|
43
|
+
const session = await getOrCreateSession(squadSlug);
|
|
44
|
+
const taskId = randomUUID();
|
|
45
|
+
createTask(taskId, squadSlug, task);
|
|
46
|
+
updateSquadStatus(squadSlug, "working");
|
|
47
|
+
// Run the task in the background — return taskId immediately
|
|
48
|
+
void (async () => {
|
|
49
|
+
try {
|
|
50
|
+
const response = await session.sendAndWait({ prompt: task });
|
|
51
|
+
const result = response?.data?.content ?? "Task completed (no output)";
|
|
52
|
+
completeTask(taskId, result);
|
|
53
|
+
updateSquadStatus(squadSlug, "idle");
|
|
54
|
+
onComplete(taskId, result);
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
58
|
+
failTask(taskId, message);
|
|
59
|
+
updateSquadStatus(squadSlug, "error");
|
|
60
|
+
}
|
|
61
|
+
})();
|
|
62
|
+
return taskId;
|
|
63
|
+
}
|
|
64
|
+
export async function shutdownAgents() {
|
|
65
|
+
for (const [slug, session] of agentSessions) {
|
|
66
|
+
try {
|
|
67
|
+
await session.destroy();
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
// best-effort cleanup
|
|
71
|
+
}
|
|
72
|
+
agentSessions.delete(slug);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
export function getActiveAgentTasks() {
|
|
76
|
+
return getActiveTasks().map((t) => ({
|
|
77
|
+
taskId: t.task_id,
|
|
78
|
+
agentSlug: t.agent_slug,
|
|
79
|
+
description: t.description,
|
|
80
|
+
status: t.status,
|
|
81
|
+
}));
|
|
82
|
+
}
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
// Internal helpers
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
async function getOrCreateSession(squadSlug) {
|
|
87
|
+
const existing = agentSessions.get(squadSlug);
|
|
88
|
+
if (existing)
|
|
89
|
+
return existing;
|
|
90
|
+
const squad = getSquad(squadSlug);
|
|
91
|
+
const client = await getClient();
|
|
92
|
+
const decisions = getDecisionsSummary(squadSlug);
|
|
93
|
+
const agentTools = buildAgentTools(squadSlug);
|
|
94
|
+
const commonConfig = {
|
|
95
|
+
model: "gpt-4.1",
|
|
96
|
+
configDir: SESSIONS_DIR,
|
|
97
|
+
streaming: false,
|
|
98
|
+
systemMessage: {
|
|
99
|
+
content: `You are a specialist agent working on the "${squad.name}" project at ${squad.project_path}.
|
|
100
|
+
|
|
101
|
+
## Past Decisions
|
|
102
|
+
${decisions}
|
|
103
|
+
|
|
104
|
+
## Your Role
|
|
105
|
+
You are a coding agent. Use the shell tool to run commands and file_ops to read/write files.
|
|
106
|
+
Log important decisions with squad_log_decision so they persist.`,
|
|
107
|
+
},
|
|
108
|
+
tools: agentTools,
|
|
109
|
+
onPermissionRequest: approveAll,
|
|
110
|
+
infiniteSessions: {
|
|
111
|
+
enabled: true,
|
|
112
|
+
backgroundCompactionThreshold: 0.8,
|
|
113
|
+
bufferExhaustionThreshold: 0.95,
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
let session;
|
|
117
|
+
// Try to resume an existing session if we have a saved session ID
|
|
118
|
+
if (squad.copilot_session_id) {
|
|
119
|
+
try {
|
|
120
|
+
session = await client.resumeSession(squad.copilot_session_id, commonConfig);
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
session = await client.createSession(commonConfig);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
session = await client.createSession(commonConfig);
|
|
128
|
+
}
|
|
129
|
+
updateSquadSession(squadSlug, session.sessionId);
|
|
130
|
+
agentSessions.set(squadSlug, session);
|
|
131
|
+
return session;
|
|
132
|
+
}
|
|
133
|
+
function buildAgentTools(squadSlug) {
|
|
134
|
+
const shell = defineTool("shell", {
|
|
135
|
+
description: "Run a shell command. Use for git, build tools, file operations, etc.",
|
|
136
|
+
parameters: z.object({
|
|
137
|
+
command: z.string().describe("The command to run"),
|
|
138
|
+
timeout_secs: z
|
|
139
|
+
.number()
|
|
140
|
+
.optional()
|
|
141
|
+
.describe("Timeout in seconds (default: 60)"),
|
|
142
|
+
working_dir: z
|
|
143
|
+
.string()
|
|
144
|
+
.optional()
|
|
145
|
+
.describe("Working directory for the command"),
|
|
146
|
+
}),
|
|
147
|
+
handler: async ({ command, timeout_secs, working_dir }) => {
|
|
148
|
+
try {
|
|
149
|
+
const result = execSync(command, {
|
|
150
|
+
encoding: "utf-8",
|
|
151
|
+
timeout: (timeout_secs ?? 60) * 1000,
|
|
152
|
+
maxBuffer: 1024 * 1024,
|
|
153
|
+
cwd: working_dir,
|
|
154
|
+
});
|
|
155
|
+
const output = result.trim();
|
|
156
|
+
if (output.length > 8000) {
|
|
157
|
+
return output.slice(0, 8000) + "\n\n[…truncated]";
|
|
158
|
+
}
|
|
159
|
+
return output || "(no output)";
|
|
160
|
+
}
|
|
161
|
+
catch (err) {
|
|
162
|
+
const execErr = err;
|
|
163
|
+
const stderr = execErr.stderr?.trim() ?? "";
|
|
164
|
+
const stdout = execErr.stdout?.trim() ?? "";
|
|
165
|
+
const msg = stderr || stdout || execErr.message || "Command failed";
|
|
166
|
+
if (msg.length > 4000) {
|
|
167
|
+
return `Error:\n${msg.slice(0, 4000)}\n[…truncated]`;
|
|
168
|
+
}
|
|
169
|
+
return `Error:\n${msg}`;
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
const fileOps = defineTool("file_ops", {
|
|
174
|
+
description: "Read, write, or list files on the local filesystem.",
|
|
175
|
+
parameters: z.object({
|
|
176
|
+
operation: z
|
|
177
|
+
.enum(["read", "write", "list"])
|
|
178
|
+
.describe("Operation to perform"),
|
|
179
|
+
path: z.string().describe("File or directory path"),
|
|
180
|
+
content: z
|
|
181
|
+
.string()
|
|
182
|
+
.optional()
|
|
183
|
+
.describe("Content to write (for write operation)"),
|
|
184
|
+
recursive: z
|
|
185
|
+
.boolean()
|
|
186
|
+
.optional()
|
|
187
|
+
.describe("Recurse into subdirectories (for list)"),
|
|
188
|
+
}),
|
|
189
|
+
handler: async ({ operation, path: filePath, content, recursive }) => {
|
|
190
|
+
try {
|
|
191
|
+
const resolved = resolve(filePath);
|
|
192
|
+
if (operation === "read") {
|
|
193
|
+
if (!existsSync(resolved))
|
|
194
|
+
return `File not found: ${filePath}`;
|
|
195
|
+
const text = readFileSync(resolved, "utf-8");
|
|
196
|
+
if (text.length > 8000) {
|
|
197
|
+
return text.slice(0, 8000) + "\n\n[…truncated]";
|
|
198
|
+
}
|
|
199
|
+
return text;
|
|
200
|
+
}
|
|
201
|
+
if (operation === "write") {
|
|
202
|
+
if (!content)
|
|
203
|
+
return "Error: content is required for write operation";
|
|
204
|
+
mkdirSync(dirname(resolved), { recursive: true });
|
|
205
|
+
writeFileSync(resolved, content, "utf-8");
|
|
206
|
+
return `Written: ${filePath}`;
|
|
207
|
+
}
|
|
208
|
+
if (operation === "list") {
|
|
209
|
+
if (!existsSync(resolved))
|
|
210
|
+
return `Directory not found: ${filePath}`;
|
|
211
|
+
if (recursive) {
|
|
212
|
+
const files = walkDirectory(resolved);
|
|
213
|
+
return files.join("\n") || "(empty directory)";
|
|
214
|
+
}
|
|
215
|
+
const entries = readdirSync(resolved);
|
|
216
|
+
return (entries
|
|
217
|
+
.map((e) => {
|
|
218
|
+
const full = join(resolved, e);
|
|
219
|
+
const isDir = statSync(full).isDirectory();
|
|
220
|
+
return isDir ? `${e}/` : e;
|
|
221
|
+
})
|
|
222
|
+
.join("\n") || "(empty directory)");
|
|
223
|
+
}
|
|
224
|
+
return `Unknown operation: ${operation}`;
|
|
225
|
+
}
|
|
226
|
+
catch (err) {
|
|
227
|
+
return `Error: ${err instanceof Error ? err.message : String(err)}`;
|
|
228
|
+
}
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
const squadLogDecision = defineTool("squad_log_decision", {
|
|
232
|
+
description: "Log an important decision for this squad so it persists across sessions.",
|
|
233
|
+
parameters: z.object({
|
|
234
|
+
decision: z.string().describe("The decision made"),
|
|
235
|
+
context: z.string().optional().describe("Context or reasoning"),
|
|
236
|
+
}),
|
|
237
|
+
handler: async ({ decision, context }) => {
|
|
238
|
+
try {
|
|
239
|
+
logDecision(squadSlug, decision, context);
|
|
240
|
+
return `Decision logged for squad ${squadSlug}`;
|
|
241
|
+
}
|
|
242
|
+
catch (err) {
|
|
243
|
+
return `Error: ${err instanceof Error ? err.message : String(err)}`;
|
|
244
|
+
}
|
|
245
|
+
},
|
|
246
|
+
});
|
|
247
|
+
return [shell, fileOps, squadLogDecision];
|
|
248
|
+
}
|
|
249
|
+
function walkDirectory(dir, maxDepth = 3, depth = 0) {
|
|
250
|
+
if (depth >= maxDepth)
|
|
251
|
+
return [];
|
|
252
|
+
const results = [];
|
|
253
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
254
|
+
if (entry.name.startsWith("."))
|
|
255
|
+
continue;
|
|
256
|
+
const full = join(dir, entry.name);
|
|
257
|
+
if (entry.isDirectory()) {
|
|
258
|
+
results.push(`${entry.name}/`);
|
|
259
|
+
results.push(...walkDirectory(full, maxDepth, depth + 1).map((f) => ` ${entry.name}/${f}`));
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
results.push(entry.name);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return results;
|
|
266
|
+
}
|
|
267
|
+
//# sourceMappingURL=agents.js.map
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { CopilotClient } from "@github/copilot-sdk";
|
|
2
|
+
let client;
|
|
3
|
+
export async function getClient() {
|
|
4
|
+
if (!client) {
|
|
5
|
+
client = new CopilotClient({
|
|
6
|
+
autoStart: true,
|
|
7
|
+
autoRestart: true,
|
|
8
|
+
});
|
|
9
|
+
await client.start();
|
|
10
|
+
}
|
|
11
|
+
return client;
|
|
12
|
+
}
|
|
13
|
+
export async function resetClient() {
|
|
14
|
+
if (client) {
|
|
15
|
+
try {
|
|
16
|
+
await client.stop();
|
|
17
|
+
}
|
|
18
|
+
catch { /* best-effort */ }
|
|
19
|
+
client = undefined;
|
|
20
|
+
}
|
|
21
|
+
return getClient();
|
|
22
|
+
}
|
|
23
|
+
export async function stopClient() {
|
|
24
|
+
if (client) {
|
|
25
|
+
await client.stop();
|
|
26
|
+
client = undefined;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=client.js.map
|