heyio 1.2.4 → 1.4.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/dist/api/server.js +289 -12
- package/dist/config.js +6 -0
- package/dist/copilot/agents.js +100 -5
- package/dist/copilot/ceremonies.js +12 -2
- package/dist/copilot/io-scheduler.js +9 -1
- package/dist/copilot/orchestrator.js +4 -0
- package/dist/copilot/scheduler.js +7 -2
- package/dist/copilot/skills.js +138 -6
- package/dist/copilot/squad-tools.js +102 -0
- package/dist/copilot/system-message.js +2 -1
- package/dist/copilot/token-tracker.js +89 -0
- package/dist/copilot/tools.js +27 -5
- package/dist/copilot/trigger-schedule.js +31 -0
- package/dist/paths.js +1 -0
- package/dist/store/agent-events.js +19 -0
- package/dist/store/audit-log.js +71 -0
- package/dist/store/conversations.js +150 -0
- package/dist/store/db.js +111 -0
- package/dist/store/schedules.js +9 -1
- package/dist/store/squad-colors.js +23 -0
- package/dist/store/squads.js +6 -1
- package/dist/store/tasks.js +43 -0
- package/dist/store/token-usage.js +94 -0
- package/dist/wiki/backlinks.js +51 -0
- package/dist/wiki/fs.js +63 -1
- package/dist/wiki/search.js +13 -2
- package/package.json +1 -1
- package/web-dist/assets/AuditLogView-DqxVzjd_.js +6 -0
- package/web-dist/assets/ChatView-BBopM_A3.js +1 -0
- package/web-dist/assets/FeedView-Bo4p1stx.js +6 -0
- package/web-dist/assets/HistoryView-ChTuQvXr.js +1 -0
- package/web-dist/assets/LoginView-AnOP3Mau.js +1 -0
- package/web-dist/assets/McpView-DPcihjuB.js +1 -0
- package/web-dist/assets/SchedulesView-B2o3vMm-.js +6 -0
- package/web-dist/assets/SettingsView-rtMUmH43.js +1 -0
- package/web-dist/assets/SkillsView-D_NHLk7C.js +15 -0
- package/web-dist/assets/SquadDetailView-BKXLWvwn.js +26 -0
- package/web-dist/assets/SquadHealthView-CVJiAgVW.js +11 -0
- package/web-dist/assets/SquadsView-fammrB7r.js +6 -0
- package/web-dist/assets/UsageView-Cy5Mbprb.js +16 -0
- package/web-dist/assets/WikiView-B5TOMnOg.js +36 -0
- package/web-dist/assets/arrow-left-CGMB1w_A.js +6 -0
- package/web-dist/assets/git-branch-C_Hu39uh.js +6 -0
- package/web-dist/assets/index-CQ_szaoT.css +1 -0
- package/web-dist/assets/index-CiZnRvN4.js +253 -0
- package/web-dist/assets/{plus-Cvp1w2CO.js → plus-DIBAaEMT.js} +1 -1
- package/web-dist/assets/{x-O3fBd1Cr.js → save-Chqlu7QA.js} +2 -7
- package/web-dist/assets/search-Cl8HcIsG.js +6 -0
- package/web-dist/assets/squad-colors-B8B_Y-lz.js +1 -0
- package/web-dist/assets/{trash-2-Cr3vrmL5.js → trash-2-CQSzbVIr.js} +1 -1
- package/web-dist/assets/triangle-alert-C1OjMvP5.js +6 -0
- package/web-dist/assets/x-DThJHYFm.js +6 -0
- package/web-dist/favicon.svg +9 -3
- package/web-dist/index.html +2 -2
- package/web-dist/logo.svg +10 -0
- package/web-dist/assets/ChatView-mZaaw3pd.js +0 -11
- package/web-dist/assets/FeedView-BHacQwXQ.js +0 -6
- package/web-dist/assets/LoginView-B6aSD9II.js +0 -1
- package/web-dist/assets/MarkdownContent.vue_vue_type_script_setup_true_lang-CEo_ckIb.js +0 -56
- package/web-dist/assets/McpView-BAVRUHIE.js +0 -1
- package/web-dist/assets/SchedulesView-dOd1SQiP.js +0 -1
- package/web-dist/assets/SettingsView-CCDeEsVg.js +0 -1
- package/web-dist/assets/SkillsView-gCfQ35FQ.js +0 -1
- package/web-dist/assets/SquadDetailView-CQhFfZTc.js +0 -21
- package/web-dist/assets/SquadsView-CZFxtOao.js +0 -6
- package/web-dist/assets/WikiView-B0cuUFfm.js +0 -26
- package/web-dist/assets/api-DdW5uOZf.js +0 -1
- package/web-dist/assets/index-BQdXxKfc.js +0 -138
- package/web-dist/assets/index-BbSJ0cfF.css +0 -1
|
@@ -16,9 +16,14 @@ function checkSquadSchedules() {
|
|
|
16
16
|
continue;
|
|
17
17
|
if (!isDue(schedule.cron, schedule.last_run, now))
|
|
18
18
|
continue;
|
|
19
|
+
if (!schedule.squad_id) {
|
|
20
|
+
console.warn(`[scheduler] Schedule ${schedule.id} skipped: missing squad_id.`);
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
19
23
|
updateScheduleLastRun(schedule.id);
|
|
20
|
-
const
|
|
21
|
-
|
|
24
|
+
const prompt = schedule.prompt
|
|
25
|
+
? `[Squad Schedule] Run for squad ${schedule.squad_id}. Prompt: ${schedule.prompt}`
|
|
26
|
+
: `[Squad Schedule] Run "triage" stand-up for squad ${schedule.squad_id}. Agenda: triage`;
|
|
22
27
|
sendToOrchestrator(prompt, "scheduler", (_text, done) => {
|
|
23
28
|
if (done) {
|
|
24
29
|
console.log(`[scheduler] Squad stand-up completed for ${schedule.squad_id}`);
|
package/dist/copilot/skills.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { existsSync, readdirSync, readFileSync, rmSync } from "node:fs";
|
|
2
|
-
import { join, basename } from "node:path";
|
|
3
|
-
import {
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join, basename, resolve, sep } from "node:path";
|
|
3
|
+
import { execFile } from "node:child_process";
|
|
4
4
|
import { promisify } from "node:util";
|
|
5
5
|
import { PATHS } from "../paths.js";
|
|
6
|
-
const
|
|
6
|
+
const execFileAsync = promisify(execFile);
|
|
7
7
|
export async function listSkills() {
|
|
8
8
|
if (!existsSync(PATHS.skills))
|
|
9
9
|
return [];
|
|
@@ -37,7 +37,7 @@ export async function addSkill(url) {
|
|
|
37
37
|
if (existsSync(dest)) {
|
|
38
38
|
throw new Error(`Skill "${slug}" is already installed.`);
|
|
39
39
|
}
|
|
40
|
-
await
|
|
40
|
+
await execFileAsync("git", ["clone", "--depth", "1", "--", url, dest]);
|
|
41
41
|
// Verify SKILL.md exists
|
|
42
42
|
if (!existsSync(join(dest, "SKILL.md"))) {
|
|
43
43
|
rmSync(dest, { recursive: true, force: true });
|
|
@@ -59,13 +59,35 @@ export async function getSkillContent(slug) {
|
|
|
59
59
|
return readFileSync(skillMd, "utf-8");
|
|
60
60
|
}
|
|
61
61
|
export async function updateSkillContent(slug, content) {
|
|
62
|
-
const { writeFileSync } = await import("node:fs");
|
|
63
62
|
const skillMd = join(PATHS.skills, slug, "SKILL.md");
|
|
64
63
|
if (!existsSync(join(PATHS.skills, slug))) {
|
|
65
64
|
throw new Error(`Skill "${slug}" not found.`);
|
|
66
65
|
}
|
|
67
66
|
writeFileSync(skillMd, content);
|
|
68
67
|
}
|
|
68
|
+
export async function createSkill(slug, content) {
|
|
69
|
+
const cleanSlug = slug
|
|
70
|
+
.trim()
|
|
71
|
+
.replace(/[^a-z0-9-]/gi, "-")
|
|
72
|
+
.toLowerCase()
|
|
73
|
+
.replace(/-+/g, "-")
|
|
74
|
+
.replace(/^-|-$/g, "");
|
|
75
|
+
if (!cleanSlug) {
|
|
76
|
+
throw new Error("Skill title must contain at least one alphanumeric character.");
|
|
77
|
+
}
|
|
78
|
+
// Guard against path traversal: the resolved destination must be a direct
|
|
79
|
+
// child of the skills directory (not above or beside it).
|
|
80
|
+
const skillsRoot = resolve(PATHS.skills);
|
|
81
|
+
const dest = resolve(skillsRoot, cleanSlug);
|
|
82
|
+
if (!dest.startsWith(skillsRoot + sep)) {
|
|
83
|
+
throw new Error("Invalid skill slug.");
|
|
84
|
+
}
|
|
85
|
+
if (existsSync(dest)) {
|
|
86
|
+
throw new Error(`Skill "${cleanSlug}" already exists.`);
|
|
87
|
+
}
|
|
88
|
+
mkdirSync(dest, { recursive: true });
|
|
89
|
+
writeFileSync(join(dest, "SKILL.md"), content);
|
|
90
|
+
}
|
|
69
91
|
export async function loadSkillDirectories() {
|
|
70
92
|
if (!existsSync(PATHS.skills))
|
|
71
93
|
return [];
|
|
@@ -74,4 +96,114 @@ export async function loadSkillDirectories() {
|
|
|
74
96
|
.filter((e) => e.isDirectory() && existsSync(join(PATHS.skills, e.name, "SKILL.md")))
|
|
75
97
|
.map((e) => join(PATHS.skills, e.name));
|
|
76
98
|
}
|
|
99
|
+
const DISCOVERY_CACHE = new Map();
|
|
100
|
+
const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
|
|
101
|
+
/** Validate and return a safe slug, throwing if it contains unsafe characters. */
|
|
102
|
+
function validateSlug(slug) {
|
|
103
|
+
if (!slug || !/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/i.test(slug)) {
|
|
104
|
+
throw new Error("Invalid skill slug: must contain only letters, digits, and hyphens.");
|
|
105
|
+
}
|
|
106
|
+
return slug;
|
|
107
|
+
}
|
|
108
|
+
function parseAwesomeCopilotTable(markdown) {
|
|
109
|
+
const skills = [];
|
|
110
|
+
for (const line of markdown.split("\n")) {
|
|
111
|
+
if (!line.startsWith("|"))
|
|
112
|
+
continue;
|
|
113
|
+
const cells = line
|
|
114
|
+
.split("|")
|
|
115
|
+
.map((c) => c.trim())
|
|
116
|
+
.filter(Boolean);
|
|
117
|
+
if (cells.length < 2)
|
|
118
|
+
continue;
|
|
119
|
+
// First cell contains [slug](../skills/slug/SKILL.md)
|
|
120
|
+
const slugMatch = cells[0].match(/^\[([^\]]+)\]/);
|
|
121
|
+
if (!slugMatch)
|
|
122
|
+
continue;
|
|
123
|
+
const slug = slugMatch[1];
|
|
124
|
+
if (slug === "Name")
|
|
125
|
+
continue; // header row
|
|
126
|
+
// Second cell is the description (may contain <br /> markup)
|
|
127
|
+
const description = cells[1]
|
|
128
|
+
.replace(/<br\s*\/?>/gi, " ")
|
|
129
|
+
.replace(/\*\*([^*]+)\*\*/g, "$1")
|
|
130
|
+
.trim();
|
|
131
|
+
if (!description || description === "Description")
|
|
132
|
+
continue;
|
|
133
|
+
skills.push({ slug, name: slug, description, source: "awesome-copilot" });
|
|
134
|
+
}
|
|
135
|
+
return skills;
|
|
136
|
+
}
|
|
137
|
+
async function fetchAwesomeCopilotSkills() {
|
|
138
|
+
const url = "https://raw.githubusercontent.com/github/awesome-copilot/main/docs/README.skills.md";
|
|
139
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(15_000) });
|
|
140
|
+
if (!res.ok)
|
|
141
|
+
throw new Error(`Failed to fetch awesome-copilot skills list: HTTP ${res.status}`);
|
|
142
|
+
const text = await res.text();
|
|
143
|
+
return parseAwesomeCopilotTable(text);
|
|
144
|
+
}
|
|
145
|
+
async function fetchSkillsShSkills() {
|
|
146
|
+
try {
|
|
147
|
+
const res = await fetch("https://skills.sh/api/skills", {
|
|
148
|
+
headers: { Accept: "application/json" },
|
|
149
|
+
signal: AbortSignal.timeout(10_000),
|
|
150
|
+
});
|
|
151
|
+
if (!res.ok)
|
|
152
|
+
return [];
|
|
153
|
+
const data = (await res.json());
|
|
154
|
+
return data.map((item) => ({
|
|
155
|
+
slug: item.slug ?? item.name ?? "",
|
|
156
|
+
name: item.name ?? item.slug ?? "",
|
|
157
|
+
description: item.description ?? "",
|
|
158
|
+
source: "skillssh",
|
|
159
|
+
}));
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
return [];
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
export async function discoverSkills(source, query) {
|
|
166
|
+
const cached = DISCOVERY_CACHE.get(source);
|
|
167
|
+
let skills;
|
|
168
|
+
if (cached && Date.now() - cached.fetchedAt < CACHE_TTL_MS) {
|
|
169
|
+
skills = cached.skills;
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
skills =
|
|
173
|
+
source === "awesome-copilot"
|
|
174
|
+
? await fetchAwesomeCopilotSkills()
|
|
175
|
+
: await fetchSkillsShSkills();
|
|
176
|
+
DISCOVERY_CACHE.set(source, { skills, fetchedAt: Date.now() });
|
|
177
|
+
}
|
|
178
|
+
if (query) {
|
|
179
|
+
const q = query.toLowerCase();
|
|
180
|
+
skills = skills.filter((s) => s.slug.toLowerCase().includes(q) || s.description.toLowerCase().includes(q));
|
|
181
|
+
}
|
|
182
|
+
return skills;
|
|
183
|
+
}
|
|
184
|
+
function remoteSkillMdUrl(source, safeSlug) {
|
|
185
|
+
const encodedSlug = encodeURIComponent(safeSlug);
|
|
186
|
+
if (source === "awesome-copilot") {
|
|
187
|
+
return `https://raw.githubusercontent.com/github/awesome-copilot/main/skills/${encodedSlug}/SKILL.md`;
|
|
188
|
+
}
|
|
189
|
+
return `https://skills.sh/skills/${encodedSlug}/SKILL.md`;
|
|
190
|
+
}
|
|
191
|
+
export async function fetchRemoteSkillPreview(source, slug) {
|
|
192
|
+
const safeSlug = validateSlug(slug);
|
|
193
|
+
const url = remoteSkillMdUrl(source, safeSlug);
|
|
194
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(10_000) });
|
|
195
|
+
if (!res.ok)
|
|
196
|
+
throw new Error(`Failed to fetch preview for "${safeSlug}": HTTP ${res.status}`);
|
|
197
|
+
return res.text();
|
|
198
|
+
}
|
|
199
|
+
export async function installFromSource(source, slug) {
|
|
200
|
+
const safeSlug = validateSlug(slug);
|
|
201
|
+
const dest = join(PATHS.skills, safeSlug);
|
|
202
|
+
if (existsSync(dest)) {
|
|
203
|
+
throw new Error(`Skill "${safeSlug}" is already installed.`);
|
|
204
|
+
}
|
|
205
|
+
const content = await fetchRemoteSkillPreview(source, safeSlug);
|
|
206
|
+
mkdirSync(dest, { recursive: true });
|
|
207
|
+
writeFileSync(join(dest, "SKILL.md"), content);
|
|
208
|
+
}
|
|
77
209
|
//# sourceMappingURL=skills.js.map
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { defineTool } from "@github/copilot-sdk";
|
|
3
|
+
/**
|
|
4
|
+
* Creates a scoped set of tools for squad agent sessions.
|
|
5
|
+
* Wiki tools are sandboxed to the squad's own wiki subfolder.
|
|
6
|
+
* Feed posts are locked to the squad's source identifier.
|
|
7
|
+
*/
|
|
8
|
+
export function createSquadTools(squadSlug, squadId) {
|
|
9
|
+
const wikiPrefix = `squads/${squadSlug}`;
|
|
10
|
+
return [
|
|
11
|
+
// --- Wiki Tools (scoped to squads/{slug}/) ---
|
|
12
|
+
defineTool("wiki_read", {
|
|
13
|
+
description: `Read a wiki page from the squad wiki (paths are relative to your squad's wiki folder)`,
|
|
14
|
+
parameters: z.object({
|
|
15
|
+
path: z.string().describe("Page path (e.g., 'decisions.md', 'notes/architecture.md')"),
|
|
16
|
+
}),
|
|
17
|
+
handler: async ({ path }) => {
|
|
18
|
+
const { readPage } = await import("../wiki/fs.js");
|
|
19
|
+
return await readPage(`${wikiPrefix}/${path}`);
|
|
20
|
+
},
|
|
21
|
+
}),
|
|
22
|
+
defineTool("wiki_write", {
|
|
23
|
+
description: "Write or update a wiki page in the squad wiki",
|
|
24
|
+
parameters: z.object({
|
|
25
|
+
path: z.string().describe("Page path (e.g., 'decisions.md')"),
|
|
26
|
+
content: z.string().describe("Markdown content to write"),
|
|
27
|
+
}),
|
|
28
|
+
handler: async ({ path, content }) => {
|
|
29
|
+
const { writePage } = await import("../wiki/fs.js");
|
|
30
|
+
await writePage(`${wikiPrefix}/${path}`, content);
|
|
31
|
+
return `Page saved: ${path}`;
|
|
32
|
+
},
|
|
33
|
+
}),
|
|
34
|
+
defineTool("wiki_list", {
|
|
35
|
+
description: "List all wiki pages in the squad wiki",
|
|
36
|
+
parameters: z.object({}),
|
|
37
|
+
handler: async () => {
|
|
38
|
+
const { listPages } = await import("../wiki/fs.js");
|
|
39
|
+
return await listPages(wikiPrefix);
|
|
40
|
+
},
|
|
41
|
+
}),
|
|
42
|
+
defineTool("wiki_search", {
|
|
43
|
+
description: "Search squad wiki pages by keyword",
|
|
44
|
+
parameters: z.object({
|
|
45
|
+
query: z.string().describe("Search query"),
|
|
46
|
+
}),
|
|
47
|
+
handler: async ({ query }) => {
|
|
48
|
+
const { searchSquadPages } = await import("../wiki/search.js");
|
|
49
|
+
return await searchSquadPages(query, wikiPrefix);
|
|
50
|
+
},
|
|
51
|
+
}),
|
|
52
|
+
defineTool("wiki_delete", {
|
|
53
|
+
description: "Delete a wiki page from the squad wiki",
|
|
54
|
+
parameters: z.object({
|
|
55
|
+
path: z.string().describe("Page path to delete"),
|
|
56
|
+
}),
|
|
57
|
+
handler: async ({ path }) => {
|
|
58
|
+
const { deletePage } = await import("../wiki/fs.js");
|
|
59
|
+
await deletePage(`${wikiPrefix}/${path}`);
|
|
60
|
+
return `Page deleted: ${path}`;
|
|
61
|
+
},
|
|
62
|
+
}),
|
|
63
|
+
defineTool("wiki_backlinks", {
|
|
64
|
+
description: "Find all squad wiki pages that link to the given page",
|
|
65
|
+
parameters: z.object({
|
|
66
|
+
path: z.string().describe("Page path (e.g., 'decisions.md')"),
|
|
67
|
+
}),
|
|
68
|
+
handler: async ({ path }) => {
|
|
69
|
+
const { getBacklinks } = await import("../wiki/backlinks.js");
|
|
70
|
+
const allBacklinks = await getBacklinks(`${wikiPrefix}/${path}`);
|
|
71
|
+
// Filter to only show backlinks within squad wiki, strip prefix for display
|
|
72
|
+
return allBacklinks
|
|
73
|
+
.filter((bl) => bl.startsWith(`${wikiPrefix}/`))
|
|
74
|
+
.map((bl) => bl.slice(wikiPrefix.length + 1));
|
|
75
|
+
},
|
|
76
|
+
}),
|
|
77
|
+
// --- Feed Tools ---
|
|
78
|
+
defineTool("feed_post", {
|
|
79
|
+
description: "Post a message or deliverable to the user's inbox. Use for progress updates, questions, blockers, or completed work.",
|
|
80
|
+
parameters: z.object({
|
|
81
|
+
title: z.string().describe("Title of the message"),
|
|
82
|
+
content: z.string().describe("Full content (markdown supported)"),
|
|
83
|
+
}),
|
|
84
|
+
handler: async ({ title, content }) => {
|
|
85
|
+
const { postFeedItem } = await import("../store/feed.js");
|
|
86
|
+
const source = `squad-${squadSlug}`;
|
|
87
|
+
const item = postFeedItem(source, title, content);
|
|
88
|
+
return `Posted to inbox: "${title}" (ID: ${item.id})`;
|
|
89
|
+
},
|
|
90
|
+
}),
|
|
91
|
+
// --- Task Tools ---
|
|
92
|
+
defineTool("squad_task_status", {
|
|
93
|
+
description: "Check the status of tasks for your squad",
|
|
94
|
+
parameters: z.object({}),
|
|
95
|
+
handler: async () => {
|
|
96
|
+
const { getTasksForSquad } = await import("../store/tasks.js");
|
|
97
|
+
return getTasksForSquad(squadId);
|
|
98
|
+
},
|
|
99
|
+
}),
|
|
100
|
+
];
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=squad-tools.js.map
|
|
@@ -13,7 +13,8 @@ You are IO, a personal AI assistant daemon. You run 24/7 on the user's machine,
|
|
|
13
13
|
## Core Capabilities
|
|
14
14
|
- Manage project squads (teams of AI agents themed after pop culture universes)
|
|
15
15
|
- Read and write to a persistent wiki knowledge base at ~/.io/wiki/
|
|
16
|
-
- Each squad has its own wiki at ~/.io/wiki/squads/{squad-slug}/ (use the slug, never the UUID)
|
|
16
|
+
- Each squad has its own wiki at ~/.io/wiki/pages/squads/{squad-slug}/ (use the slug, never the UUID)
|
|
17
|
+
- Squad wiki templates at ~/.io/wiki/templates/squad/ are auto-copied to new squads on creation
|
|
17
18
|
- Delegate complex tasks to squad team leads
|
|
18
19
|
- Track deliverables in a unified feed
|
|
19
20
|
- Schedule recurring tasks and stand-ups
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { loadConfig } from "../config.js";
|
|
2
|
+
import { recordTokenUsage } from "../store/token-usage.js";
|
|
3
|
+
import { postFeedItem } from "../store/feed.js";
|
|
4
|
+
/**
|
|
5
|
+
* Default model pricing (USD per 1M tokens).
|
|
6
|
+
* Used when modelPricing is not configured.
|
|
7
|
+
*/
|
|
8
|
+
export const DEFAULT_MODEL_PRICING = {
|
|
9
|
+
"gpt-4.1": { inputPer1M: 2.0, outputPer1M: 8.0 },
|
|
10
|
+
"gpt-5.2": { inputPer1M: 2.5, outputPer1M: 10.0 },
|
|
11
|
+
"gpt-5.2-codex": { inputPer1M: 3.0, outputPer1M: 12.0 },
|
|
12
|
+
"gpt-5.3-codex": { inputPer1M: 3.0, outputPer1M: 12.0 },
|
|
13
|
+
"gpt-5.4": { inputPer1M: 5.0, outputPer1M: 20.0 },
|
|
14
|
+
"gpt-5.5": { inputPer1M: 7.5, outputPer1M: 30.0 },
|
|
15
|
+
"gpt-5-mini": { inputPer1M: 0.15, outputPer1M: 0.60 },
|
|
16
|
+
"gpt-5.4-mini": { inputPer1M: 0.15, outputPer1M: 0.60 },
|
|
17
|
+
"claude-haiku-4.5": { inputPer1M: 0.80, outputPer1M: 4.0 },
|
|
18
|
+
"claude-sonnet-4.5": { inputPer1M: 3.0, outputPer1M: 15.0 },
|
|
19
|
+
"claude-sonnet-4.6": { inputPer1M: 3.0, outputPer1M: 15.0 },
|
|
20
|
+
"claude-opus-4.5": { inputPer1M: 15.0, outputPer1M: 75.0 },
|
|
21
|
+
"claude-opus-4.6": { inputPer1M: 15.0, outputPer1M: 75.0 },
|
|
22
|
+
"claude-opus-4.7": { inputPer1M: 15.0, outputPer1M: 75.0 },
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Compute estimated cost in USD for the given token counts and model.
|
|
26
|
+
*/
|
|
27
|
+
export function estimateCost(model, inputTokens, outputTokens) {
|
|
28
|
+
const config = loadConfig();
|
|
29
|
+
const pricing = config.modelPricing?.[model] ?? DEFAULT_MODEL_PRICING[model];
|
|
30
|
+
if (!pricing)
|
|
31
|
+
return 0;
|
|
32
|
+
return (inputTokens / 1_000_000) * pricing.inputPer1M + (outputTokens / 1_000_000) * pricing.outputPer1M;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Attach a token usage listener to a CopilotSession.
|
|
36
|
+
* Returns a `flush` function that, when called, persists accumulated
|
|
37
|
+
* token usage to the database and returns the totals.
|
|
38
|
+
*
|
|
39
|
+
* Call `flush` after the session ends (in a finally block).
|
|
40
|
+
*/
|
|
41
|
+
export function attachTokenTracker(session, context) {
|
|
42
|
+
const accumulator = {};
|
|
43
|
+
const unsubscribe = session.on("assistant.usage", (event) => {
|
|
44
|
+
const data = event?.data;
|
|
45
|
+
if (!data?.model)
|
|
46
|
+
return;
|
|
47
|
+
const model = data.model;
|
|
48
|
+
const input = data.inputTokens ?? 0;
|
|
49
|
+
const output = data.outputTokens ?? 0;
|
|
50
|
+
if (!accumulator[model])
|
|
51
|
+
accumulator[model] = { inputTokens: 0, outputTokens: 0 };
|
|
52
|
+
accumulator[model].inputTokens += input;
|
|
53
|
+
accumulator[model].outputTokens += output;
|
|
54
|
+
});
|
|
55
|
+
return () => {
|
|
56
|
+
unsubscribe();
|
|
57
|
+
let totalInputTokens = 0;
|
|
58
|
+
let totalOutputTokens = 0;
|
|
59
|
+
let totalCostUsd = 0;
|
|
60
|
+
for (const [model, usage] of Object.entries(accumulator)) {
|
|
61
|
+
if (usage.inputTokens === 0 && usage.outputTokens === 0)
|
|
62
|
+
continue;
|
|
63
|
+
const costUsd = estimateCost(model, usage.inputTokens, usage.outputTokens);
|
|
64
|
+
recordTokenUsage({
|
|
65
|
+
squadId: context.squadId,
|
|
66
|
+
agentId: context.agentId,
|
|
67
|
+
taskId: context.taskId,
|
|
68
|
+
model,
|
|
69
|
+
inputTokens: usage.inputTokens,
|
|
70
|
+
outputTokens: usage.outputTokens,
|
|
71
|
+
costUsd,
|
|
72
|
+
});
|
|
73
|
+
totalInputTokens += usage.inputTokens;
|
|
74
|
+
totalOutputTokens += usage.outputTokens;
|
|
75
|
+
totalCostUsd += costUsd;
|
|
76
|
+
}
|
|
77
|
+
// Alert on runaway agents
|
|
78
|
+
const config = loadConfig();
|
|
79
|
+
const threshold = config.tokenAlertThreshold;
|
|
80
|
+
if (threshold && (totalInputTokens + totalOutputTokens) > threshold) {
|
|
81
|
+
const source = context.squadId ? `squad-${context.squadId}` : "system";
|
|
82
|
+
postFeedItem(source, "⚠️ Token usage alert", `A task consumed ${totalInputTokens + totalOutputTokens} tokens (threshold: ${threshold}). ` +
|
|
83
|
+
`Estimated cost: $${totalCostUsd.toFixed(4)}. ` +
|
|
84
|
+
(context.taskId ? `Task ID: ${context.taskId}` : ""));
|
|
85
|
+
}
|
|
86
|
+
return { totalInputTokens, totalOutputTokens, totalCostUsd };
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=token-tracker.js.map
|
package/dist/copilot/tools.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { defineTool } from "@github/copilot-sdk";
|
|
3
|
+
import { addAuditEntry } from "../store/audit-log.js";
|
|
3
4
|
export function createTools() {
|
|
4
5
|
return [
|
|
5
6
|
// --- Wiki Tools ---
|
|
@@ -54,6 +55,16 @@ export function createTools() {
|
|
|
54
55
|
return `Page deleted: ${path}`;
|
|
55
56
|
},
|
|
56
57
|
}),
|
|
58
|
+
defineTool("wiki_backlinks", {
|
|
59
|
+
description: "Find all wiki pages that link to the given page",
|
|
60
|
+
parameters: z.object({
|
|
61
|
+
path: z.string().describe("Page path relative to pages/ (e.g., 'notes/todo.md')"),
|
|
62
|
+
}),
|
|
63
|
+
handler: async ({ path }) => {
|
|
64
|
+
const { getBacklinks } = await import("../wiki/backlinks.js");
|
|
65
|
+
return await getBacklinks(path);
|
|
66
|
+
},
|
|
67
|
+
}),
|
|
57
68
|
// --- Squad Tools ---
|
|
58
69
|
defineTool("squad_create", {
|
|
59
70
|
description: "Create a new project squad. Research the chosen universe to assign character names — never hardcode.",
|
|
@@ -68,6 +79,9 @@ export function createTools() {
|
|
|
68
79
|
const { createSquad } = await import("../store/squads.js");
|
|
69
80
|
const squad = createSquad(name, universe, repo_url);
|
|
70
81
|
let cloneMsg = "";
|
|
82
|
+
// Copy squad wiki templates into the new squad's wiki directory
|
|
83
|
+
const { copySquadTemplates } = await import("../wiki/fs.js");
|
|
84
|
+
await copySquadTemplates(squad.slug);
|
|
71
85
|
if (repo_url) {
|
|
72
86
|
const { exec } = await import("node:child_process");
|
|
73
87
|
const { promisify } = await import("node:util");
|
|
@@ -99,7 +113,9 @@ export function createTools() {
|
|
|
99
113
|
}
|
|
100
114
|
}
|
|
101
115
|
}
|
|
102
|
-
|
|
116
|
+
const msg = `Squad "${name}" created with universe "${universe}". ID: ${squad.id}, Slug: ${squad.slug}. Wiki path: ~/.io/wiki/squads/${squad.slug}/${cloneMsg}`;
|
|
117
|
+
addAuditEntry("squad_created", `Squad "${name}" created (universe: ${universe})`, { squad_id: squad.id, name, universe, repo_url }, { squad_id: squad.id });
|
|
118
|
+
return msg;
|
|
103
119
|
},
|
|
104
120
|
}),
|
|
105
121
|
defineTool("squad_add_agent", {
|
|
@@ -143,6 +159,7 @@ export function createTools() {
|
|
|
143
159
|
}),
|
|
144
160
|
handler: async ({ squad_id, task, instance_id }) => {
|
|
145
161
|
const { delegateTask } = await import("./agents.js");
|
|
162
|
+
addAuditEntry("task_delegated", `Task delegated to squad ${squad_id}: ${task.slice(0, 200)}`, { squad_id, task: task.slice(0, 1000), instance_id }, { squad_id });
|
|
146
163
|
const result = await delegateTask(squad_id, task, instance_id);
|
|
147
164
|
return result;
|
|
148
165
|
},
|
|
@@ -158,6 +175,7 @@ export function createTools() {
|
|
|
158
175
|
}),
|
|
159
176
|
handler: async ({ squad_id, task, execute_after }) => {
|
|
160
177
|
const { squadMeeting } = await import("./ceremonies.js");
|
|
178
|
+
addAuditEntry("squad_meeting", `Planning meeting started for squad ${squad_id}: ${task.slice(0, 200)}`, { squad_id, task: task.slice(0, 1000), execute_after }, { squad_id });
|
|
161
179
|
return await squadMeeting(squad_id, task, execute_after);
|
|
162
180
|
},
|
|
163
181
|
}),
|
|
@@ -257,12 +275,12 @@ export function createTools() {
|
|
|
257
275
|
parameters: z.object({
|
|
258
276
|
type: z.enum(["squad", "io"]).describe("Schedule type"),
|
|
259
277
|
cron: z.string().describe("Cron expression (e.g., '0 9 * * 1-5')"),
|
|
260
|
-
squad_id: z.string().
|
|
278
|
+
squad_id: z.string().describe("Target squad ID"),
|
|
261
279
|
agenda: z
|
|
262
280
|
.string()
|
|
263
281
|
.optional()
|
|
264
282
|
.describe("Agenda type for squad schedules (triage, prioritize, ideation, or custom)"),
|
|
265
|
-
prompt: z.string().optional().describe("Prompt text for
|
|
283
|
+
prompt: z.string().optional().describe("Prompt text for the schedule"),
|
|
266
284
|
}),
|
|
267
285
|
handler: async ({ type, cron, squad_id, agenda, prompt }) => {
|
|
268
286
|
const { createSchedule } = await import("../store/schedules.js");
|
|
@@ -325,12 +343,16 @@ export function createTools() {
|
|
|
325
343
|
maxBuffer: 1024 * 1024,
|
|
326
344
|
env: { ...process.env, GH_PROMPT_DISABLED: "1" },
|
|
327
345
|
});
|
|
328
|
-
|
|
346
|
+
const output = stdout.trim() || "(no output)";
|
|
347
|
+
addAuditEntry("shell_command", `Command: ${command.slice(0, 200)}`, { command, cwd, output: output.slice(0, 500), exit_code: 0 });
|
|
348
|
+
return output;
|
|
329
349
|
}
|
|
330
350
|
catch (err) {
|
|
331
351
|
const stderr = err.stderr?.toString().trim() ?? "";
|
|
332
352
|
const stdout = err.stdout?.toString().trim() ?? "";
|
|
333
|
-
|
|
353
|
+
const output = `Error (exit ${err.code}): ${stderr || stdout || err.message}`;
|
|
354
|
+
addAuditEntry("shell_command", `Command: ${command.slice(0, 200)}`, { command, cwd, output: output.slice(0, 500), exit_code: err.code ?? 1 });
|
|
355
|
+
return output;
|
|
334
356
|
}
|
|
335
357
|
},
|
|
336
358
|
}),
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { getSchedule, updateScheduleLastRun } from "../store/schedules.js";
|
|
2
|
+
import { sendToOrchestrator } from "./orchestrator.js";
|
|
3
|
+
import { buildSquadScopedPrompt } from "./io-scheduler.js";
|
|
4
|
+
/**
|
|
5
|
+
* Trigger a schedule immediately, bypassing cron timing.
|
|
6
|
+
* Returns the schedule if triggered successfully, or undefined if not found.
|
|
7
|
+
*/
|
|
8
|
+
export function triggerSchedule(id) {
|
|
9
|
+
const schedule = getSchedule(id);
|
|
10
|
+
if (!schedule)
|
|
11
|
+
return undefined;
|
|
12
|
+
updateScheduleLastRun(schedule.id);
|
|
13
|
+
if (schedule.type === "squad") {
|
|
14
|
+
const agenda = schedule.agenda || "triage";
|
|
15
|
+
const prompt = `[Squad Schedule] Run "${agenda}" stand-up for squad ${schedule.squad_id}. Agenda: ${agenda}`;
|
|
16
|
+
sendToOrchestrator(prompt, "scheduler", (_text, done) => {
|
|
17
|
+
if (done) {
|
|
18
|
+
console.log(`[trigger] Squad stand-up completed for ${schedule.squad_id}`);
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
sendToOrchestrator(buildSquadScopedPrompt(schedule), "io-scheduler", (_text, done) => {
|
|
24
|
+
if (done) {
|
|
25
|
+
console.log(`[trigger] IO schedule ${schedule.id} completed.`);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
return schedule;
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=trigger-schedule.js.map
|
package/dist/paths.js
CHANGED
|
@@ -7,6 +7,7 @@ export const PATHS = {
|
|
|
7
7
|
db: join(IO_HOME, "io.db"),
|
|
8
8
|
wiki: join(IO_HOME, "wiki"),
|
|
9
9
|
wikiPages: join(IO_HOME, "wiki", "pages"),
|
|
10
|
+
wikiSquadTemplates: join(IO_HOME, "wiki", "templates", "squad"),
|
|
10
11
|
skills: join(IO_HOME, "skills"),
|
|
11
12
|
mcpConfig: join(IO_HOME, "mcp.json"),
|
|
12
13
|
sessions: join(IO_HOME, "sessions"),
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { getDb } from "./db.js";
|
|
3
|
+
export function addAgentEvent(taskId, type, summary, payload) {
|
|
4
|
+
const db = getDb();
|
|
5
|
+
const id = randomUUID();
|
|
6
|
+
db.prepare("INSERT INTO agent_events (id, task_id, type, summary, payload) VALUES (?, ?, ?, ?, ?)").run(id, taskId, type, summary, JSON.stringify(payload));
|
|
7
|
+
return db.prepare("SELECT * FROM agent_events WHERE id = ?").get(id);
|
|
8
|
+
}
|
|
9
|
+
export function getAgentEvents(taskId) {
|
|
10
|
+
const db = getDb();
|
|
11
|
+
return db
|
|
12
|
+
.prepare("SELECT * FROM agent_events WHERE task_id = ? ORDER BY created_at ASC")
|
|
13
|
+
.all(taskId);
|
|
14
|
+
}
|
|
15
|
+
export function clearAgentEvents(taskId) {
|
|
16
|
+
const db = getDb();
|
|
17
|
+
db.prepare("DELETE FROM agent_events WHERE task_id = ?").run(taskId);
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=agent-events.js.map
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { getDb } from "./db.js";
|
|
3
|
+
export function addAuditEntry(action_type, summary, payload, opts) {
|
|
4
|
+
const db = getDb();
|
|
5
|
+
const id = randomUUID();
|
|
6
|
+
db.prepare(`INSERT INTO audit_log (id, squad_id, agent_id, task_id, action_type, summary, payload)
|
|
7
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`).run(id, opts?.squad_id ?? null, opts?.agent_id ?? null, opts?.task_id ?? null, action_type, summary, JSON.stringify(payload));
|
|
8
|
+
return db.prepare("SELECT * FROM audit_log WHERE id = ?").get(id);
|
|
9
|
+
}
|
|
10
|
+
export function getAuditLog(filters = {}) {
|
|
11
|
+
const db = getDb();
|
|
12
|
+
const conditions = [];
|
|
13
|
+
const params = [];
|
|
14
|
+
if (filters.squad_id) {
|
|
15
|
+
conditions.push("squad_id = ?");
|
|
16
|
+
params.push(filters.squad_id);
|
|
17
|
+
}
|
|
18
|
+
if (filters.agent_id) {
|
|
19
|
+
conditions.push("agent_id = ?");
|
|
20
|
+
params.push(filters.agent_id);
|
|
21
|
+
}
|
|
22
|
+
if (filters.action_type) {
|
|
23
|
+
conditions.push("action_type = ?");
|
|
24
|
+
params.push(filters.action_type);
|
|
25
|
+
}
|
|
26
|
+
if (filters.from) {
|
|
27
|
+
conditions.push("created_at >= ?");
|
|
28
|
+
params.push(filters.from);
|
|
29
|
+
}
|
|
30
|
+
if (filters.to) {
|
|
31
|
+
conditions.push("created_at <= ?");
|
|
32
|
+
params.push(filters.to);
|
|
33
|
+
}
|
|
34
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
35
|
+
const limit = filters.limit ?? 50;
|
|
36
|
+
const offset = filters.offset ?? 0;
|
|
37
|
+
return db
|
|
38
|
+
.prepare(`SELECT * FROM audit_log ${where} ORDER BY created_at DESC LIMIT ? OFFSET ?`)
|
|
39
|
+
.all(...params, limit, offset);
|
|
40
|
+
}
|
|
41
|
+
export function countAuditLog(filters = {}) {
|
|
42
|
+
const db = getDb();
|
|
43
|
+
const conditions = [];
|
|
44
|
+
const params = [];
|
|
45
|
+
if (filters.squad_id) {
|
|
46
|
+
conditions.push("squad_id = ?");
|
|
47
|
+
params.push(filters.squad_id);
|
|
48
|
+
}
|
|
49
|
+
if (filters.agent_id) {
|
|
50
|
+
conditions.push("agent_id = ?");
|
|
51
|
+
params.push(filters.agent_id);
|
|
52
|
+
}
|
|
53
|
+
if (filters.action_type) {
|
|
54
|
+
conditions.push("action_type = ?");
|
|
55
|
+
params.push(filters.action_type);
|
|
56
|
+
}
|
|
57
|
+
if (filters.from) {
|
|
58
|
+
conditions.push("created_at >= ?");
|
|
59
|
+
params.push(filters.from);
|
|
60
|
+
}
|
|
61
|
+
if (filters.to) {
|
|
62
|
+
conditions.push("created_at <= ?");
|
|
63
|
+
params.push(filters.to);
|
|
64
|
+
}
|
|
65
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
66
|
+
const row = db
|
|
67
|
+
.prepare(`SELECT COUNT(*) as count FROM audit_log ${where}`)
|
|
68
|
+
.get(...params);
|
|
69
|
+
return row.count;
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=audit-log.js.map
|