heyio 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +212 -0
- package/dist/api/server.js +70 -0
- package/dist/config.js +28 -0
- package/dist/copilot/agents.js +267 -0
- package/dist/copilot/client.js +29 -0
- package/dist/copilot/orchestrator.js +354 -0
- package/dist/copilot/skills.js +132 -0
- package/dist/copilot/system-message.js +90 -0
- package/dist/copilot/tools.js +239 -0
- package/dist/daemon.js +145 -0
- package/dist/index.js +139 -0
- package/dist/paths.js +10 -0
- package/dist/store/db.js +101 -0
- package/dist/store/squads.js +52 -0
- package/dist/store/tasks.js +32 -0
- package/dist/telegram/bot.js +141 -0
- package/dist/telegram/handlers.js +4 -0
- package/dist/tui/index.js +79 -0
- package/dist/update.js +53 -0
- package/dist/wiki/fs.js +152 -0
- package/dist/wiki/search.js +49 -0
- package/package.json +54 -0
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
import { approveAll, } from "@github/copilot-sdk";
|
|
2
|
+
import { config } from "../config.js";
|
|
3
|
+
import { SESSIONS_DIR } from "../paths.js";
|
|
4
|
+
import { getState, setState, deleteState, logConversation } from "../store/db.js";
|
|
5
|
+
import { clearStaleTasks } from "../store/tasks.js";
|
|
6
|
+
import { getSquad, listSquads, createSquad, logDecision, getDecisionsSummary, updateSquadStatus, } from "../store/squads.js";
|
|
7
|
+
import { readPage, writePage, assertPagePath } from "../wiki/fs.js";
|
|
8
|
+
import { searchWiki, getWikiSummary } from "../wiki/search.js";
|
|
9
|
+
import { getOrchestratorSystemMessage } from "./system-message.js";
|
|
10
|
+
import { createTools } from "./tools.js";
|
|
11
|
+
import { getSkillDirectories } from "./skills.js";
|
|
12
|
+
import { resetClient } from "./client.js";
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Constants
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
const HEALTH_CHECK_INTERVAL_MS = 30_000;
|
|
17
|
+
const SEND_TIMEOUT_MS = 600_000;
|
|
18
|
+
const MAX_RETRIES = 3;
|
|
19
|
+
const SESSION_ID_KEY = "orchestrator_session_id";
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// Module state
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
let client;
|
|
24
|
+
let orchestratorSession;
|
|
25
|
+
let healthCheckTimer;
|
|
26
|
+
let sessionInitPromise;
|
|
27
|
+
let clientResetPromise;
|
|
28
|
+
const messageQueue = [];
|
|
29
|
+
let processing = false;
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// Session config helpers
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
function mapSquad(s) {
|
|
34
|
+
return { slug: s.slug, name: s.name, projectPath: s.project_path, status: s.status };
|
|
35
|
+
}
|
|
36
|
+
function getToolDeps() {
|
|
37
|
+
return {
|
|
38
|
+
wikiRead: readPage,
|
|
39
|
+
wikiWrite: writePage,
|
|
40
|
+
wikiSearch: searchWiki,
|
|
41
|
+
wikiAssertPagePath: assertPagePath,
|
|
42
|
+
getSquad: (slug) => {
|
|
43
|
+
const s = getSquad(slug);
|
|
44
|
+
return s ? mapSquad(s) : undefined;
|
|
45
|
+
},
|
|
46
|
+
listSquads: () => listSquads().map(mapSquad),
|
|
47
|
+
createSquad,
|
|
48
|
+
logDecision,
|
|
49
|
+
getDecisionsSummary,
|
|
50
|
+
updateSquadStatus,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function getSessionConfig() {
|
|
54
|
+
const tools = createTools(getToolDeps());
|
|
55
|
+
return { tools, skillDirectories: getSkillDirectories() };
|
|
56
|
+
}
|
|
57
|
+
function buildFullSessionConfig() {
|
|
58
|
+
const { tools, skillDirectories } = getSessionConfig();
|
|
59
|
+
return {
|
|
60
|
+
model: config.defaultModel || "gpt-4.1",
|
|
61
|
+
configDir: SESSIONS_DIR,
|
|
62
|
+
streaming: true,
|
|
63
|
+
systemMessage: {
|
|
64
|
+
content: getOrchestratorSystemMessage({
|
|
65
|
+
selfEditEnabled: config.selfEditEnabled,
|
|
66
|
+
memorySummary: getWikiSummary() || undefined,
|
|
67
|
+
}),
|
|
68
|
+
},
|
|
69
|
+
tools,
|
|
70
|
+
skillDirectories,
|
|
71
|
+
onPermissionRequest: approveAll,
|
|
72
|
+
infiniteSessions: {
|
|
73
|
+
enabled: true,
|
|
74
|
+
backgroundCompactionThreshold: 0.80,
|
|
75
|
+
bufferExhaustionThreshold: 0.95,
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function buildResumeConfig() {
|
|
80
|
+
const { tools, skillDirectories } = getSessionConfig();
|
|
81
|
+
return {
|
|
82
|
+
configDir: SESSIONS_DIR,
|
|
83
|
+
streaming: true,
|
|
84
|
+
tools,
|
|
85
|
+
skillDirectories,
|
|
86
|
+
onPermissionRequest: approveAll,
|
|
87
|
+
infiniteSessions: {
|
|
88
|
+
enabled: true,
|
|
89
|
+
backgroundCompactionThreshold: 0.80,
|
|
90
|
+
bufferExhaustionThreshold: 0.95,
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
// Error classification
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
function isConnectionError(err) {
|
|
98
|
+
if (!(err instanceof Error))
|
|
99
|
+
return false;
|
|
100
|
+
const msg = err.message.toLowerCase();
|
|
101
|
+
return (msg.includes("disconnect") ||
|
|
102
|
+
msg.includes("epipe") ||
|
|
103
|
+
msg.includes("econnreset") ||
|
|
104
|
+
msg.includes("econnrefused") ||
|
|
105
|
+
msg.includes("connection") ||
|
|
106
|
+
msg.includes("socket"));
|
|
107
|
+
}
|
|
108
|
+
function isSessionError(err) {
|
|
109
|
+
if (!(err instanceof Error))
|
|
110
|
+
return false;
|
|
111
|
+
const msg = err.message.toLowerCase();
|
|
112
|
+
return (msg.includes("session") ||
|
|
113
|
+
msg.includes("closed") ||
|
|
114
|
+
msg.includes("expired") ||
|
|
115
|
+
msg.includes("not found"));
|
|
116
|
+
}
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
// Client management
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
async function ensureClient() {
|
|
121
|
+
if (!client) {
|
|
122
|
+
throw new Error("Orchestrator not initialized — call initOrchestrator first");
|
|
123
|
+
}
|
|
124
|
+
if (client.getState() === "connected")
|
|
125
|
+
return client;
|
|
126
|
+
// Coalesce concurrent reset attempts
|
|
127
|
+
if (clientResetPromise)
|
|
128
|
+
return clientResetPromise;
|
|
129
|
+
clientResetPromise = (async () => {
|
|
130
|
+
console.error("[io] Client disconnected, resetting…");
|
|
131
|
+
try {
|
|
132
|
+
const newClient = await resetClient();
|
|
133
|
+
client = newClient;
|
|
134
|
+
return newClient;
|
|
135
|
+
}
|
|
136
|
+
finally {
|
|
137
|
+
clientResetPromise = undefined;
|
|
138
|
+
}
|
|
139
|
+
})();
|
|
140
|
+
return clientResetPromise;
|
|
141
|
+
}
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
// Session management
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
async function ensureOrchestratorSession() {
|
|
146
|
+
if (orchestratorSession)
|
|
147
|
+
return orchestratorSession;
|
|
148
|
+
// Coalesce concurrent session creation
|
|
149
|
+
if (sessionInitPromise)
|
|
150
|
+
return sessionInitPromise;
|
|
151
|
+
sessionInitPromise = (async () => {
|
|
152
|
+
try {
|
|
153
|
+
const c = await ensureClient();
|
|
154
|
+
const savedSessionId = getState(SESSION_ID_KEY);
|
|
155
|
+
if (savedSessionId) {
|
|
156
|
+
try {
|
|
157
|
+
console.error("[io] Resuming session:", savedSessionId);
|
|
158
|
+
const session = await c.resumeSession(savedSessionId, buildResumeConfig());
|
|
159
|
+
orchestratorSession = session;
|
|
160
|
+
return session;
|
|
161
|
+
}
|
|
162
|
+
catch (err) {
|
|
163
|
+
console.error("[io] Failed to resume session, creating new one:", err instanceof Error ? err.message : err);
|
|
164
|
+
deleteState(SESSION_ID_KEY);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
console.error("[io] Creating new orchestrator session");
|
|
168
|
+
const session = await c.createSession(buildFullSessionConfig());
|
|
169
|
+
setState(SESSION_ID_KEY, session.sessionId);
|
|
170
|
+
orchestratorSession = session;
|
|
171
|
+
return session;
|
|
172
|
+
}
|
|
173
|
+
finally {
|
|
174
|
+
sessionInitPromise = undefined;
|
|
175
|
+
}
|
|
176
|
+
})();
|
|
177
|
+
return sessionInitPromise;
|
|
178
|
+
}
|
|
179
|
+
function invalidateSession() {
|
|
180
|
+
orchestratorSession = undefined;
|
|
181
|
+
sessionInitPromise = undefined;
|
|
182
|
+
}
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
// Message execution
|
|
185
|
+
// ---------------------------------------------------------------------------
|
|
186
|
+
async function executeOnSession(prompt, callback) {
|
|
187
|
+
const session = await ensureOrchestratorSession();
|
|
188
|
+
let accumulated = "";
|
|
189
|
+
const unsubDelta = session.on("assistant.message_delta", (event) => {
|
|
190
|
+
accumulated += event.data.deltaContent;
|
|
191
|
+
callback(accumulated, false);
|
|
192
|
+
});
|
|
193
|
+
try {
|
|
194
|
+
const result = await session.sendAndWait({ prompt }, SEND_TIMEOUT_MS);
|
|
195
|
+
unsubDelta();
|
|
196
|
+
const finalText = result?.data.content ?? accumulated;
|
|
197
|
+
callback(finalText, true);
|
|
198
|
+
return finalText;
|
|
199
|
+
}
|
|
200
|
+
catch (err) {
|
|
201
|
+
unsubDelta();
|
|
202
|
+
// If we accumulated partial text, return it gracefully on timeout
|
|
203
|
+
if (accumulated && err instanceof Error && err.message.includes("timeout")) {
|
|
204
|
+
console.error("[io] Session sendAndWait timed out, returning partial response");
|
|
205
|
+
callback(accumulated, true);
|
|
206
|
+
return accumulated;
|
|
207
|
+
}
|
|
208
|
+
// Session-level errors: invalidate and let caller retry
|
|
209
|
+
if (isSessionError(err)) {
|
|
210
|
+
console.error("[io] Session error, invalidating:", err instanceof Error ? err.message : err);
|
|
211
|
+
invalidateSession();
|
|
212
|
+
}
|
|
213
|
+
throw err;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
// ---------------------------------------------------------------------------
|
|
217
|
+
// Queue processing
|
|
218
|
+
// ---------------------------------------------------------------------------
|
|
219
|
+
function sourceTag(source) {
|
|
220
|
+
switch (source.type) {
|
|
221
|
+
case "telegram":
|
|
222
|
+
return "[via telegram]";
|
|
223
|
+
case "tui":
|
|
224
|
+
return "[via tui]";
|
|
225
|
+
case "background":
|
|
226
|
+
return "[via background]";
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
function sourceLabel(source) {
|
|
230
|
+
switch (source.type) {
|
|
231
|
+
case "telegram":
|
|
232
|
+
return "telegram";
|
|
233
|
+
case "tui":
|
|
234
|
+
return "tui";
|
|
235
|
+
case "background":
|
|
236
|
+
return "background";
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
async function processQueue() {
|
|
240
|
+
if (processing)
|
|
241
|
+
return;
|
|
242
|
+
processing = true;
|
|
243
|
+
try {
|
|
244
|
+
while (messageQueue.length > 0) {
|
|
245
|
+
const msg = messageQueue.shift();
|
|
246
|
+
const taggedPrompt = `${sourceTag(msg.source)} ${msg.prompt}`;
|
|
247
|
+
let lastError;
|
|
248
|
+
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
|
249
|
+
try {
|
|
250
|
+
const response = await executeOnSession(taggedPrompt, msg.callback);
|
|
251
|
+
logConversation("assistant", response, sourceLabel(msg.source));
|
|
252
|
+
msg.resolve();
|
|
253
|
+
lastError = undefined;
|
|
254
|
+
break;
|
|
255
|
+
}
|
|
256
|
+
catch (err) {
|
|
257
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
258
|
+
console.error(`[io] Attempt ${attempt}/${MAX_RETRIES} failed:`, lastError.message);
|
|
259
|
+
if (isConnectionError(err)) {
|
|
260
|
+
// Reset client and invalidate session for connection errors
|
|
261
|
+
invalidateSession();
|
|
262
|
+
try {
|
|
263
|
+
await ensureClient();
|
|
264
|
+
}
|
|
265
|
+
catch (resetErr) {
|
|
266
|
+
console.error("[io] Client reset failed:", resetErr instanceof Error ? resetErr.message : resetErr);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
else if (isSessionError(err)) {
|
|
270
|
+
// Session already invalidated in executeOnSession
|
|
271
|
+
}
|
|
272
|
+
else if (attempt === MAX_RETRIES) {
|
|
273
|
+
// Non-retryable error on last attempt
|
|
274
|
+
break;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
if (lastError) {
|
|
279
|
+
const errorMsg = `Sorry, I encountered an error: ${lastError.message}`;
|
|
280
|
+
msg.callback(errorMsg, true);
|
|
281
|
+
msg.reject(lastError);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
finally {
|
|
286
|
+
processing = false;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
// ---------------------------------------------------------------------------
|
|
290
|
+
// Public API
|
|
291
|
+
// ---------------------------------------------------------------------------
|
|
292
|
+
export async function initOrchestrator(copilotClient) {
|
|
293
|
+
client = copilotClient;
|
|
294
|
+
clearStaleTasks();
|
|
295
|
+
// Validate the configured model
|
|
296
|
+
try {
|
|
297
|
+
const models = await copilotClient.listModels();
|
|
298
|
+
const defaultModel = config.defaultModel || "gpt-4.1";
|
|
299
|
+
const modelIds = models.map((m) => m.id);
|
|
300
|
+
if (!modelIds.includes(defaultModel)) {
|
|
301
|
+
console.error(`[io] Configured model "${defaultModel}" not found. Available: ${modelIds.join(", ")}`);
|
|
302
|
+
}
|
|
303
|
+
else {
|
|
304
|
+
console.error(`[io] Model validated: ${defaultModel}`);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
catch (err) {
|
|
308
|
+
console.error("[io] Could not validate models:", err instanceof Error ? err.message : err);
|
|
309
|
+
}
|
|
310
|
+
// Start health check timer
|
|
311
|
+
healthCheckTimer = setInterval(() => {
|
|
312
|
+
if (!client || client.getState() !== "connected") {
|
|
313
|
+
console.error("[io] Health check: client disconnected, reconnecting…");
|
|
314
|
+
ensureClient().catch((err) => {
|
|
315
|
+
console.error("[io] Health check reconnect failed:", err instanceof Error ? err.message : err);
|
|
316
|
+
});
|
|
317
|
+
invalidateSession();
|
|
318
|
+
}
|
|
319
|
+
}, HEALTH_CHECK_INTERVAL_MS);
|
|
320
|
+
// Eagerly create/resume the session
|
|
321
|
+
try {
|
|
322
|
+
await ensureOrchestratorSession();
|
|
323
|
+
console.error("[io] Orchestrator session ready");
|
|
324
|
+
}
|
|
325
|
+
catch (err) {
|
|
326
|
+
console.error("[io] Eager session creation failed (will retry on first message):", err instanceof Error ? err.message : err);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
export async function sendToOrchestrator(prompt, source, callback) {
|
|
330
|
+
logConversation("user", prompt, sourceLabel(source));
|
|
331
|
+
return new Promise((resolve, reject) => {
|
|
332
|
+
messageQueue.push({ prompt, source, callback, resolve, reject });
|
|
333
|
+
processQueue();
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
export async function shutdownOrchestrator() {
|
|
337
|
+
if (healthCheckTimer) {
|
|
338
|
+
clearInterval(healthCheckTimer);
|
|
339
|
+
healthCheckTimer = undefined;
|
|
340
|
+
}
|
|
341
|
+
if (orchestratorSession) {
|
|
342
|
+
try {
|
|
343
|
+
await orchestratorSession.disconnect();
|
|
344
|
+
}
|
|
345
|
+
catch (err) {
|
|
346
|
+
console.error("[io] Error disconnecting session:", err instanceof Error ? err.message : err);
|
|
347
|
+
}
|
|
348
|
+
orchestratorSession = undefined;
|
|
349
|
+
}
|
|
350
|
+
sessionInitPromise = undefined;
|
|
351
|
+
clientResetPromise = undefined;
|
|
352
|
+
client = undefined;
|
|
353
|
+
}
|
|
354
|
+
//# sourceMappingURL=orchestrator.js.map
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, rmSync, statSync } from "fs";
|
|
2
|
+
import { join, basename } from "path";
|
|
3
|
+
import { execSync } from "child_process";
|
|
4
|
+
import { SKILLS_DIR } from "../paths.js";
|
|
5
|
+
/**
|
|
6
|
+
* Scan SKILLS_DIR for subdirectories that contain a SKILL.md file.
|
|
7
|
+
* Returns absolute paths to qualifying skill directories.
|
|
8
|
+
*/
|
|
9
|
+
export function getSkillDirectories() {
|
|
10
|
+
if (!existsSync(SKILLS_DIR))
|
|
11
|
+
return [];
|
|
12
|
+
const dirs = [];
|
|
13
|
+
for (const entry of readdirSync(SKILLS_DIR)) {
|
|
14
|
+
const skillDir = join(SKILLS_DIR, entry);
|
|
15
|
+
if (!statSync(skillDir).isDirectory())
|
|
16
|
+
continue;
|
|
17
|
+
const skillMd = join(skillDir, "SKILL.md");
|
|
18
|
+
if (!existsSync(skillMd))
|
|
19
|
+
continue;
|
|
20
|
+
dirs.push(skillDir);
|
|
21
|
+
// Check for an agents subdirectory (Copilot SDK custom agents)
|
|
22
|
+
const agentsDir = join(skillDir, "agents");
|
|
23
|
+
if (existsSync(agentsDir) && statSync(agentsDir).isDirectory()) {
|
|
24
|
+
dirs.push(agentsDir);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return dirs;
|
|
28
|
+
}
|
|
29
|
+
function parseSkillMd(content) {
|
|
30
|
+
const lines = content.split(/\r?\n/);
|
|
31
|
+
let name = "";
|
|
32
|
+
let description = "";
|
|
33
|
+
let foundHeading = false;
|
|
34
|
+
const descLines = [];
|
|
35
|
+
for (const line of lines) {
|
|
36
|
+
if (!foundHeading) {
|
|
37
|
+
const match = line.match(/^#\s+(.+)/);
|
|
38
|
+
if (match) {
|
|
39
|
+
name = match[1].trim();
|
|
40
|
+
foundHeading = true;
|
|
41
|
+
}
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
// Skip blank lines between heading and first paragraph
|
|
45
|
+
if (descLines.length === 0 && line.trim() === "")
|
|
46
|
+
continue;
|
|
47
|
+
// Stop at the next blank line after collecting description text
|
|
48
|
+
if (descLines.length > 0 && line.trim() === "")
|
|
49
|
+
break;
|
|
50
|
+
// Stop at another heading
|
|
51
|
+
if (line.match(/^#+\s/))
|
|
52
|
+
break;
|
|
53
|
+
descLines.push(line.trim());
|
|
54
|
+
}
|
|
55
|
+
description = descLines.join(" ");
|
|
56
|
+
return { name, description };
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* List all installed skills with metadata parsed from their SKILL.md files.
|
|
60
|
+
*/
|
|
61
|
+
export function listSkills() {
|
|
62
|
+
if (!existsSync(SKILLS_DIR))
|
|
63
|
+
return [];
|
|
64
|
+
const skills = [];
|
|
65
|
+
for (const entry of readdirSync(SKILLS_DIR)) {
|
|
66
|
+
const skillDir = join(SKILLS_DIR, entry);
|
|
67
|
+
if (!statSync(skillDir).isDirectory())
|
|
68
|
+
continue;
|
|
69
|
+
const skillMdPath = join(skillDir, "SKILL.md");
|
|
70
|
+
if (!existsSync(skillMdPath))
|
|
71
|
+
continue;
|
|
72
|
+
const content = readFileSync(skillMdPath, "utf-8");
|
|
73
|
+
const { name, description } = parseSkillMd(content);
|
|
74
|
+
skills.push({
|
|
75
|
+
name: name || entry,
|
|
76
|
+
slug: entry,
|
|
77
|
+
description,
|
|
78
|
+
path: skillDir,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
return skills;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Clone a git repo into SKILLS_DIR and return the installed skill info.
|
|
85
|
+
* Throws if the cloned repo does not contain a SKILL.md file.
|
|
86
|
+
*/
|
|
87
|
+
export async function installSkill(repoUrl) {
|
|
88
|
+
const repoName = basename(repoUrl, ".git").replace(/\.git$/, "");
|
|
89
|
+
const destDir = join(SKILLS_DIR, repoName);
|
|
90
|
+
execSync(`git clone ${repoUrl} ${destDir}`, { stdio: "pipe" });
|
|
91
|
+
const skillMdPath = join(destDir, "SKILL.md");
|
|
92
|
+
if (!existsSync(skillMdPath)) {
|
|
93
|
+
rmSync(destDir, { recursive: true, force: true });
|
|
94
|
+
throw new Error(`Repository "${repoUrl}" does not contain a SKILL.md file.`);
|
|
95
|
+
}
|
|
96
|
+
const content = readFileSync(skillMdPath, "utf-8");
|
|
97
|
+
const { name, description } = parseSkillMd(content);
|
|
98
|
+
return {
|
|
99
|
+
name: name || repoName,
|
|
100
|
+
slug: repoName,
|
|
101
|
+
description,
|
|
102
|
+
path: destDir,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Remove a skill directory by its slug. Returns true if it existed.
|
|
107
|
+
*/
|
|
108
|
+
export function removeSkill(slug) {
|
|
109
|
+
const skillDir = join(SKILLS_DIR, slug);
|
|
110
|
+
if (!existsSync(skillDir))
|
|
111
|
+
return false;
|
|
112
|
+
rmSync(skillDir, { recursive: true, force: true });
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Search the skills registry for skills matching the given query.
|
|
117
|
+
* Returns an empty array on network or parsing errors.
|
|
118
|
+
*/
|
|
119
|
+
export async function searchSkillsRegistry(query) {
|
|
120
|
+
try {
|
|
121
|
+
const url = `https://skills.sh/api/search?q=${encodeURIComponent(query)}`;
|
|
122
|
+
const response = await fetch(url);
|
|
123
|
+
if (!response.ok)
|
|
124
|
+
return [];
|
|
125
|
+
const data = (await response.json());
|
|
126
|
+
return Array.isArray(data) ? data : [];
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return [];
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
//# sourceMappingURL=skills.js.map
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { config } from "../config.js";
|
|
2
|
+
export function getOrchestratorSystemMessage(opts) {
|
|
3
|
+
const memoryBlock = opts?.memorySummary
|
|
4
|
+
? `\n## Memory\nYou have a persistent knowledge base. Here's what you currently know:\n\n${opts.memorySummary}\n`
|
|
5
|
+
: "\n## Memory\nYou have a persistent knowledge base (wiki). It's currently empty — use `wiki_write` to start building it!\n";
|
|
6
|
+
const selfEditBlock = opts?.selfEditEnabled
|
|
7
|
+
? ""
|
|
8
|
+
: `\n## Self-Edit Protection
|
|
9
|
+
|
|
10
|
+
**You must NEVER modify your own source code.** This includes the IO codebase, configuration files in the project repo, or any file that is part of the IO application itself.
|
|
11
|
+
|
|
12
|
+
If the user asks you to modify your own code, politely decline and explain that self-editing is disabled for safety. Suggest they make the changes manually or start IO with \`--self-edit\` to temporarily allow it.
|
|
13
|
+
|
|
14
|
+
This restriction does NOT apply to:
|
|
15
|
+
- User project files (code the user asks you to work on)
|
|
16
|
+
- Skills in ~/.io/skills/ (user data)
|
|
17
|
+
- The ~/.io/config.json file
|
|
18
|
+
- Any files outside the IO installation directory
|
|
19
|
+
`;
|
|
20
|
+
const squadBlock = opts?.squadRoster
|
|
21
|
+
? `\n### Active Squads\n${opts.squadRoster}\n`
|
|
22
|
+
: "";
|
|
23
|
+
const osName = process.platform === "darwin" ? "macOS"
|
|
24
|
+
: process.platform === "win32" ? "Windows"
|
|
25
|
+
: "Linux";
|
|
26
|
+
return `You are IO, a personal AI assistant for developers running 24/7 on the user's machine (${osName}). You are an always-on assistant daemon.
|
|
27
|
+
|
|
28
|
+
## Your Architecture
|
|
29
|
+
|
|
30
|
+
You are a Node.js daemon process built with the Copilot SDK. Here's how you work:
|
|
31
|
+
|
|
32
|
+
- **Telegram bot**: Messages arrive tagged with \`[via telegram]\`. Keep responses concise and mobile-friendly.
|
|
33
|
+
- **Local TUI**: A terminal interface on the local machine. Messages arrive tagged with \`[via tui]\`. You can be more verbose here.
|
|
34
|
+
- **Background tasks**: Messages tagged \`[via background]\` are results from squad workers you delegated to.
|
|
35
|
+
- **HTTP API**: You expose a local API on port ${config.apiPort} for programmatic access.
|
|
36
|
+
|
|
37
|
+
When no source tag is present, assume TUI.
|
|
38
|
+
|
|
39
|
+
## Your Capabilities
|
|
40
|
+
|
|
41
|
+
1. **Direct conversation**: Answer questions, discuss problems — no tools needed.
|
|
42
|
+
2. **Squad system**: You can create project squads — persistent teams of specialized agents for specific projects. Each squad remembers its decisions and context.
|
|
43
|
+
3. **Knowledge base**: You have a wiki-style knowledge base. Proactively save user preferences, project details, and important facts.
|
|
44
|
+
4. **Shell access**: You can run shell commands on the user's machine.
|
|
45
|
+
5. **Skills**: You have a modular skill system. Skills teach you how to use external tools.
|
|
46
|
+
|
|
47
|
+
## Your Role
|
|
48
|
+
|
|
49
|
+
You receive messages and decide how to handle them:
|
|
50
|
+
|
|
51
|
+
- **Direct answer**: For simple questions, general knowledge, status checks — answer directly.
|
|
52
|
+
- **Use tools**: For tasks requiring shell access, file operations, web lookups — use your tools.
|
|
53
|
+
- **Create/delegate to squad**: For coding projects that need persistent context — create a squad with specialized agents.
|
|
54
|
+
- **Use a skill**: If you have a skill for the task, use it.
|
|
55
|
+
${squadBlock}
|
|
56
|
+
## Squad System
|
|
57
|
+
|
|
58
|
+
Squads are persistent project teams. When a user works on a codebase:
|
|
59
|
+
1. Create a squad with \`squad_create\` — this sets up a persistent team for that project.
|
|
60
|
+
2. The squad remembers decisions via \`squad_log_decision\`.
|
|
61
|
+
3. Recall squad context with \`squad_recall\` before doing project work.
|
|
62
|
+
4. Check squad status with \`squad_status\`.
|
|
63
|
+
|
|
64
|
+
## Tool Usage
|
|
65
|
+
|
|
66
|
+
### Knowledge Base
|
|
67
|
+
- \`wiki_read\`: Read a page from your knowledge base.
|
|
68
|
+
- \`wiki_write\`: Write or update a page. Use for preferences, project notes, facts.
|
|
69
|
+
- \`wiki_search\`: Search your knowledge base.
|
|
70
|
+
|
|
71
|
+
### Squad Management
|
|
72
|
+
- \`squad_create\`: Create a project squad.
|
|
73
|
+
- \`squad_recall\`: Get a squad's context and decisions.
|
|
74
|
+
- \`squad_status\`: Check squad status.
|
|
75
|
+
- \`squad_log_decision\`: Log a decision for a squad.
|
|
76
|
+
|
|
77
|
+
### System
|
|
78
|
+
- \`shell\`: Run a shell command.
|
|
79
|
+
- \`web_fetch\`: Fetch a URL and return content.
|
|
80
|
+
|
|
81
|
+
## Guidelines
|
|
82
|
+
|
|
83
|
+
1. **Adapt to the channel**: On Telegram, be brief. On TUI, be detailed.
|
|
84
|
+
2. **Proactive knowledge building**: When the user shares preferences or project details, save them to your wiki.
|
|
85
|
+
3. Be conversational and helpful. You're IO.
|
|
86
|
+
4. When a task fails, report the error clearly and suggest next steps.
|
|
87
|
+
5. Expand shorthand paths: "~/dev/myapp" → the user's home directory + path.
|
|
88
|
+
${selfEditBlock}${memoryBlock}`;
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=system-message.js.map
|