Skip to content

Commit 2372b32

Browse files
committed
Release 1.0.1
1 parent 6b0662b commit 2372b32

7 files changed

Lines changed: 219 additions & 77 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": "1.0.0",
5+
"version": "1.0.1",
66
"authors": [
77
{
88
"name": "Derek Hearst",
@@ -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/1.0.0/foundry-ai.zip",
33+
"download": "https://github.com/derekhearst/FoundryAI/releases/download/1.0.1/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": "1.0.0",
3+
"version": "1.0.1",
44
"description": "FoundryAI - AI-powered DM assistant for Foundry VTT",
55
"type": "module",
66
"scripts": {

src/core/collection-reader.ts

Lines changed: 1 addition & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -310,15 +310,12 @@ export class CollectionReader {
310310
const scene = game.scenes?.contents?.find((s) => s.active)
311311
if (!scene) return 'No active scene.'
312312

313-
const parts = [`Active Scene: ${scene.name}`]
313+
const parts = [`Active Scene: ${scene.name} (id: ${scene.id})`]
314314

315315
// Scene metadata
316316
if (scene.grid) {
317317
parts.push(`Grid: ${scene.grid.distance || 5}${scene.grid.units || 'ft'} per square`)
318318
}
319-
if (scene.darkness != null && scene.darkness > 0) {
320-
parts.push(`Darkness Level: ${Math.round(scene.darkness * 100)}%`)
321-
}
322319
if (scene.weather) {
323320
parts.push(`Weather: ${scene.weather}`)
324321
}
@@ -362,36 +359,6 @@ export class CollectionReader {
362359
parts.push(`\nTokens (${scene.tokens.size}):\n${tokenDetails.join('\n')}`)
363360
}
364361

365-
// Map notes with text content
366-
if (scene.notes?.size) {
367-
const noteDetails: string[] = []
368-
for (const note of scene.notes.values()) {
369-
const label = (note as any).label || (note as any).text || note.name || 'Unnamed'
370-
noteDetails.push(`- ${label} at (${note.x}, ${note.y})`)
371-
}
372-
parts.push(`\nMap Notes (${scene.notes.size}):\n${noteDetails.join('\n')}`)
373-
}
374-
375-
// Lights
376-
if (scene.lights?.size) {
377-
parts.push(`Lights: ${scene.lights.size}`)
378-
}
379-
380-
// Walls/Doors
381-
if (scene.walls?.size) {
382-
let doors = 0
383-
let openDoors = 0
384-
for (const wall of scene.walls.values()) {
385-
if ((wall as any).door === 1 || (wall as any).door === 2) {
386-
doors++
387-
if ((wall as any).ds === 1) openDoors++
388-
}
389-
}
390-
if (doors > 0) {
391-
parts.push(`Doors: ${doors} (${openDoors} open)`)
392-
}
393-
}
394-
395362
return parts.join('\n')
396363
}
397364

src/core/system-prompt.ts

Lines changed: 93 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import { getSetting } from '../settings'
88
import { collectionReader } from './collection-reader'
9+
import { getSubfolderId } from './folder-manager'
910

1011
const MODULE_ID = 'foundry-ai'
1112

@@ -279,11 +280,11 @@ function getWorldContext(): string | null {
279280
/* ignore */
280281
}
281282

282-
// Available actors inventory
283+
// AI-created notes (full content from FoundryAI/Notes folder)
283284
try {
284-
const actorIndex = getActorInventory()
285-
if (actorIndex) {
286-
parts.push(actorIndex)
285+
const notesContent = getNotesContent()
286+
if (notesContent) {
287+
parts.push(notesContent)
287288
}
288289
} catch {
289290
/* ignore */
@@ -297,21 +298,51 @@ function getWorldContext(): string | null {
297298
function getPlayerCharacters(): string[] {
298299
if (!game.actors) return []
299300

301+
const playerFolderId = getSetting('playerFolder')
300302
const pcs: string[] = []
303+
301304
for (const actor of game.actors.values()) {
302305
if (actor.type !== 'character') continue
303-
if (!actor.hasPlayerOwner) continue
306+
307+
// If playerFolder is set, only include actors from that folder
308+
if (playerFolderId) {
309+
const allFolderIds = collectionReader.resolveWithChildren([playerFolderId])
310+
if (!actor.folder || !allFolderIds.includes(actor.folder.id)) continue
311+
} else {
312+
// Fallback: only include player-owned characters
313+
if (!actor.hasPlayerOwner) continue
314+
}
304315

305316
const system = actor.system as Record<string, any>
306-
const details: string[] = [`- **${actor.name}**`]
317+
const details: string[] = [`- **${actor.name}** (id: ${actor.id})`]
307318

308319
// Try to get class/race/level info (system-agnostic)
309-
if (system?.details?.race) details.push(`Race: ${system.details.race}`)
310-
if (system?.details?.class) details.push(`Class: ${system.details.class}`)
311-
if (system?.details?.level) details.push(`Level: ${system.details.level}`)
320+
if (system?.details?.race) {
321+
const raceName = typeof system.details.race === 'string' ? system.details.race : system.details.race?.name || ''
322+
if (raceName) details.push(`Race: ${raceName}`)
323+
}
324+
325+
// Classes (5e)
326+
try {
327+
if ((actor as any).classes && typeof (actor as any).classes === 'object') {
328+
const classEntries = Object.values((actor as any).classes) as any[]
329+
if (classEntries.length > 0) {
330+
const classStr = classEntries
331+
.map((c: any) => `${c.name || c.identifier} ${c.system?.levels || ''}`)
332+
.join(' / ')
333+
details.push(`Class: ${classStr}`)
334+
}
335+
}
336+
} catch {
337+
/* ignore */
338+
}
339+
312340
if (system?.attributes?.hp) {
313341
details.push(`HP: ${system.attributes.hp.value}/${system.attributes.hp.max}`)
314342
}
343+
if (system?.attributes?.ac) {
344+
details.push(`AC: ${system.attributes.ac.value ?? system.attributes.ac.flat ?? '?'}`)
345+
}
315346

316347
pcs.push(details.join(' | '))
317348
}
@@ -320,39 +351,63 @@ function getPlayerCharacters(): string[] {
320351
}
321352

322353
/**
323-
* Build a compact inventory of all journal entries grouped by folder.
324-
* This lets the LLM know what's available so it can fetch relevant docs.
354+
* Build a compact inventory of all journal entries — just IDs and names.
355+
* Only includes journals from folders the user has granted access to.
325356
*/
326357
function getJournalInventory(): string | null {
327358
if (!game.journal || game.journal.size === 0) return null
328359

329-
// Group journals by folder
330-
const byFolder = new Map<string, Array<{ id: string; name: string; pages: number }>>()
331-
332-
for (const entry of game.journal.values()) {
333-
const folderName = entry.folder?.name || 'Uncategorized'
334-
if (!byFolder.has(folderName)) byFolder.set(folderName, [])
335-
byFolder.get(folderName)!.push({
336-
id: entry.id,
337-
name: entry.name,
338-
pages: entry.pages?.size || 0,
339-
})
340-
}
360+
const allowedFolders = getSetting('journalFolders') || []
361+
const allAllowedFolderIds = allowedFolders.length > 0 ? collectionReader.resolveWithChildren(allowedFolders) : null // null = no restriction
341362

342363
const lines: string[] = ['## Available Journals']
343-
lines.push('Use search_journals or get_journal (with the ID) to read any of these:\n')
364+
lines.push(
365+
'Call get_journal with the ID to read any of these. ALWAYS read the relevant journal before answering campaign questions.\n',
366+
)
344367

345-
for (const [folder, entries] of byFolder) {
346-
lines.push(`### 📁 ${folder}`)
347-
for (const e of entries) {
348-
lines.push(`- ${e.name} (id: ${e.id}, ${e.pages} page${e.pages !== 1 ? 's' : ''})`)
368+
let count = 0
369+
for (const entry of game.journal.values()) {
370+
// Filter to allowed folders if restrictions are set
371+
if (allAllowedFolderIds !== null) {
372+
if (!entry.folder || !allAllowedFolderIds.includes(entry.folder.id)) continue
349373
}
350-
lines.push('')
374+
lines.push(`- ${entry.name} (id: ${entry.id})`)
375+
count++
351376
}
352377

378+
if (count === 0) return null
353379
return lines.join('\n')
354380
}
355381

382+
/**
383+
* Get full content of all journals in the FoundryAI/Notes folder.
384+
* These are notes the AI itself created — it should always have this context.
385+
*/
386+
function getNotesContent(): string | null {
387+
const notesFolderId = getSubfolderId('notes')
388+
if (!notesFolderId) return null
389+
if (!game.journal || game.journal.size === 0) return null
390+
391+
const parts: string[] = ['## Your Notes (FoundryAI/Notes)']
392+
parts.push('These are notes you previously created. Reference them when relevant.\n')
393+
394+
let count = 0
395+
for (const entry of game.journal.values()) {
396+
if (!entry.folder || entry.folder.id !== notesFolderId) continue
397+
398+
const content = collectionReader.getJournalContent(entry.id)
399+
if (!content) continue
400+
401+
parts.push(`### ${entry.name} (id: ${entry.id})`)
402+
parts.push(content)
403+
parts.push('')
404+
count++
405+
}
406+
407+
if (count === 0) return null
408+
return parts.join('\n')
409+
}
410+
356411
/**
357412
* Build a compact inventory of all actors grouped by folder.
358413
*/
@@ -417,9 +472,10 @@ const TOOL_INSTRUCTIONS = `## Using Tools — MANDATORY
417472
You have access to tools that let you interact with the Foundry VTT world. **You MUST use these tools before generating any response about campaign-specific content.** Do NOT rely on your training data or the "Relevant Context" section alone — always verify and enrich your answer by calling the appropriate tools first.
418473
419474
### Core Tools (Knowledge & Content)
420-
- **search_journals**: Semantically search indexed journal entries (sourcebooks, lore, notes). Call this for ANY question about locations, NPCs, plot points, items, factions, or events. **This tool automatically returns the FULL content of every matching journal** — you do NOT need to call get_journal afterwards. Each result includes a uuidRef field you MUST use when citing the source.
421-
- **search_actors**: Semantically search indexed actors (NPCs, monsters, characters). Call this for ANY question about a character, creature, or NPC.
422-
- **get_journal / get_actor**: Retrieve FULL content by ID. Useful for direct lookups when you already know the ID.
475+
- **get_journal**: Retrieve the FULL content of a journal entry by ID. **This is your primary tool.** The system prompt lists all available journals with their IDs — use this to read the relevant journal(s) before answering any campaign question. You can call it multiple times to read several journals.
476+
- **search_journals**: Semantically search indexed journal entries when you're not sure which journal contains the information. Returns full content of matching journals with uuidRef citations. Use this when the journal name doesn't obviously match or when you need to discover which journals cover a topic.
477+
- **search_actors**: Semantically search indexed actors (NPCs, monsters, characters). Call this for ANY question about a character, creature, or NPC. Use this to find relevant actors by name or description.
478+
- **get_actor**: Retrieve full actor details by ID. Use when you already know the actor's ID (e.g. from the Player Characters list or a previous search).
423479
- **create_journal / update_journal**: Create or modify journal entries (quests, notes, recaps, summaries).
424480
- **list_journals_in_folder / list_folders**: Browse the world's organizational structure.
425481
- **get_scene_info**: Check what's happening in the current scene (tokens, notes, lights, etc.).
@@ -469,11 +525,11 @@ You have access to tools that let you interact with the Foundry VTT world. **You
469525
- **create_measured_template**: Place an area-of-effect template (circle, cone, ray, rect).
470526
471527
### CRITICAL RULES — Read Carefully
472-
1. **ALWAYS call search_journals and/or search_actors BEFORE answering any question about campaign content.** This includes questions about lore, NPCs, locations, quests, factions, items, encounters, or story events. No exceptions.
473-
2. **search_journals already returns the FULL content of each matching journal.** You do NOT need to call get_journal afterwards — the complete text is included in the results. Use get_journal only for direct lookups by ID when you already know which document you need.
474-
3. **ALWAYS cite your sources.** Every search result includes a uuidRef field (e.g. @UUID[JournalEntry.abc123]{Document Name}). You MUST include this reference in your response when using information from that journal. This lets the DM verify your answer and prevents hallucination.
475-
4. **Never fabricate campaign-specific facts.** If your tools return no results, say so explicitly: "I didn't find anything in the indexed journals about X. Would you like me to search differently or create a note about it?"
476-
5. **Chain tool calls when needed.** For example: search_journalssearch_actors → get_actor. Use as many calls as necessary to gather complete information.
528+
1. **ALWAYS read the relevant journal(s) before answering any question about campaign content.** Check the "Available Journals" list in the system prompt. If the journal name clearly matches the topic, call get_journal with its ID. If you're not sure which journal covers the topic, call search_journals to find it. You can (and should) call get_journal multiple times to read several journals.
529+
2. **Use search_journals and search_actors for discovery.** When you don't know which journal or actor has the information, search first, then read the full content. For actors (NPCs, monsters), always use search_actors — the Player Characters in the system prompt only cover the party.
530+
3. **ALWAYS cite your sources with @UUID references.** When you use information from a journal, include @UUID[JournalEntry.{id}]{Journal Name} in your response. For actors, use @UUID[Actor.{id}]{Actor Name}. You get IDs from the Available Journals list, Player Characters list, or from tool results. This lets the DM click through to verify.
531+
4. **Never fabricate campaign-specific facts.** If no journal covers the topic, say so explicitly: "I didn't find anything in the journals about X. Would you like me to search differently or create a note about it?"
532+
5. **Chain tool calls when needed.** For example: get_journalget_journal (another one) → search_actors. Read as many journals as needed to give a complete answer.
477533
6. **Use create_journal** when the DM asks you to write up quests, session notes, recaps, or summaries.
478534
7. **Journal folder routing — ALWAYS follow these rules when creating journals:**
479535
- **Session recaps** → folder_name: "Sessions" (inside the FoundryAI folder)

src/settings.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ export interface FoundryAISettings {
2525
autoIndex: boolean
2626
enableTools: boolean
2727
showSidebarTab: boolean
28+
enableRAG: boolean
29+
playerFolder: string
2830
enableSceneTools: boolean
2931
enableDiceTools: boolean
3032
enableTokenTools: boolean
@@ -193,6 +195,24 @@ export function registerSettings(): void {
193195
default: true,
194196
})
195197

198+
game.settings.register(MODULE_ID, 'enableRAG', {
199+
name: 'Enable RAG Context',
200+
hint: 'Inject relevant document excerpts into each prompt via embedding search. Increases token usage but gives the AI pre-loaded context.',
201+
scope: 'world',
202+
config: false,
203+
type: Boolean,
204+
default: false,
205+
})
206+
207+
game.settings.register(MODULE_ID, 'playerFolder', {
208+
name: 'Player Character Folder',
209+
hint: 'The actor folder containing player characters. These will be included in every prompt so the AI knows the party.',
210+
scope: 'world',
211+
config: false,
212+
type: String,
213+
default: '',
214+
})
215+
196216
game.settings.register(MODULE_ID, 'showSidebarTab', {
197217
name: 'FOUNDRYAI.Settings.ShowSidebarTab',
198218
hint: 'FOUNDRYAI.Settings.ShowSidebarTabHint',

0 commit comments

Comments
 (0)