Skip to content

Commit 2ac6aec

Browse files
committed
Release 0.6.0
1 parent da3da95 commit 2ac6aec

5 files changed

Lines changed: 165 additions & 15 deletions

File tree

module.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"id": "foundry-ai",
33
"title": "FoundryAI",
44
"description": "AI-powered DM assistant with chat interface, RAG over journals/actors, session recaps, quest tracking, and more.",
5-
"version": "0.5.2",
5+
"version": "0.6.0",
66
"authors": [
77
{
88
"name": "Derek",
@@ -30,7 +30,7 @@
3030
"socket": true,
3131
"url": "https://github.com/derekhearst/FoundryAI",
3232
"manifest": "https://github.com/derekhearst/FoundryAI/releases/latest/download/module.json",
33-
"download": "https://github.com/derekhearst/FoundryAI/releases/download/0.5.2/foundry-ai.zip",
33+
"download": "https://github.com/derekhearst/FoundryAI/releases/download/0.6.0/foundry-ai.zip",
3434
"license": "MIT",
3535
"readme": "https://github.com/derekhearst/FoundryAI/blob/main/README.md"
3636
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "foundry-ai",
3-
"version": "0.5.2",
3+
"version": "0.6.0",
44
"description": "FoundryAI - AI-powered DM assistant for Foundry VTT",
55
"type": "module",
66
"scripts": {

src/core/system-prompt.ts

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,26 @@ function getWorldContext(): string | null {
7878
/* ignore */
7979
}
8080

81+
// Available journals inventory
82+
try {
83+
const journalIndex = getJournalInventory()
84+
if (journalIndex) {
85+
parts.push(journalIndex)
86+
}
87+
} catch {
88+
/* ignore */
89+
}
90+
91+
// Available actors inventory
92+
try {
93+
const actorIndex = getActorInventory()
94+
if (actorIndex) {
95+
parts.push(actorIndex)
96+
}
97+
} catch {
98+
/* ignore */
99+
}
100+
81101
if (parts.length === 0) return null
82102

83103
return `# Campaign Context\n\n${parts.join('\n\n')}`
@@ -108,6 +128,72 @@ function getPlayerCharacters(): string[] {
108128
return pcs
109129
}
110130

131+
/**
132+
* Build a compact inventory of all journal entries grouped by folder.
133+
* This lets the LLM know what's available so it can fetch relevant docs.
134+
*/
135+
function getJournalInventory(): string | null {
136+
if (!game.journal || game.journal.size === 0) return null
137+
138+
// Group journals by folder
139+
const byFolder = new Map<string, Array<{ id: string; name: string; pages: number }>>()
140+
141+
for (const entry of game.journal.values()) {
142+
const folderName = entry.folder?.name || 'Uncategorized'
143+
if (!byFolder.has(folderName)) byFolder.set(folderName, [])
144+
byFolder.get(folderName)!.push({
145+
id: entry.id,
146+
name: entry.name,
147+
pages: entry.pages?.size || 0,
148+
})
149+
}
150+
151+
const lines: string[] = ['## Available Journals']
152+
lines.push('Use search_journals or get_journal (with the ID) to read any of these:\n')
153+
154+
for (const [folder, entries] of byFolder) {
155+
lines.push(`### 📁 ${folder}`)
156+
for (const e of entries) {
157+
lines.push(`- ${e.name} (id: ${e.id}, ${e.pages} page${e.pages !== 1 ? 's' : ''})`)
158+
}
159+
lines.push('')
160+
}
161+
162+
return lines.join('\n')
163+
}
164+
165+
/**
166+
* Build a compact inventory of all actors grouped by folder.
167+
*/
168+
function getActorInventory(): string | null {
169+
if (!game.actors || game.actors.size === 0) return null
170+
171+
const byFolder = new Map<string, Array<{ id: string; name: string; type: string }>>()
172+
173+
for (const actor of game.actors.values()) {
174+
const folderName = actor.folder?.name || 'Uncategorized'
175+
if (!byFolder.has(folderName)) byFolder.set(folderName, [])
176+
byFolder.get(folderName)!.push({
177+
id: actor.id,
178+
name: actor.name,
179+
type: actor.type,
180+
})
181+
}
182+
183+
const lines: string[] = ['## Available Actors']
184+
lines.push('Use search_actors or get_actor (with the ID) to look up any of these:\n')
185+
186+
for (const [folder, actors] of byFolder) {
187+
lines.push(`### 📁 ${folder}`)
188+
for (const a of actors) {
189+
lines.push(`- ${a.name} (id: ${a.id}, type: ${a.type})`)
190+
}
191+
lines.push('')
192+
}
193+
194+
return lines.join('\n')
195+
}
196+
111197
// ---- Prompt Templates ----
112198

113199
const BASE_PROMPT = `You are **FoundryAI**, an expert AI Dungeon Master assistant integrated directly into Foundry Virtual Tabletop. You help the DM run their game by providing guidance, generating content, and managing game information.
@@ -156,6 +242,10 @@ You have access to tools that let you interact with the Foundry VTT world. **You
156242
4. **Chain tool calls when needed.** For example: search_journals → get_journal → search_actors → get_actor. Use as many calls as necessary to gather complete information.
157243
5. **Use create_journal** when the DM asks you to write up quests, session notes, recaps, or summaries.
158244
6. **Use get_scene_info** when asked about the current scene, map, or tokens.
245+
7. **Journal folder routing — ALWAYS follow these rules when creating journals:**
246+
- **Session recaps** → folder_name: "Sessions"
247+
- **Notes, stored data, quest logs, reminders, or any other created content** → folder_name: "Notes"
248+
- NEVER create journals in the root. Always specify the appropriate folder_name.
159249
160250
### When tools are NOT needed
161251
- General D&D rules questions (use training knowledge)

src/core/tool-system.ts

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [
9292
function: {
9393
name: 'create_journal',
9494
description:
95-
'Create a new journal entry in a specified folder. Use this for quest tracking, session recaps, notes, etc.',
95+
'Create a new journal entry in a specified folder. Session recaps MUST go in the "Sessions" folder. Notes and stored data MUST go in the "Notes" folder.',
9696
parameters: {
9797
type: 'object',
9898
properties: {
@@ -104,9 +104,14 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [
104104
type: 'string',
105105
description: 'The HTML content of the journal entry. Use proper HTML formatting.',
106106
},
107+
folder_name: {
108+
type: 'string',
109+
description:
110+
'The name of the journal folder to create in (e.g. "Sessions", "Notes"). The folder will be created if it does not exist.',
111+
},
107112
folder_id: {
108113
type: 'string',
109-
description: 'The folder ID to create the journal in. If omitted, creates in the root.',
114+
description: 'The folder ID to create the journal in. Prefer folder_name instead.',
110115
},
111116
},
112117
required: ['name', 'content'],
@@ -229,7 +234,7 @@ export async function executeTool(toolCall: ToolCall): Promise<string> {
229234
return handleGetActor(args.actor_id)
230235

231236
case 'create_journal':
232-
return await handleCreateJournal(args.name, args.content, args.folder_id)
237+
return await handleCreateJournal(args.name, args.content, args.folder_name, args.folder_id)
233238

234239
case 'update_journal':
235240
return await handleUpdateJournal(args.journal_id, args.content, args.page_name)
@@ -323,7 +328,31 @@ function handleGetActor(actorId: string): string {
323328
})
324329
}
325330

326-
async function handleCreateJournal(name: string, content: string, folderId?: string): Promise<string> {
331+
async function handleCreateJournal(
332+
name: string,
333+
content: string,
334+
folderName?: string,
335+
folderId?: string,
336+
): Promise<string> {
337+
// Resolve folder: prefer folderName, fall back to folderId
338+
let resolvedFolderId = folderId || null
339+
340+
if (folderName && !resolvedFolderId) {
341+
// Find existing folder by name
342+
let folder = game.folders?.find((f: any) => f.type === 'JournalEntry' && f.name === folderName)
343+
344+
// Create folder if it doesn't exist
345+
if (!folder) {
346+
folder = await Folder.create({
347+
name: folderName,
348+
type: 'JournalEntry',
349+
parent: null,
350+
} as any)
351+
}
352+
353+
resolvedFolderId = folder?.id || null
354+
}
355+
327356
const journalData: any = {
328357
name,
329358
pages: [
@@ -335,16 +364,17 @@ async function handleCreateJournal(name: string, content: string, folderId?: str
335364
],
336365
}
337366

338-
if (folderId) {
339-
journalData.folder = folderId
367+
if (resolvedFolderId) {
368+
journalData.folder = resolvedFolderId
340369
}
341370

342371
const journal = await JournalEntry.create(journalData)
343372
return JSON.stringify({
344373
success: true,
345374
id: journal.id,
346375
name: journal.name,
347-
message: `Created journal entry "${name}"`,
376+
folder: folderName || 'Root',
377+
message: `Created journal entry "${name}" in folder "${folderName || 'Root'}"`,
348378
})
349379
}
350380

src/module.ts

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -88,21 +88,51 @@ Hooks.once('ready', async () => {
8888
Hooks.on(`${MODULE_ID}.settingsChanged`, (key: string) => {
8989
if (key === 'apiKey') {
9090
const newKey = getSetting('apiKey')
91-
if (newKey) openRouterService.configure({
92-
apiKey: newKey,
93-
defaultModel: getSetting('chatModel'),
94-
embeddingModel: getSetting('embeddingModel'),
95-
})
91+
if (newKey)
92+
openRouterService.configure({
93+
apiKey: newKey,
94+
defaultModel: getSetting('chatModel'),
95+
embeddingModel: getSetting('embeddingModel'),
96+
})
9697
}
9798
})
9899

100+
// Ensure standard journal folders exist
101+
await ensureJournalFolders()
102+
99103
// Create/update the hotbar macro for easy access
100104
await ensureChatMacro()
101105

102106
// Notification
103107
ui.notifications.info('FoundryAI is ready! Use the hotbar macro or scene controls brain icon to chat.')
104108
})
105109

110+
// ---- Journal Folder Setup ----
111+
112+
/**
113+
* Ensure the standard FoundryAI journal folders exist: "Sessions" and "Notes".
114+
* Creates them if missing so the LLM always has valid targets.
115+
*/
116+
async function ensureJournalFolders() {
117+
try {
118+
const REQUIRED_FOLDERS = ['Sessions', 'Notes']
119+
120+
for (const folderName of REQUIRED_FOLDERS) {
121+
const exists = game.folders?.find((f: any) => f.type === 'JournalEntry' && f.name === folderName)
122+
if (!exists) {
123+
await Folder.create({
124+
name: folderName,
125+
type: 'JournalEntry',
126+
parent: null,
127+
} as any)
128+
console.log(`FoundryAI | Created journal folder: ${folderName}`)
129+
}
130+
}
131+
} catch (err) {
132+
console.error('FoundryAI | Failed to create journal folders:', err)
133+
}
134+
}
135+
106136
// ---- Macro Creation ----
107137

108138
/**

0 commit comments

Comments
 (0)