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.
Files changed (69) hide show
  1. package/dist/api/server.js +289 -12
  2. package/dist/config.js +6 -0
  3. package/dist/copilot/agents.js +100 -5
  4. package/dist/copilot/ceremonies.js +12 -2
  5. package/dist/copilot/io-scheduler.js +9 -1
  6. package/dist/copilot/orchestrator.js +4 -0
  7. package/dist/copilot/scheduler.js +7 -2
  8. package/dist/copilot/skills.js +138 -6
  9. package/dist/copilot/squad-tools.js +102 -0
  10. package/dist/copilot/system-message.js +2 -1
  11. package/dist/copilot/token-tracker.js +89 -0
  12. package/dist/copilot/tools.js +27 -5
  13. package/dist/copilot/trigger-schedule.js +31 -0
  14. package/dist/paths.js +1 -0
  15. package/dist/store/agent-events.js +19 -0
  16. package/dist/store/audit-log.js +71 -0
  17. package/dist/store/conversations.js +150 -0
  18. package/dist/store/db.js +111 -0
  19. package/dist/store/schedules.js +9 -1
  20. package/dist/store/squad-colors.js +23 -0
  21. package/dist/store/squads.js +6 -1
  22. package/dist/store/tasks.js +43 -0
  23. package/dist/store/token-usage.js +94 -0
  24. package/dist/wiki/backlinks.js +51 -0
  25. package/dist/wiki/fs.js +63 -1
  26. package/dist/wiki/search.js +13 -2
  27. package/package.json +1 -1
  28. package/web-dist/assets/AuditLogView-DqxVzjd_.js +6 -0
  29. package/web-dist/assets/ChatView-BBopM_A3.js +1 -0
  30. package/web-dist/assets/FeedView-Bo4p1stx.js +6 -0
  31. package/web-dist/assets/HistoryView-ChTuQvXr.js +1 -0
  32. package/web-dist/assets/LoginView-AnOP3Mau.js +1 -0
  33. package/web-dist/assets/McpView-DPcihjuB.js +1 -0
  34. package/web-dist/assets/SchedulesView-B2o3vMm-.js +6 -0
  35. package/web-dist/assets/SettingsView-rtMUmH43.js +1 -0
  36. package/web-dist/assets/SkillsView-D_NHLk7C.js +15 -0
  37. package/web-dist/assets/SquadDetailView-BKXLWvwn.js +26 -0
  38. package/web-dist/assets/SquadHealthView-CVJiAgVW.js +11 -0
  39. package/web-dist/assets/SquadsView-fammrB7r.js +6 -0
  40. package/web-dist/assets/UsageView-Cy5Mbprb.js +16 -0
  41. package/web-dist/assets/WikiView-B5TOMnOg.js +36 -0
  42. package/web-dist/assets/arrow-left-CGMB1w_A.js +6 -0
  43. package/web-dist/assets/git-branch-C_Hu39uh.js +6 -0
  44. package/web-dist/assets/index-CQ_szaoT.css +1 -0
  45. package/web-dist/assets/index-CiZnRvN4.js +253 -0
  46. package/web-dist/assets/{plus-Cvp1w2CO.js → plus-DIBAaEMT.js} +1 -1
  47. package/web-dist/assets/{x-O3fBd1Cr.js → save-Chqlu7QA.js} +2 -7
  48. package/web-dist/assets/search-Cl8HcIsG.js +6 -0
  49. package/web-dist/assets/squad-colors-B8B_Y-lz.js +1 -0
  50. package/web-dist/assets/{trash-2-Cr3vrmL5.js → trash-2-CQSzbVIr.js} +1 -1
  51. package/web-dist/assets/triangle-alert-C1OjMvP5.js +6 -0
  52. package/web-dist/assets/x-DThJHYFm.js +6 -0
  53. package/web-dist/favicon.svg +9 -3
  54. package/web-dist/index.html +2 -2
  55. package/web-dist/logo.svg +10 -0
  56. package/web-dist/assets/ChatView-mZaaw3pd.js +0 -11
  57. package/web-dist/assets/FeedView-BHacQwXQ.js +0 -6
  58. package/web-dist/assets/LoginView-B6aSD9II.js +0 -1
  59. package/web-dist/assets/MarkdownContent.vue_vue_type_script_setup_true_lang-CEo_ckIb.js +0 -56
  60. package/web-dist/assets/McpView-BAVRUHIE.js +0 -1
  61. package/web-dist/assets/SchedulesView-dOd1SQiP.js +0 -1
  62. package/web-dist/assets/SettingsView-CCDeEsVg.js +0 -1
  63. package/web-dist/assets/SkillsView-gCfQ35FQ.js +0 -1
  64. package/web-dist/assets/SquadDetailView-CQhFfZTc.js +0 -21
  65. package/web-dist/assets/SquadsView-CZFxtOao.js +0 -6
  66. package/web-dist/assets/WikiView-B0cuUFfm.js +0 -26
  67. package/web-dist/assets/api-DdW5uOZf.js +0 -1
  68. package/web-dist/assets/index-BQdXxKfc.js +0 -138
  69. 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 agenda = schedule.agenda || "triage";
21
- const prompt = `[Squad Schedule] Run "${agenda}" stand-up for squad ${schedule.squad_id}. Agenda: ${agenda}`;
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}`);
@@ -1,9 +1,9 @@
1
- import { existsSync, readdirSync, readFileSync, rmSync } from "node:fs";
2
- import { join, basename } from "node:path";
3
- import { exec } from "node:child_process";
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 execAsync = promisify(exec);
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 execAsync(`git clone --depth 1 ${url} ${dest}`);
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
@@ -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
- return `Squad "${name}" created with universe "${universe}". ID: ${squad.id}, Slug: ${squad.slug}. Wiki path: ~/.io/wiki/squads/${squad.slug}/${cloneMsg}`;
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().optional().describe("Squad ID (required for squad schedules)"),
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 IO schedules"),
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
- return stdout.trim() || "(no output)";
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
- return `Error (exit ${err.code}): ${stderr || stdout || err.message}`;
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