Skip to content

Commit b63dcc2

Browse files
committed
Release 0.10.0
1 parent a055b33 commit b63dcc2

9 files changed

Lines changed: 528 additions & 51 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.9.0",
5+
"version": "0.10.0",
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/0.9.0/foundry-ai.zip",
33+
"download": "https://github.com/derekhearst/FoundryAI/releases/download/0.10.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.9.0",
3+
"version": "0.10.0",
44
"description": "FoundryAI - AI-powered DM assistant for Foundry VTT",
55
"type": "module",
66
"scripts": {

src/core/chat-session-manager.ts

Lines changed: 42 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
/* ==========================================================================
22
Chat Session Manager
3-
Persists raw chat conversations as JournalEntry documents in a dedicated
4-
"FoundryAI Chat History" folder.
3+
Persists raw chat conversations as JournalEntry documents in the
4+
FoundryAI/Chat History subfolder (or FoundryAI/Actors for roleplay).
55
========================================================================== */
66

77
import type { LLMMessage } from './openrouter-service'
8+
import { getSubfolderId } from './folder-manager'
89

910
const MODULE_ID = 'foundry-ai'
10-
const CHAT_HISTORY_FOLDER_NAME = 'FoundryAI Chat History'
1111

1212
export interface ChatSession {
1313
id: string // JournalEntry document ID
@@ -17,6 +17,8 @@ export interface ChatSession {
1717
updatedAt: number // Epoch ms
1818
model: string // Model used for this session
1919
tokenCount?: number // Approximate total tokens
20+
actorId?: string // If this is an actor roleplay session
21+
actorName?: string // Display name of the actor
2022
}
2123

2224
export interface SessionSummary {
@@ -26,6 +28,8 @@ export interface SessionSummary {
2628
updatedAt: number
2729
messageCount: number
2830
model: string
31+
actorId?: string
32+
actorName?: string
2933
}
3034

3135
class ChatSessionManager {
@@ -40,22 +44,31 @@ class ChatSessionManager {
4044
if (folder) return folder
4145
}
4246

43-
// Find existing folder
47+
// Use the FoundryAI/Chat History subfolder
48+
const subfolderId = getSubfolderId('chatHistory')
49+
if (subfolderId) {
50+
const folder = game.folders?.get(subfolderId)
51+
if (folder) return folder
52+
}
53+
54+
// Find existing folder (legacy fallback)
4455
if (this.folderCache) {
4556
const cached = game.folders?.get(this.folderCache)
4657
if (cached) return cached
4758
}
4859

49-
const existing = game.folders?.find((f: Folder) => f.name === CHAT_HISTORY_FOLDER_NAME && f.type === 'JournalEntry')
60+
const existing = game.folders?.find(
61+
(f: Folder) => f.name === 'Chat History' && f.type === 'JournalEntry',
62+
)
5063

5164
if (existing) {
5265
this.folderCache = existing.id
5366
return existing
5467
}
5568

56-
// Create folder
69+
// Create folder as last resort
5770
const folder = await Folder.create({
58-
name: CHAT_HISTORY_FOLDER_NAME,
71+
name: 'Chat History',
5972
type: 'JournalEntry',
6073
color: '#5e4fa2',
6174
})
@@ -64,30 +77,39 @@ class ChatSessionManager {
6477
return folder
6578
}
6679

80+
/** Get the Actors roleplay subfolder */
81+
getActorRoleplayFolder(): any | null {
82+
const folderId = getSubfolderId('actors')
83+
if (folderId) return game.folders?.get(folderId) || null
84+
return null
85+
}
86+
6787
/** Create a new chat session */
68-
async createSession(name?: string): Promise<ChatSession> {
69-
const folder = await this.getChatHistoryFolder()
88+
async createSession(name?: string, actorId?: string, actorName?: string): Promise<ChatSession> {
89+
// Actor roleplay sessions go in the Actors folder
90+
const folder = actorId ? (this.getActorRoleplayFolder() || await this.getChatHistoryFolder()) : await this.getChatHistoryFolder()
7091
const now = Date.now()
71-
const sessionName = name || `Chat ${new Date(now).toLocaleString()}`
92+
const sessionName = name || (actorName ? `${actorName}${new Date(now).toLocaleString()}` : `Chat ${new Date(now).toLocaleString()}`)
7293

7394
const journal = await JournalEntry.create({
7495
name: sessionName,
7596
folder: folder.id,
7697
pages: [
7798
{
78-
name: 'Chat Log',
99+
name: actorName ? `${actorName} Roleplay` : 'Chat Log',
79100
type: 'text',
80101
text: { content: '', format: 1 },
81102
},
82103
],
83104
flags: {
84105
[MODULE_ID]: {
85-
type: 'chat-session',
106+
type: actorId ? 'actor-roleplay' : 'chat-session',
86107
messages: [],
87108
createdAt: now,
88109
updatedAt: now,
89110
model: '',
90111
tokenCount: 0,
112+
...(actorId ? { actorId, actorName } : {}),
91113
},
92114
},
93115
})
@@ -99,6 +121,8 @@ class ChatSessionManager {
99121
createdAt: now,
100122
updatedAt: now,
101123
model: '',
124+
actorId,
125+
actorName,
102126
}
103127
}
104128

@@ -178,7 +202,7 @@ class ChatSessionManager {
178202
if (!entry) return null
179203

180204
const flags = entry.flags?.[MODULE_ID] as Record<string, any>
181-
if (!flags || flags.type !== 'chat-session') return null
205+
if (!flags || (flags.type !== 'chat-session' && flags.type !== 'actor-roleplay')) return null
182206

183207
return {
184208
id: entry.id,
@@ -188,6 +212,8 @@ class ChatSessionManager {
188212
updatedAt: flags.updatedAt || 0,
189213
model: flags.model || '',
190214
tokenCount: flags.tokenCount,
215+
actorId: flags.actorId,
216+
actorName: flags.actorName,
191217
}
192218
}
193219

@@ -199,7 +225,7 @@ class ChatSessionManager {
199225

200226
for (const entry of game.journal.values()) {
201227
const flags = entry.flags?.[MODULE_ID] as Record<string, any>
202-
if (!flags || flags.type !== 'chat-session') continue
228+
if (!flags || (flags.type !== 'chat-session' && flags.type !== 'actor-roleplay')) continue
203229

204230
sessions.push({
205231
id: entry.id,
@@ -208,6 +234,8 @@ class ChatSessionManager {
208234
updatedAt: flags.updatedAt || 0,
209235
messageCount: (flags.messages || []).length,
210236
model: flags.model || '',
237+
actorId: flags.actorId,
238+
actorName: flags.actorName,
211239
})
212240
}
213241

src/core/folder-manager.ts

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
/* ==========================================================================
2+
Folder Manager
3+
Ensures a consistent "FoundryAI" top-level journal folder structure:
4+
FoundryAI/
5+
├── Notes
6+
├── Chat History
7+
├── Sessions
8+
└── Actors
9+
Provides ID lookups so other managers can place journals in the right spot.
10+
========================================================================== */
11+
12+
const ROOT_FOLDER_NAME = 'FoundryAI'
13+
const ROOT_FOLDER_COLOR = '#5e4fa2'
14+
15+
/** Subfolder names under the FoundryAI root */
16+
export const SUBFOLDER_NAMES = {
17+
notes: 'Notes',
18+
chatHistory: 'Chat History',
19+
sessions: 'Sessions',
20+
actors: 'Actors',
21+
} as const
22+
23+
export type SubfolderKey = keyof typeof SUBFOLDER_NAMES
24+
25+
/** Cache of resolved folder IDs */
26+
const folderIdCache: Record<string, string | null> = {}
27+
28+
/**
29+
* Bootstrap the full FoundryAI folder tree.
30+
* Safe to call multiple times — only creates missing folders.
31+
* Returns the root folder.
32+
*/
33+
export async function ensureFoundryAIFolders(): Promise<any> {
34+
const root = await getOrCreateFolder(ROOT_FOLDER_NAME, null, ROOT_FOLDER_COLOR)
35+
36+
// Create subfolders under root
37+
for (const key of Object.keys(SUBFOLDER_NAMES) as SubfolderKey[]) {
38+
const name = SUBFOLDER_NAMES[key]
39+
await getOrCreateFolder(name, root.id)
40+
}
41+
42+
// Invalidate cache — the IDs may have just been created
43+
Object.keys(folderIdCache).forEach((k) => delete folderIdCache[k])
44+
45+
return root
46+
}
47+
48+
/**
49+
* Get the Folder document for a named subfolder under FoundryAI.
50+
* Returns the Folder or null if it doesn't exist yet.
51+
*/
52+
export function getSubfolder(key: SubfolderKey): any | null {
53+
const name = SUBFOLDER_NAMES[key]
54+
const root = game.folders?.find((f: any) => f.type === 'JournalEntry' && f.name === ROOT_FOLDER_NAME && !f.folder)
55+
if (!root) return null
56+
return game.folders?.find((f: any) => f.type === 'JournalEntry' && f.name === name && f.folder?.id === root.id) || null
57+
}
58+
59+
/**
60+
* Get the subfolder ID. Returns null if not bootstrapped yet.
61+
*/
62+
export function getSubfolderId(key: SubfolderKey): string | null {
63+
// Check cache first
64+
const cacheKey = `subfolder-${key}`
65+
if (folderIdCache[cacheKey]) {
66+
// Verify it still exists
67+
const folder = game.folders?.get(folderIdCache[cacheKey]!)
68+
if (folder) return folderIdCache[cacheKey]!
69+
delete folderIdCache[cacheKey]
70+
}
71+
72+
const folder = getSubfolder(key)
73+
if (folder) {
74+
folderIdCache[cacheKey] = folder.id
75+
return folder.id
76+
}
77+
return null
78+
}
79+
80+
/**
81+
* Get the root FoundryAI folder ID. Returns null if not bootstrapped yet.
82+
*/
83+
export function getRootFolderId(): string | null {
84+
const root = game.folders?.find((f: any) => f.type === 'JournalEntry' && f.name === ROOT_FOLDER_NAME && !f.folder)
85+
return root?.id || null
86+
}
87+
88+
// ---- Helpers ----
89+
90+
async function getOrCreateFolder(name: string, parentId: string | null, color?: string): Promise<any> {
91+
// Find existing
92+
const existing = game.folders?.find((f: any) => {
93+
if (f.type !== 'JournalEntry' || f.name !== name) return false
94+
if (parentId) return f.folder?.id === parentId
95+
return !f.folder // top-level
96+
})
97+
98+
if (existing) return existing
99+
100+
// Create
101+
const data: Record<string, any> = {
102+
name,
103+
type: 'JournalEntry',
104+
parent: parentId,
105+
}
106+
if (color) data.color = color
107+
108+
const folder = await Folder.create(data)
109+
console.log(`FoundryAI | Created folder: ${name}${parentId ? ' (subfolder)' : ''}`)
110+
return folder
111+
}

src/core/session-recap-manager.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@
77
import { openRouterService, type LLMMessage } from './openrouter-service'
88
import { chatSessionManager } from './chat-session-manager'
99
import { collectionReader } from './collection-reader'
10+
import { getSubfolderId } from './folder-manager'
1011

1112
const MODULE_ID = 'foundry-ai'
12-
const RECAP_FOLDER_NAME = 'Session Recaps'
13+
const RECAP_FOLDER_NAME = 'Sessions'
1314

1415
export interface SessionRecap {
1516
id: string // JournalEntry document ID
@@ -38,13 +39,20 @@ class SessionRecapManager {
3839
if (folder) return folder
3940
}
4041

42+
// Use the FoundryAI/Sessions subfolder
43+
const subfolderId = getSubfolderId('sessions')
44+
if (subfolderId) {
45+
const folder = game.folders?.get(subfolderId)
46+
if (folder) return folder
47+
}
48+
4149
// Check cache
4250
if (this.folderCache) {
4351
const cached = game.folders?.get(this.folderCache)
4452
if (cached) return cached
4553
}
4654

47-
// Find existing
55+
// Find existing (legacy fallback)
4856
const existing = game.folders?.find((f: Folder) => f.name === RECAP_FOLDER_NAME && f.type === 'JournalEntry')
4957

5058
if (existing) {

0 commit comments

Comments
 (0)