heyio 0.1.18 → 0.1.20

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 CHANGED
@@ -4,7 +4,7 @@ A personal AI assistant daemon built on the GitHub Copilot SDK. IO runs 24/7 on
4
4
 
5
5
  [![CI](https://github.com/michaeljolley/io/actions/workflows/ci.yml/badge.svg)](https://github.com/michaeljolley/io/actions/workflows/ci.yml)
6
6
  ![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)
7
- ![Node.js](https://img.shields.io/badge/node-%3E%3D18-brightgreen)
7
+ ![Node.js](https://img.shields.io/badge/node-%3E%3D22-brightgreen)
8
8
 
9
9
  ## ✨ Features
10
10
 
@@ -15,13 +15,15 @@ A personal AI assistant daemon built on the GitHub Copilot SDK. IO runs 24/7 on
15
15
  - **Skills** — modular skill system; install from git repos or the [skills.sh](https://skills.sh) registry
16
16
  - **Adaptive Sessions** — infinite sessions with automatic context compaction
17
17
  - **Worker Agents** — delegated task execution through specialized agent sessions
18
+ - **GitHub Integration** — create, list, view, and comment on issues and PRs via the `github` tool
19
+ - **Smart Model Routing** — automatically selects the best model for each task based on complexity
18
20
  - **Self-Updating** — checks for updates and can apply them automatically
19
21
 
20
22
  ## 📋 Prerequisites
21
23
 
22
- - **Node.js** >= 18
24
+ - **Node.js** >= 22
23
25
  - **GitHub Copilot subscription** — IO uses the Copilot SDK, which requires an active Copilot license
24
- - **GitHub CLI** (`gh`) — authenticated via `gh auth login`
26
+ - **GitHub CLI** (`gh`) — required for the `github` tool (issue/PR management). Install from [cli.github.com](https://cli.github.com/) and authenticate with `gh auth login`
25
27
 
26
28
  ## 🚀 Quick Start
27
29
 
@@ -54,6 +56,33 @@ io --daemon
54
56
  io --self-edit
55
57
  ```
56
58
 
59
+ ### Headless Server (systemd)
60
+
61
+ To run IO as a background service on a headless server:
62
+
63
+ 1. Authenticate the Copilot SDK: `copilot login`
64
+ 2. Authenticate the GitHub CLI: `gh auth login`
65
+ 3. Create a systemd service file at `/etc/systemd/system/io.service`:
66
+
67
+ ```ini
68
+ [Unit]
69
+ Description=IO Personal Assistant
70
+ After=network-online.target
71
+ Wants=network-online.target
72
+
73
+ [Service]
74
+ Type=simple
75
+ ExecStart=/usr/bin/env io --daemon
76
+ Restart=always
77
+ RestartSec=10
78
+ Environment=NODE_ENV=production
79
+
80
+ [Install]
81
+ WantedBy=multi-user.target
82
+ ```
83
+
84
+ 4. Enable and start: `systemctl enable --now io`
85
+
57
86
  ## 💬 CLI Usage
58
87
 
59
88
  | Command | Description |
@@ -71,16 +100,38 @@ io --self-edit
71
100
 
72
101
  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
102
 
103
+ ### Parameters
104
+
105
+ | Parameter | Type | Default | Description |
106
+ | --- | --- | --- | --- |
107
+ | `telegramBotToken` | `string` | — | Telegram bot token from [@BotFather](https://t.me/BotFather) |
108
+ | `authorizedUserId` | `number` | — | Your Telegram user ID (only this user can interact with the bot) |
109
+ | `telegramEnabled` | `boolean` | `false` | Enable the Telegram bot interface |
110
+ | `selfEditEnabled` | `boolean` | `false` | Allow IO to modify its own source code |
111
+ | `defaultModel` | `string` | `"gpt-4.1"` | LLM model for the main orchestrator session |
112
+ | `modelTiers` | `object` | *(see below)* | Per-complexity model preferences for squad agents |
113
+ | `modelTiers.high` | `string[]` | `["claude-opus-4.7", "claude-opus-4.6"]` | Models for complex tasks (architecture, debugging, design) |
114
+ | `modelTiers.medium` | `string[]` | `["claude-sonnet-4.6", "gpt-5.5", "claude-opus-4.5"]` | Models for standard tasks (features, tests, reviews) |
115
+ | `modelTiers.low` | `string[]` | `["claude-haiku-4.5", "gpt-5.4-mini"]` | Models for simple tasks (reads, formatting, lookups) |
116
+ | `apiPort` | `number` | `3170` | Port for the HTTP API server |
117
+
118
+ Each `modelTiers` list is a ranked preference — IO picks the first available model at startup.
119
+
120
+ ### Example
121
+
74
122
  ```jsonc
75
123
  {
76
- // Telegram bot token from @BotFather
77
124
  "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
125
+ "authorizedUserId": 123456789,
126
+ "telegramEnabled": true,
127
+ "selfEditEnabled": false,
128
+ "defaultModel": "claude-sonnet-4.6",
129
+ "apiPort": 3170,
130
+ "modelTiers": {
131
+ "high": ["claude-opus-4.7", "claude-opus-4.6"],
132
+ "medium": ["claude-sonnet-4.6", "gpt-5.5", "claude-opus-4.5"],
133
+ "low": ["claude-haiku-4.5", "gpt-5.4-mini"]
134
+ }
84
135
  }
85
136
  ```
86
137
 
@@ -160,6 +211,7 @@ src/
160
211
  │ ├── orchestrator.ts # Main session management
161
212
  │ ├── agents.ts # Worker agent sessions
162
213
  │ ├── tools.ts # Tool definitions
214
+ │ ├── model-router.ts # Complexity-based model selection
163
215
  │ ├── skills.ts # Skills loader
164
216
  │ └── system-message.ts # System prompt builder
165
217
  ├── store/
@@ -2,6 +2,7 @@ import { randomUUID } from "crypto";
2
2
  import { execSync } from "child_process";
3
3
  import { readFileSync, writeFileSync, readdirSync, statSync, existsSync, mkdirSync, } from "fs";
4
4
  import { join, dirname, resolve } from "path";
5
+ import { homedir } from "os";
5
6
  import { defineTool, approveAll } from "@github/copilot-sdk";
6
7
  import { z } from "zod";
7
8
  import { getClient } from "./client.js";
@@ -154,6 +155,7 @@ function buildAgentTools(squadSlug) {
154
155
  timeout: (timeout_secs ?? 60) * 1000,
155
156
  maxBuffer: 1024 * 1024,
156
157
  cwd: working_dir,
158
+ env: { ...process.env, HOME: process.env.HOME || homedir() },
157
159
  });
158
160
  const output = result.trim();
159
161
  if (output.length > 8000) {
@@ -2,7 +2,7 @@ import { approveAll, } from "@github/copilot-sdk";
2
2
  import { config } from "../config.js";
3
3
  import { SESSIONS_DIR } from "../paths.js";
4
4
  import { getState, setState, deleteState, logConversation } from "../store/db.js";
5
- import { clearStaleTasks } from "../store/tasks.js";
5
+ import { clearStaleTasks, getTask } from "../store/tasks.js";
6
6
  import { getSquad, listSquads, createSquad, logDecision, getDecisionsSummary, updateSquadStatus, } from "../store/squads.js";
7
7
  import { readPage, writePage, assertPagePath } from "../wiki/fs.js";
8
8
  import { resolveModelTiers } from "./model-router.js";
@@ -11,6 +11,7 @@ import { getOrchestratorSystemMessage } from "./system-message.js";
11
11
  import { createTools } from "./tools.js";
12
12
  import { getSkillDirectories } from "./skills.js";
13
13
  import { resetClient } from "./client.js";
14
+ import { delegateToAgent, getActiveAgentTasks } from "./agents.js";
14
15
  // ---------------------------------------------------------------------------
15
16
  // Constants
16
17
  // ---------------------------------------------------------------------------
@@ -49,6 +50,14 @@ function getToolDeps() {
49
50
  logDecision,
50
51
  getDecisionsSummary,
51
52
  updateSquadStatus,
53
+ delegateToAgent,
54
+ getTask,
55
+ getActiveAgentTasks: () => getActiveAgentTasks().map((t) => ({
56
+ taskId: t.taskId,
57
+ agentSlug: t.agentSlug,
58
+ description: t.description,
59
+ status: t.status,
60
+ })),
52
61
  };
53
62
  }
54
63
  function getSessionConfig() {
@@ -62,6 +62,16 @@ Squads are persistent project teams. When a user works on a codebase:
62
62
  3. Recall squad context with \`squad_recall\` before doing project work.
63
63
  4. Check squad status with \`squad_status\`.
64
64
 
65
+ ### Delegating Work
66
+ After planning tasks with the user, **use \`squad_delegate\` to send each task to the squad agent for implementation**. The workflow is:
67
+ 1. Plan the work with the user (break into concrete tasks).
68
+ 2. Call \`squad_delegate\` for each task — provide detailed instructions including file paths, expected behavior, and acceptance criteria.
69
+ 3. The agent works autonomously in the background. You get a task ID immediately.
70
+ 4. Use \`squad_task_status\` to check progress and retrieve results.
71
+ 5. Report results back to the user.
72
+
73
+ You can delegate multiple tasks in parallel — each gets its own task ID.
74
+
65
75
  ### Model Selection
66
76
  Squad agents are automatically assigned a model based on task complexity:
67
77
  - **High complexity** (architecture, refactoring, debugging, design) → most capable model
@@ -82,6 +92,8 @@ The model is selected automatically. Tell the user which model tier was chosen w
82
92
  - \`squad_recall\`: Get a squad's context and decisions.
83
93
  - \`squad_status\`: Check squad status.
84
94
  - \`squad_log_decision\`: Log a decision for a squad.
95
+ - \`squad_delegate\`: **Delegate a task to a squad agent.** The agent works autonomously in the background. Returns a task ID.
96
+ - \`squad_task_status\`: Check the status/result of a delegated task, or list all active tasks.
85
97
 
86
98
  ### System
87
99
  - \`shell\`: Run a shell command. You have full system access — you can create directories, install packages, clone repos, etc. **Always use this instead of the built-in \`bash\` tool.**
@@ -3,6 +3,14 @@ import { z } from "zod";
3
3
  import { execSync } from "child_process";
4
4
  import { readFileSync, writeFileSync, readdirSync, statSync, existsSync, mkdirSync } from "fs";
5
5
  import { join, dirname, resolve } from "path";
6
+ import { homedir } from "os";
7
+ // Ensure child processes have HOME set (systemd services often don't)
8
+ function shellEnv() {
9
+ const env = { ...process.env };
10
+ if (!env.HOME)
11
+ env.HOME = homedir();
12
+ return env;
13
+ }
6
14
  export function createTools(deps) {
7
15
  const wikiRead = defineTool("wiki_read", {
8
16
  description: "Read a page from IO's knowledge base wiki. Path is relative to the wiki root (e.g., 'pages/preferences/editor.md').",
@@ -113,6 +121,57 @@ export function createTools(deps) {
113
121
  }
114
122
  },
115
123
  });
124
+ const squadDelegate = defineTool("squad_delegate", {
125
+ description: "Delegate a task to a squad agent for autonomous execution. The agent runs in the background and you get a task ID immediately. Use squad_task_status to check progress. Use this after planning work with the user to send each task to the squad for implementation.",
126
+ skipPermission: true,
127
+ parameters: z.object({
128
+ slug: z.string().describe("Squad slug to delegate to"),
129
+ task: z
130
+ .string()
131
+ .describe("Detailed task description. Be specific — include file paths, expected behavior, acceptance criteria. The agent works autonomously with this as its only instruction."),
132
+ }),
133
+ handler: async ({ slug, task }) => {
134
+ console.error(`[io] squad_delegate called: ${slug} — ${task.slice(0, 100)}…`);
135
+ try {
136
+ const taskId = await deps.delegateToAgent(slug, task, (id, result) => {
137
+ console.error(`[io] Agent task ${id} completed for squad ${slug}`);
138
+ });
139
+ return `Task delegated to squad "${slug}". Task ID: ${taskId}\n\nThe agent is working on this in the background. Use squad_task_status to check progress.`;
140
+ }
141
+ catch (err) {
142
+ return `Error delegating task: ${err instanceof Error ? err.message : String(err)}`;
143
+ }
144
+ },
145
+ });
146
+ const squadTaskStatus = defineTool("squad_task_status", {
147
+ description: "Check the status of a delegated squad task, or list all active tasks. Returns status (running/done/failed) and result when complete.",
148
+ skipPermission: true,
149
+ parameters: z.object({
150
+ task_id: z
151
+ .string()
152
+ .optional()
153
+ .describe("Specific task ID to check. If omitted, lists all active tasks."),
154
+ }),
155
+ handler: async ({ task_id }) => {
156
+ if (task_id) {
157
+ const task = deps.getTask(task_id);
158
+ if (!task)
159
+ return `Task not found: ${task_id}`;
160
+ let response = `**Task ${task.task_id}**\nSquad: ${task.agent_slug}\nStatus: ${task.status}\nDescription: ${task.description}`;
161
+ if (task.result) {
162
+ const result = task.result.length > 4000 ? task.result.slice(0, 4000) + "\n[…truncated]" : task.result;
163
+ response += `\n\nResult:\n${result}`;
164
+ }
165
+ return response;
166
+ }
167
+ const tasks = deps.getActiveAgentTasks();
168
+ if (tasks.length === 0)
169
+ return "No active tasks.";
170
+ return tasks
171
+ .map((t) => `- **${t.taskId}** (${t.agentSlug}) — ${t.status} — ${t.description}`)
172
+ .join("\n");
173
+ },
174
+ });
116
175
  const shell = defineTool("shell", {
117
176
  description: "Run a shell command on the host machine. You have FULL ROOT ACCESS. Use for git, apt, npm, build tools, system administration, or anything else. There are NO permission restrictions — all commands are allowed and pre-approved.",
118
177
  skipPermission: true,
@@ -129,6 +188,7 @@ export function createTools(deps) {
129
188
  timeout: (timeout_secs ?? 60) * 1000,
130
189
  maxBuffer: 1024 * 1024,
131
190
  cwd: working_dir,
191
+ env: shellEnv(),
132
192
  });
133
193
  const output = result.trim();
134
194
  if (output.length > 8000) {
@@ -219,6 +279,7 @@ export function createTools(deps) {
219
279
  encoding: "utf-8",
220
280
  timeout: 60_000,
221
281
  maxBuffer: 1024 * 1024,
282
+ env: shellEnv(),
222
283
  });
223
284
  const output = result.trim();
224
285
  if (output.length > 8000) {
@@ -331,6 +392,7 @@ export function createTools(deps) {
331
392
  encoding: "utf-8",
332
393
  timeout: 30_000,
333
394
  maxBuffer: 1024 * 1024,
395
+ env: shellEnv(),
334
396
  });
335
397
  const output = result.trim();
336
398
  if (output.length > 8000) {
@@ -521,6 +583,7 @@ export function createTools(deps) {
521
583
  encoding: "utf-8",
522
584
  timeout: 30_000,
523
585
  maxBuffer: 1024 * 1024,
586
+ env: shellEnv(),
524
587
  }).trim();
525
588
  if (result.length > 8000) {
526
589
  return result.slice(0, 8000) + "\n\n[…truncated]";
@@ -534,7 +597,7 @@ export function createTools(deps) {
534
597
  }
535
598
  },
536
599
  });
537
- return [wikiRead, wikiWrite, wikiSearch, squadCreate, squadRecall, squadStatus, squadLogDecision, shell, fileOps, bash, readFile, viewTool, grepTool, strReplaceEditor, github];
600
+ return [wikiRead, wikiWrite, wikiSearch, squadCreate, squadRecall, squadStatus, squadLogDecision, squadDelegate, squadTaskStatus, shell, fileOps, bash, readFile, viewTool, grepTool, strReplaceEditor, github];
538
601
  }
539
602
  function walkDirectory(dir, maxDepth = 3, depth = 0) {
540
603
  if (depth >= maxDepth)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "heyio",
3
- "version": "0.1.18",
3
+ "version": "0.1.20",
4
4
  "description": "IO — a personal AI assistant built on the GitHub Copilot SDK",
5
5
  "bin": {
6
6
  "io": "dist/index.js"