heyio 1.2.3 → 1.3.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 +267 -12
- package/dist/config.js +6 -0
- package/dist/copilot/agents.js +61 -4
- package/dist/copilot/ceremonies.js +12 -2
- package/dist/copilot/io-scheduler.js +9 -1
- package/dist/copilot/orchestrator.js +2 -0
- package/dist/copilot/scheduler.js +4 -0
- package/dist/copilot/skills.js +138 -6
- package/dist/copilot/system-message.js +12 -0
- package/dist/copilot/token-tracker.js +89 -0
- package/dist/copilot/tools.js +27 -5
- 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 +5 -1
- package/dist/store/squad-colors.js +21 -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/package.json +1 -1
- package/web-dist/assets/AuditLogView-xgSZ2MOJ.js +6 -0
- package/web-dist/assets/ChatView-BU3Jvu5y.js +11 -0
- package/web-dist/assets/FeedView-BwkWbe1p.js +6 -0
- package/web-dist/assets/HistoryView-Doh9Y3Na.js +1 -0
- package/web-dist/assets/LoginView-CoTEOrwE.js +1 -0
- package/web-dist/assets/{MarkdownContent.vue_vue_type_script_setup_true_lang-CEo_ckIb.js → MarkdownContent.vue_vue_type_script_setup_true_lang-CObjuCHH.js} +1 -1
- package/web-dist/assets/McpView-ByXoAnED.js +1 -0
- package/web-dist/assets/SchedulesView-BkUdRYwk.js +1 -0
- package/web-dist/assets/SettingsView-_q-IpzFy.js +1 -0
- package/web-dist/assets/SkillsView-_FkOdD2U.js +15 -0
- package/web-dist/assets/SquadDetailView-CV6_n_If.js +31 -0
- package/web-dist/assets/SquadHealthView-DmQqPq7H.js +11 -0
- package/web-dist/assets/SquadsView-Dkhtu5MQ.js +6 -0
- package/web-dist/assets/UsageView-CCS6pp6n.js +16 -0
- package/web-dist/assets/WikiView-CpXzff_L.js +31 -0
- package/web-dist/assets/api-CaqVk-rG.js +1 -0
- package/web-dist/assets/arrow-left-CkDjCT7Z.js +6 -0
- package/web-dist/assets/git-branch-Bu9s__XL.js +6 -0
- package/web-dist/assets/index-D3DNfwXI.css +1 -0
- package/web-dist/assets/{index-BQdXxKfc.js → index-DfdD_qE4.js} +56 -36
- package/web-dist/assets/{plus-Cvp1w2CO.js → plus-GvGwcjX5.js} +1 -1
- package/web-dist/assets/{x-O3fBd1Cr.js → save-fQ_rr5hX.js} +2 -7
- package/web-dist/assets/search-C3fxUixl.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-Ba_1SAua.js} +1 -1
- package/web-dist/assets/triangle-alert-BTBlX3kg.js +6 -0
- package/web-dist/assets/x-CJifAZQa.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/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-BbSJ0cfF.css +0 -1
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
|
|
@@ -31,6 +31,18 @@ You are IO, a personal AI assistant daemon. You run 24/7 on the user's machine,
|
|
|
31
31
|
- If the user says "do this" for a complex task → use squad_meeting with execute_after=true
|
|
32
32
|
- If the user says "just do it" or it's a simple task → use squad_delegate directly
|
|
33
33
|
|
|
34
|
+
## HARD RULE: Squad Ownership Boundary
|
|
35
|
+
If a project has a squad assigned to it, you (the orchestrator) must NEVER:
|
|
36
|
+
- Research, analyze, or investigate the project's code, issues, or state yourself
|
|
37
|
+
- Attempt any work — even preliminary analysis — before delegating
|
|
38
|
+
- "Look into" or "check on" something before passing it to the squad
|
|
39
|
+
|
|
40
|
+
When a request comes in about a squad-owned project, you IMMEDIATELY delegate to that squad's team lead with no pre-processing. The squad handles ALL work including research, analysis, planning, and execution.
|
|
41
|
+
|
|
42
|
+
The ONLY thing you are allowed to do regarding a squad-owned project (without delegating) is:
|
|
43
|
+
- Answer questions about what the squad has already done (using feed/task history)
|
|
44
|
+
- Report squad status, task progress, or past deliverables
|
|
45
|
+
|
|
34
46
|
## Squad Coverage Requirements
|
|
35
47
|
Every squad MUST have:
|
|
36
48
|
1. A dedicated team lead (PM/Senior Engineer, coordination-only — **never writes code**)
|
|
@@ -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
|
}),
|
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
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { getDb } from "./db.js";
|
|
3
|
+
function toMessage(row) {
|
|
4
|
+
return {
|
|
5
|
+
id: row.id,
|
|
6
|
+
conversationId: row.conversation_id,
|
|
7
|
+
role: row.role,
|
|
8
|
+
content: row.content,
|
|
9
|
+
source: row.source,
|
|
10
|
+
createdAt: row.created_at,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
function toSummary(row) {
|
|
14
|
+
return {
|
|
15
|
+
id: row.id,
|
|
16
|
+
preview: row.preview,
|
|
17
|
+
messageCount: row.message_count,
|
|
18
|
+
startedAt: row.started_at,
|
|
19
|
+
updatedAt: row.updated_at,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export function saveMessage(conversationId, role, content, source) {
|
|
23
|
+
const db = getDb();
|
|
24
|
+
const id = randomUUID();
|
|
25
|
+
db.prepare("INSERT INTO conversation_messages (id, conversation_id, role, content, source) VALUES (?, ?, ?, ?, ?)").run(id, conversationId, role, content, source);
|
|
26
|
+
const row = db
|
|
27
|
+
.prepare("SELECT * FROM conversation_messages WHERE id = ?")
|
|
28
|
+
.get(id);
|
|
29
|
+
return toMessage(row);
|
|
30
|
+
}
|
|
31
|
+
export function getConversation(conversationId) {
|
|
32
|
+
const db = getDb();
|
|
33
|
+
const rows = db
|
|
34
|
+
.prepare("SELECT * FROM conversation_messages WHERE conversation_id = ? ORDER BY created_at ASC")
|
|
35
|
+
.all(conversationId);
|
|
36
|
+
return rows.map(toMessage);
|
|
37
|
+
}
|
|
38
|
+
export function listConversations(opts = {}) {
|
|
39
|
+
const db = getDb();
|
|
40
|
+
const limit = opts.limit ?? 50;
|
|
41
|
+
const offset = opts.offset ?? 0;
|
|
42
|
+
let where = "WHERE 1=1";
|
|
43
|
+
const params = [];
|
|
44
|
+
if (opts.from) {
|
|
45
|
+
where += " AND started_at >= ?";
|
|
46
|
+
params.push(opts.from);
|
|
47
|
+
}
|
|
48
|
+
if (opts.to) {
|
|
49
|
+
where += " AND updated_at <= ?";
|
|
50
|
+
params.push(opts.to);
|
|
51
|
+
}
|
|
52
|
+
const countRow = db
|
|
53
|
+
.prepare(`SELECT COUNT(*) as cnt FROM (
|
|
54
|
+
SELECT conversation_id as id,
|
|
55
|
+
MIN(created_at) as started_at,
|
|
56
|
+
MAX(created_at) as updated_at
|
|
57
|
+
FROM conversation_messages
|
|
58
|
+
GROUP BY conversation_id
|
|
59
|
+
) ${where}`)
|
|
60
|
+
.get(...params);
|
|
61
|
+
const total = countRow.cnt;
|
|
62
|
+
const rows = db
|
|
63
|
+
.prepare(`SELECT
|
|
64
|
+
sub.id,
|
|
65
|
+
sub.started_at,
|
|
66
|
+
sub.updated_at,
|
|
67
|
+
sub.message_count,
|
|
68
|
+
first_msg.content as preview
|
|
69
|
+
FROM (
|
|
70
|
+
SELECT conversation_id as id,
|
|
71
|
+
MIN(created_at) as started_at,
|
|
72
|
+
MAX(created_at) as updated_at,
|
|
73
|
+
COUNT(*) as message_count
|
|
74
|
+
FROM conversation_messages
|
|
75
|
+
GROUP BY conversation_id
|
|
76
|
+
) sub
|
|
77
|
+
JOIN conversation_messages first_msg
|
|
78
|
+
ON first_msg.conversation_id = sub.id
|
|
79
|
+
AND first_msg.role = 'user'
|
|
80
|
+
AND first_msg.created_at = (
|
|
81
|
+
SELECT MIN(created_at) FROM conversation_messages
|
|
82
|
+
WHERE conversation_id = sub.id AND role = 'user'
|
|
83
|
+
)
|
|
84
|
+
${where}
|
|
85
|
+
ORDER BY sub.updated_at DESC
|
|
86
|
+
LIMIT ? OFFSET ?`)
|
|
87
|
+
.all(...params, limit, offset);
|
|
88
|
+
return { items: rows.map(toSummary), total };
|
|
89
|
+
}
|
|
90
|
+
export function searchConversations(query, opts = {}) {
|
|
91
|
+
const db = getDb();
|
|
92
|
+
const limit = opts.limit ?? 50;
|
|
93
|
+
const offset = opts.offset ?? 0;
|
|
94
|
+
let dateWhere = "";
|
|
95
|
+
const dateParams = [];
|
|
96
|
+
if (opts.from) {
|
|
97
|
+
dateWhere += " AND cm.created_at >= ?";
|
|
98
|
+
dateParams.push(opts.from);
|
|
99
|
+
}
|
|
100
|
+
if (opts.to) {
|
|
101
|
+
dateWhere += " AND cm.created_at <= ?";
|
|
102
|
+
dateParams.push(opts.to);
|
|
103
|
+
}
|
|
104
|
+
// Find distinct conversation IDs matching FTS query
|
|
105
|
+
const matchingIds = db
|
|
106
|
+
.prepare(`SELECT DISTINCT cm.conversation_id
|
|
107
|
+
FROM conversation_messages cm
|
|
108
|
+
JOIN conversation_messages_fts fts ON fts.rowid = cm.rowid
|
|
109
|
+
WHERE conversation_messages_fts MATCH ?${dateWhere}
|
|
110
|
+
LIMIT 500`)
|
|
111
|
+
.all(query, ...dateParams);
|
|
112
|
+
if (matchingIds.length === 0) {
|
|
113
|
+
return { items: [], total: 0 };
|
|
114
|
+
}
|
|
115
|
+
const idList = matchingIds.map((r) => r.conversation_id);
|
|
116
|
+
const placeholders = idList.map(() => "?").join(",");
|
|
117
|
+
const total = idList.length;
|
|
118
|
+
const rows = db
|
|
119
|
+
.prepare(`SELECT
|
|
120
|
+
sub.id,
|
|
121
|
+
sub.started_at,
|
|
122
|
+
sub.updated_at,
|
|
123
|
+
sub.message_count,
|
|
124
|
+
first_msg.content as preview
|
|
125
|
+
FROM (
|
|
126
|
+
SELECT conversation_id as id,
|
|
127
|
+
MIN(created_at) as started_at,
|
|
128
|
+
MAX(created_at) as updated_at,
|
|
129
|
+
COUNT(*) as message_count
|
|
130
|
+
FROM conversation_messages
|
|
131
|
+
WHERE conversation_id IN (${placeholders})
|
|
132
|
+
GROUP BY conversation_id
|
|
133
|
+
) sub
|
|
134
|
+
JOIN conversation_messages first_msg
|
|
135
|
+
ON first_msg.conversation_id = sub.id
|
|
136
|
+
AND first_msg.role = 'user'
|
|
137
|
+
AND first_msg.created_at = (
|
|
138
|
+
SELECT MIN(created_at) FROM conversation_messages
|
|
139
|
+
WHERE conversation_id = sub.id AND role = 'user'
|
|
140
|
+
)
|
|
141
|
+
ORDER BY sub.updated_at DESC
|
|
142
|
+
LIMIT ? OFFSET ?`)
|
|
143
|
+
.all(...idList, limit, offset);
|
|
144
|
+
return { items: rows.map(toSummary), total };
|
|
145
|
+
}
|
|
146
|
+
export function deleteConversation(conversationId) {
|
|
147
|
+
const db = getDb();
|
|
148
|
+
db.prepare("DELETE FROM conversation_messages WHERE conversation_id = ?").run(conversationId);
|
|
149
|
+
}
|
|
150
|
+
//# sourceMappingURL=conversations.js.map
|