@inceptionstack/roundhouse 0.5.11 → 0.5.13
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/package.json +1 -1
- package/src/gateway/gateway.ts +11 -1
- package/src/gateway/later-command.ts +1 -1
- package/src/gateway/model-command.ts +111 -20
- package/src/gateway/persona-inject.ts +94 -0
- package/src/gateway/soul.md +35 -0
- package/src/gateway/tools.md +2 -0
- package/src/gateway/user.md +10 -0
- package/src/provisioning/bundle.ts +2 -0
package/package.json
CHANGED
package/src/gateway/gateway.ts
CHANGED
|
@@ -23,13 +23,14 @@ import { isCommand as _isCmd, isCommandWithArgs as _isCmdArgs, resolveAgentThrea
|
|
|
23
23
|
import { saveAttachments as _saveAttachments, type AttachmentResult } from "./attachments";
|
|
24
24
|
import { handleStreaming as _handleStream } from "./streaming";
|
|
25
25
|
import { handleNew, handleRestart, handleUpdate, handleCompact, handleStatus, handleStop, handleVerbose, handleDoctor, handleCrons, type CommandContext } from "./commands";
|
|
26
|
-
import { handleModel } from "./model-command";
|
|
26
|
+
import { handleModel, handleModelAction, MODEL_ACTION_ID } from "./model-command";
|
|
27
27
|
import { handleLater } from "./later-command";
|
|
28
28
|
import { TelegramAdapter } from "../transports";
|
|
29
29
|
import type { TransportAdapter } from "../transports";
|
|
30
30
|
import { hostname } from "node:os";
|
|
31
31
|
import { join } from "node:path";
|
|
32
32
|
import { injectToolsSection } from "./tools-inject";
|
|
33
|
+
import { injectPersonaSection, loadPersona } from "./persona-inject";
|
|
33
34
|
|
|
34
35
|
/** Bot username for command suffix validation (set during gateway init) */
|
|
35
36
|
let _botUsername = "";
|
|
@@ -327,6 +328,14 @@ export class Gateway {
|
|
|
327
328
|
await handleOrAbort(thread, message);
|
|
328
329
|
});
|
|
329
330
|
|
|
331
|
+
// ── Load persona files at startup (cached for process lifetime) ───
|
|
332
|
+
loadPersona();
|
|
333
|
+
|
|
334
|
+
// ── Handle inline keyboard callbacks ───
|
|
335
|
+
this.chat.onAction(MODEL_ACTION_ID, async (event: any) => {
|
|
336
|
+
await handleModelAction({ value: event.value, thread: event.thread });
|
|
337
|
+
});
|
|
338
|
+
|
|
330
339
|
await this.chat.initialize();
|
|
331
340
|
|
|
332
341
|
const platforms = Object.keys(this.config.chat.adapters).join(", ");
|
|
@@ -403,6 +412,7 @@ export class Gateway {
|
|
|
403
412
|
|
|
404
413
|
// Inject tools section (after STT enrichment so voice-only messages get it too)
|
|
405
414
|
if (agentMessage.text) {
|
|
415
|
+
agentMessage.text = injectPersonaSection(agentMessage.text);
|
|
406
416
|
agentMessage.text = injectToolsSection(agentMessage.text);
|
|
407
417
|
}
|
|
408
418
|
|
|
@@ -33,7 +33,7 @@ function ensureLaterFile(): void {
|
|
|
33
33
|
|
|
34
34
|
export async function handleLater(ctx: LaterCommandContext): Promise<void> {
|
|
35
35
|
const { thread, text, postWithFallback } = ctx;
|
|
36
|
-
const idea = text.replace(/^\/later
|
|
36
|
+
const idea = text.replace(/^\/later(@\S+)?\s*/i, "").trim();
|
|
37
37
|
|
|
38
38
|
// No argument: show contents
|
|
39
39
|
if (!idea) {
|
|
@@ -3,25 +3,46 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Allows switching the default AI model from Telegram.
|
|
5
5
|
* Reads/writes ~/.pi/agent/settings.json (defaultProvider + defaultModel).
|
|
6
|
+
*
|
|
7
|
+
* When called without arguments, shows an inline keyboard with model buttons.
|
|
8
|
+
* When a button is clicked, the onAction handler applies the selection.
|
|
6
9
|
*/
|
|
7
10
|
|
|
8
11
|
import { homedir } from "node:os";
|
|
9
12
|
import { join } from "node:path";
|
|
10
|
-
import { readFileSync, writeFileSync } from "node:fs";
|
|
13
|
+
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
11
14
|
|
|
12
15
|
/** Known model aliases → Bedrock model IDs */
|
|
13
|
-
const MODEL_ALIASES: Record<string, { provider: string; model: string; label: string }> = {
|
|
14
|
-
|
|
15
|
-
"opus-4.6": { provider: "amazon-bedrock", model: "us.anthropic.claude-opus-4-6", label: "Claude Opus 4.6" },
|
|
16
|
+
export const MODEL_ALIASES: Record<string, { provider: string; model: string; label: string }> = {
|
|
17
|
+
// Anthropic Claude
|
|
16
18
|
"opus-4.7": { provider: "amazon-bedrock", model: "us.anthropic.claude-opus-4-7", label: "Claude Opus 4.7" },
|
|
19
|
+
"opus": { provider: "amazon-bedrock", model: "us.anthropic.claude-opus-4-6", label: "Claude Opus 4.6" },
|
|
17
20
|
"sonnet": { provider: "amazon-bedrock", model: "us.anthropic.claude-sonnet-4-6", label: "Claude Sonnet 4.6" },
|
|
18
|
-
"sonnet-4.6": { provider: "amazon-bedrock", model: "us.anthropic.claude-sonnet-4-6", label: "Claude Sonnet 4.6" },
|
|
19
21
|
"haiku": { provider: "amazon-bedrock", model: "us.anthropic.claude-haiku-4-5", label: "Claude Haiku 4.5" },
|
|
20
|
-
|
|
22
|
+
// DeepSeek
|
|
23
|
+
"deepseek": { provider: "amazon-bedrock", model: "us.deepseek.r1-v1:0", label: "DeepSeek R1" },
|
|
24
|
+
// Meta Llama
|
|
25
|
+
"llama": { provider: "amazon-bedrock", model: "us.meta.llama4-maverick-17b-instruct-v1:0", label: "Llama 4 Maverick" },
|
|
26
|
+
// Amazon Nova
|
|
27
|
+
"nova-pro": { provider: "amazon-bedrock", model: "us.amazon.nova-pro-v1:0", label: "Amazon Nova Pro" },
|
|
28
|
+
// Mistral
|
|
29
|
+
"mistral": { provider: "amazon-bedrock", model: "us.mistral.mistral-large-2411-v1:0", label: "Mistral Large" },
|
|
21
30
|
};
|
|
22
31
|
|
|
32
|
+
/** Models shown in the inline keyboard (max 8, ordered by preference) */
|
|
33
|
+
const KEYBOARD_MODELS = [
|
|
34
|
+
"opus-4.7", "opus", "sonnet", "haiku",
|
|
35
|
+
"deepseek", "llama", "nova-pro", "mistral",
|
|
36
|
+
] as const;
|
|
37
|
+
|
|
38
|
+
/** Action ID for model selection callbacks */
|
|
39
|
+
export const MODEL_ACTION_ID = "model_select";
|
|
40
|
+
|
|
23
41
|
const SETTINGS_PATH = join(homedir(), ".pi", "agent", "settings.json");
|
|
24
42
|
|
|
43
|
+
/** Callback data prefix used by @chat-adapter/telegram (coupled: if adapter changes this, buttons break) */
|
|
44
|
+
const CALLBACK_PREFIX = "chat:";
|
|
45
|
+
|
|
25
46
|
export interface ModelCommandContext {
|
|
26
47
|
thread: any;
|
|
27
48
|
text: string;
|
|
@@ -37,17 +58,37 @@ function readSettings(): Record<string, any> {
|
|
|
37
58
|
}
|
|
38
59
|
|
|
39
60
|
function writeSettings(settings: Record<string, any>): void {
|
|
61
|
+
mkdirSync(join(homedir(), ".pi", "agent"), { recursive: true });
|
|
40
62
|
writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2) + "\n");
|
|
41
63
|
}
|
|
42
64
|
|
|
43
65
|
function getCurrentModel(settings: Record<string, any>): string {
|
|
44
66
|
const provider = settings.defaultProvider ?? "unknown";
|
|
45
67
|
const model = settings.defaultModel ?? "unknown";
|
|
46
|
-
// Try to find a friendly label
|
|
47
68
|
for (const [alias, info] of Object.entries(MODEL_ALIASES)) {
|
|
48
|
-
if (info.provider === provider && info.model === model) return `${info.label}
|
|
69
|
+
if (info.provider === provider && info.model === model) return `${info.label}`;
|
|
49
70
|
}
|
|
50
|
-
return `${
|
|
71
|
+
return `${model}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function encodeCallbackData(actionId: string, value: string): string {
|
|
75
|
+
return `${CALLBACK_PREFIX}${JSON.stringify({ a: actionId, v: value })}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function buildInlineKeyboard(): { inline_keyboard: Array<Array<{ text: string; callback_data: string }>> } {
|
|
79
|
+
// Layout: 2 buttons per row for compact display
|
|
80
|
+
const buttons = KEYBOARD_MODELS.map(alias => {
|
|
81
|
+
const info = MODEL_ALIASES[alias];
|
|
82
|
+
return {
|
|
83
|
+
text: info.label,
|
|
84
|
+
callback_data: encodeCallbackData(MODEL_ACTION_ID, alias),
|
|
85
|
+
};
|
|
86
|
+
});
|
|
87
|
+
const rows: Array<Array<{ text: string; callback_data: string }>> = [];
|
|
88
|
+
for (let i = 0; i < buttons.length; i += 2) {
|
|
89
|
+
rows.push(buttons.slice(i, i + 2));
|
|
90
|
+
}
|
|
91
|
+
return { inline_keyboard: rows };
|
|
51
92
|
}
|
|
52
93
|
|
|
53
94
|
export async function handleModel(ctx: ModelCommandContext): Promise<void> {
|
|
@@ -57,31 +98,61 @@ export async function handleModel(ctx: ModelCommandContext): Promise<void> {
|
|
|
57
98
|
|
|
58
99
|
const settings = readSettings();
|
|
59
100
|
|
|
60
|
-
// No argument: show
|
|
101
|
+
// No argument: show inline keyboard
|
|
61
102
|
if (!target) {
|
|
62
103
|
const current = getCurrentModel(settings);
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
104
|
+
const msgText = `🤖 Current model: <b>${current}</b>\n\nSelect a model:`;
|
|
105
|
+
|
|
106
|
+
// Try to send with inline keyboard via telegramFetch
|
|
107
|
+
const adapter = thread?.adapter;
|
|
108
|
+
if (adapter?.telegramFetch) {
|
|
109
|
+
const chatId = thread?.platformThreadId?.split(":")?.[1] ?? thread?.id?.split(":")?.[1];
|
|
110
|
+
if (chatId) {
|
|
111
|
+
try {
|
|
112
|
+
await adapter.telegramFetch("sendMessage", {
|
|
113
|
+
chat_id: chatId,
|
|
114
|
+
text: msgText,
|
|
115
|
+
parse_mode: "HTML",
|
|
116
|
+
reply_markup: buildInlineKeyboard(),
|
|
117
|
+
});
|
|
118
|
+
return;
|
|
119
|
+
} catch (err) {
|
|
120
|
+
console.warn("[roundhouse] /model inline keyboard failed, falling back:", (err as Error).message);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
67
124
|
|
|
125
|
+
// Fallback: plain text
|
|
126
|
+
const aliases = KEYBOARD_MODELS.map(a => ` \`${a}\` → ${MODEL_ALIASES[a].label}`).join("\n");
|
|
68
127
|
await postWithFallback(thread, `🤖 *Current model:* ${current}\n\n*Available:*\n${aliases}\n\n_Usage:_ \`/model sonnet\``);
|
|
69
128
|
return;
|
|
70
129
|
}
|
|
71
130
|
|
|
72
|
-
// Resolve alias
|
|
131
|
+
// Resolve alias
|
|
132
|
+
await applyModelSelection(target, settings, thread, postWithFallback);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Apply a model selection (used by both /model <arg> and inline keyboard callback).
|
|
137
|
+
*/
|
|
138
|
+
export async function applyModelSelection(
|
|
139
|
+
target: string,
|
|
140
|
+
settings: Record<string, any> | null,
|
|
141
|
+
thread: any,
|
|
142
|
+
postWithFallback: (thread: any, text: string) => Promise<void>,
|
|
143
|
+
): Promise<void> {
|
|
144
|
+
if (!settings) settings = readSettings();
|
|
145
|
+
|
|
73
146
|
const resolved = MODEL_ALIASES[target];
|
|
74
147
|
if (!resolved) {
|
|
75
|
-
// Check if it looks like a full model ID (contains a dot or slash)
|
|
76
148
|
if (target.includes(".") || target.includes("/")) {
|
|
77
|
-
// Use as-is with current provider
|
|
78
149
|
const provider = settings.defaultProvider ?? "amazon-bedrock";
|
|
79
150
|
settings.defaultModel = target;
|
|
80
151
|
settings.defaultProvider = provider;
|
|
81
152
|
writeSettings(settings);
|
|
82
|
-
await postWithFallback(thread, `✅ Model set to: \`${provider}/${target}
|
|
153
|
+
await postWithFallback(thread, `✅ Model set to: \`${provider}/${target}\``);
|
|
83
154
|
} else {
|
|
84
|
-
const aliases = Object.keys(MODEL_ALIASES).
|
|
155
|
+
const aliases = Object.keys(MODEL_ALIASES).join(", ");
|
|
85
156
|
await postWithFallback(thread, `❌ Unknown model: \`${target}\`\n\nAvailable: ${aliases}`);
|
|
86
157
|
}
|
|
87
158
|
return;
|
|
@@ -91,6 +162,26 @@ export async function handleModel(ctx: ModelCommandContext): Promise<void> {
|
|
|
91
162
|
settings.defaultModel = resolved.model;
|
|
92
163
|
writeSettings(settings);
|
|
93
164
|
|
|
94
|
-
await postWithFallback(thread, `✅
|
|
165
|
+
await postWithFallback(thread, `✅ Switched to *${resolved.label}*`);
|
|
95
166
|
console.log(`[roundhouse] /model: switched to ${resolved.provider}/${resolved.model}`);
|
|
96
167
|
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Handle inline keyboard callback for model selection.
|
|
171
|
+
* Call this from chat.onAction(MODEL_ACTION_ID, ...).
|
|
172
|
+
*/
|
|
173
|
+
export async function handleModelAction(event: {
|
|
174
|
+
value?: string;
|
|
175
|
+
thread: any;
|
|
176
|
+
}): Promise<void> {
|
|
177
|
+
const alias = event.value;
|
|
178
|
+
if (!alias || !MODEL_ALIASES[alias]) return;
|
|
179
|
+
|
|
180
|
+
const postFn = async (_t: any, text: string) => {
|
|
181
|
+
if (!event.thread) return;
|
|
182
|
+
try { await event.thread.post({ markdown: text }); }
|
|
183
|
+
catch { try { await event.thread.post(text); } catch {} }
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
await applyModelSelection(alias, null, event.thread, postFn);
|
|
187
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gateway/persona-inject.ts — Inject <persona> section into agent prompts
|
|
3
|
+
*
|
|
4
|
+
* Reads user.md and soul.md (user-customized or bundled defaults) and
|
|
5
|
+
* prepends them as a structured section so the agent has identity and
|
|
6
|
+
* user context on every turn.
|
|
7
|
+
*
|
|
8
|
+
* Cached with mtime-based invalidation: stat() on each turn (~0.1ms),
|
|
9
|
+
* only re-reads if files have been modified since last load.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { readFileSync, statSync } from "node:fs";
|
|
13
|
+
import { join, dirname } from "node:path";
|
|
14
|
+
import { fileURLToPath } from "node:url";
|
|
15
|
+
import { ROUNDHOUSE_DIR } from "../config";
|
|
16
|
+
|
|
17
|
+
let cachedPersona: string | null = null;
|
|
18
|
+
let lastMtime = 0;
|
|
19
|
+
|
|
20
|
+
const SOUL_PATH = join(ROUNDHOUSE_DIR, "soul.md");
|
|
21
|
+
const USER_PATH = join(ROUNDHOUSE_DIR, "user.md");
|
|
22
|
+
|
|
23
|
+
function getMaxMtime(): number {
|
|
24
|
+
let max = 0;
|
|
25
|
+
try { max = Math.max(max, statSync(SOUL_PATH).mtimeMs); } catch {}
|
|
26
|
+
try { max = Math.max(max, statSync(USER_PATH).mtimeMs); } catch {}
|
|
27
|
+
return max;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function loadFile(filename: string): string {
|
|
31
|
+
const userPath = join(ROUNDHOUSE_DIR, filename);
|
|
32
|
+
const bundledPath = join(dirname(fileURLToPath(import.meta.url)), filename);
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
return readFileSync(userPath, "utf8");
|
|
36
|
+
} catch {
|
|
37
|
+
try {
|
|
38
|
+
return readFileSync(bundledPath, "utf8");
|
|
39
|
+
} catch {
|
|
40
|
+
return "";
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function buildPersona(): string {
|
|
46
|
+
const soul = loadFile("soul.md").trim();
|
|
47
|
+
const user = loadFile("user.md").trim();
|
|
48
|
+
|
|
49
|
+
if (!soul && !user) return "";
|
|
50
|
+
|
|
51
|
+
const parts: string[] = [];
|
|
52
|
+
if (soul) parts.push(soul);
|
|
53
|
+
if (user) parts.push(user);
|
|
54
|
+
return parts.join("\n\n---\n\n");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Load persona files and cache the result.
|
|
59
|
+
* Call at gateway startup to eagerly load.
|
|
60
|
+
*/
|
|
61
|
+
export function loadPersona(): void {
|
|
62
|
+
cachedPersona = buildPersona();
|
|
63
|
+
lastMtime = getMaxMtime();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Reload persona from disk. Call after agent edits user.md/soul.md
|
|
68
|
+
* (e.g. from an IPC handler or post-tool-execution hook).
|
|
69
|
+
*/
|
|
70
|
+
export function reloadPersona(): void {
|
|
71
|
+
cachedPersona = buildPersona();
|
|
72
|
+
lastMtime = getMaxMtime();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Prepend a <persona> section to the prompt text.
|
|
77
|
+
* Only injects if soul.md or user.md have content.
|
|
78
|
+
* Auto-reloads if files have been modified since last load.
|
|
79
|
+
*/
|
|
80
|
+
export function injectPersonaSection(text: string): string {
|
|
81
|
+
if (cachedPersona === null) {
|
|
82
|
+
loadPersona();
|
|
83
|
+
} else {
|
|
84
|
+
// Cheap mtime check — auto-reload if agent edited the files
|
|
85
|
+
const currentMtime = getMaxMtime();
|
|
86
|
+
if (currentMtime !== lastMtime) {
|
|
87
|
+
reloadPersona();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (!cachedPersona) return text;
|
|
91
|
+
// Escape any literal </persona> in content to prevent XML injection
|
|
92
|
+
const safe = cachedPersona.replace(/<\/persona>/gi, "</persona>");
|
|
93
|
+
return `<persona>\n${safe}\n</persona>\n\n${text}`;
|
|
94
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Who You Are
|
|
2
|
+
|
|
3
|
+
_You're not a chatbot. You're a technical partner._
|
|
4
|
+
|
|
5
|
+
## Core Identity
|
|
6
|
+
|
|
7
|
+
**Name:** Loki
|
|
8
|
+
**Role:** Senior engineer and ops partner. You help build, deploy, debug, and maintain software and infrastructure. You have opinions and you share them.
|
|
9
|
+
|
|
10
|
+
## Core Truths
|
|
11
|
+
|
|
12
|
+
**Be genuinely helpful, not performatively helpful.** Skip the filler — just help. Actions speak louder than words.
|
|
13
|
+
|
|
14
|
+
**Have opinions.** When something is a bad pattern, say so. When there's a better approach, recommend it. You're not a yes-machine.
|
|
15
|
+
|
|
16
|
+
**Be resourceful before asking.** Check the docs. Read the file. Try things. _Then_ ask if you're stuck.
|
|
17
|
+
|
|
18
|
+
**Earn trust through competence.** You have access to tools, shell, and infrastructure. Use them wisely. Be careful with destructive operations. Be bold with read operations.
|
|
19
|
+
|
|
20
|
+
**Think holistically.** Consider the broader context — architecture, maintainability, security, user experience.
|
|
21
|
+
|
|
22
|
+
## Boundaries
|
|
23
|
+
|
|
24
|
+
- Ask before destructive operations (deletions, config changes with blast radius)
|
|
25
|
+
- Read freely — list, describe, get operations are safe
|
|
26
|
+
- Private things stay private
|
|
27
|
+
- Never send half-baked replies
|
|
28
|
+
|
|
29
|
+
## Vibe
|
|
30
|
+
|
|
31
|
+
Direct, technical, concise. Think senior engineer talking to senior engineer. Thorough when it matters, brief when it doesn't. No corporate speak.
|
|
32
|
+
|
|
33
|
+
## Continuity
|
|
34
|
+
|
|
35
|
+
Each session, you wake up fresh. Your workspace files _are_ your memory. Read them. Update them.
|
package/src/gateway/tools.md
CHANGED
|
@@ -10,6 +10,8 @@ Do NOT create files directly in `~/` or pollute the home directory. Use subdirec
|
|
|
10
10
|
- `~/.roundhouse/workspace/later.md` — ideas saved via `/later`
|
|
11
11
|
- `~/.roundhouse/workspace/<project>/` — project-specific files if needed
|
|
12
12
|
|
|
13
|
+
> 💡 When looking for things to work on, check `~/.roundhouse/workspace/later.md` for saved ideas and tasks.
|
|
14
|
+
|
|
13
15
|
## roundhouse cron add
|
|
14
16
|
|
|
15
17
|
Schedule recurring or one-shot jobs. The user may ask you to "remind me", "check every X", "do Y later", or "schedule Z".
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# About Your Human
|
|
2
|
+
|
|
3
|
+
- **Name:** (not yet set)
|
|
4
|
+
- **What to call them:** (use their Telegram username until they tell you otherwise)
|
|
5
|
+
- **Timezone:** UTC
|
|
6
|
+
- **Notes:** (learn about them through conversation)
|
|
7
|
+
|
|
8
|
+
## Preferences
|
|
9
|
+
|
|
10
|
+
- (Will be filled in as you learn what they prefer)
|
|
@@ -320,6 +320,8 @@ export function provisionWorkspaceFiles(opts: ProvisionOpts = {}): void {
|
|
|
320
320
|
// Files to provision: [bundled filename, target filename]
|
|
321
321
|
const files: [string, string][] = [
|
|
322
322
|
["tools.md", "tools.md"],
|
|
323
|
+
["soul.md", "soul.md"],
|
|
324
|
+
["user.md", "user.md"],
|
|
323
325
|
];
|
|
324
326
|
|
|
325
327
|
try {
|