@onmars/lunar-core 0.5.0 → 0.6.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onmars/lunar-core",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -19,6 +19,9 @@
19
19
  * minMessages — Minimum session message count
20
20
  */
21
21
 
22
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, appendFileSync } from 'node:fs'
23
+ import { join } from 'node:path'
24
+ import { execSync } from 'node:child_process'
22
25
  import type { Fact, MemoryProvider, MemoryResult } from '../types/memory'
23
26
  import type { HookCondition, HookEntry, HooksConfig, LifecycleEvent } from './config-loader'
24
27
  import { log } from './logger'
@@ -1129,6 +1132,206 @@ const executeDiaryRecall: ActionExecutor = async (hook, context, providers) => {
1129
1132
  }
1130
1133
  }
1131
1134
 
1135
+ // ════════════════════════════════════════════════════════════
1136
+ // sessionLog — Write session summary to memory/YYYY-MM-DD.md
1137
+ // ════════════════════════════════════════════════════════════
1138
+
1139
+ /**
1140
+ * `sessionLog` — Write session details to a daily markdown file.
1141
+ *
1142
+ * Creates/appends to `~/.lunar/memory/YYYY-MM-DD.md` with session summary,
1143
+ * topics, channel, and message count. This serves as an offline backup
1144
+ * that's git-tracked and grep-searchable, independent of Brain availability.
1145
+ *
1146
+ * Fields: (none required — uses context)
1147
+ * Events: afterSessionEnd
1148
+ */
1149
+ const executeSessionLog: ActionExecutor = async (_hook, context) => {
1150
+ if (!context.sessionSummary && (!context.messages || context.messages.length === 0)) {
1151
+ log.debug({ action: 'sessionLog' }, 'Hook sessionLog: no summary or messages — skipping')
1152
+ return
1153
+ }
1154
+
1155
+ const sessionDate = context.sessionDate ?? new Date().toISOString().split('T')[0]
1156
+ const memoryDir = join(process.env.HOME ?? '/home/mars', '.lunar', 'memory')
1157
+ const filePath = join(memoryDir, `${sessionDate}.md`)
1158
+
1159
+ try {
1160
+ // Ensure memory directory exists
1161
+ if (!existsSync(memoryDir)) {
1162
+ mkdirSync(memoryDir, { recursive: true })
1163
+ }
1164
+
1165
+ const now = new Date()
1166
+ const timeStr = now.toLocaleTimeString('es-ES', { hour: '2-digit', minute: '2-digit' })
1167
+ const channel = context.channel ?? 'unknown'
1168
+
1169
+ // Build session section
1170
+ const lines: string[] = []
1171
+
1172
+ // Add header if file is new
1173
+ if (!existsSync(filePath)) {
1174
+ lines.push(`# Session Log — ${sessionDate}`)
1175
+ lines.push('')
1176
+ }
1177
+
1178
+ lines.push(`## ${timeStr} — #${channel} (${context.messageCount} msgs)`)
1179
+ lines.push('')
1180
+
1181
+ if (context.topics?.length) {
1182
+ lines.push(`**Topics:** ${context.topics.join(', ')}`)
1183
+ lines.push('')
1184
+ }
1185
+
1186
+ if (context.sessionSummary) {
1187
+ lines.push('### Summary')
1188
+ lines.push('')
1189
+ lines.push(context.sessionSummary)
1190
+ lines.push('')
1191
+ }
1192
+
1193
+ if (context.episodeId) {
1194
+ lines.push(`*Episode: ${context.episodeId.slice(0, 8)}*`)
1195
+ lines.push('')
1196
+ }
1197
+
1198
+ lines.push('---')
1199
+ lines.push('')
1200
+
1201
+ const content = lines.join('\n')
1202
+
1203
+ // Append to existing file or create new
1204
+ if (existsSync(filePath)) {
1205
+ appendFileSync(filePath, content, 'utf-8')
1206
+ } else {
1207
+ writeFileSync(filePath, content, 'utf-8')
1208
+ }
1209
+
1210
+ log.info(
1211
+ { file: filePath, channel, messageCount: context.messageCount },
1212
+ 'Hook sessionLog: written',
1213
+ )
1214
+ } catch (err) {
1215
+ const errMsg = err instanceof Error ? err.message : String(err)
1216
+ log.warn({ error: errMsg, file: filePath }, 'Hook sessionLog: failed — continuing')
1217
+ }
1218
+ }
1219
+
1220
+ // ════════════════════════════════════════════════════════════
1221
+ // pipelineExtract — Extract entities/facts via Brain pipeline
1222
+ // ════════════════════════════════════════════════════════════
1223
+
1224
+ /**
1225
+ * `pipelineExtract` — Call Brain's pipeline/extract endpoint to automatically
1226
+ * extract entities, facts, and relations from the conversation transcript.
1227
+ *
1228
+ * Fields: provider (brain provider with extractPipeline support)
1229
+ * Events: afterSessionEnd
1230
+ */
1231
+ const executePipelineExtract: ActionExecutor = async (hook, context, providers) => {
1232
+ const { provider: providerName } = hook
1233
+
1234
+ if (!providerName) {
1235
+ log.warn({ action: 'pipelineExtract' }, 'Hook pipelineExtract: requires "provider" — skipping')
1236
+ return
1237
+ }
1238
+
1239
+ if (!context.messages || context.messages.length < 4) {
1240
+ log.debug(
1241
+ { action: 'pipelineExtract', messageCount: context.messages?.length ?? 0 },
1242
+ 'Hook pipelineExtract: too few messages — skipping',
1243
+ )
1244
+ return
1245
+ }
1246
+
1247
+ const provider = providers.get(providerName)
1248
+ if (!provider) {
1249
+ log.warn(
1250
+ { provider: providerName, action: 'pipelineExtract' },
1251
+ 'Hook pipelineExtract: provider not found — skipping',
1252
+ )
1253
+ return
1254
+ }
1255
+
1256
+ // Access brain-specific extractPipeline method
1257
+ const brain = provider as unknown as {
1258
+ extractPipeline?: (conversation: string, channel?: string, sessionDate?: string) => Promise<unknown>
1259
+ }
1260
+ if (!brain.extractPipeline) {
1261
+ log.warn(
1262
+ { action: 'pipelineExtract' },
1263
+ 'Hook pipelineExtract: provider does not support extractPipeline — skipping',
1264
+ )
1265
+ return
1266
+ }
1267
+
1268
+ try {
1269
+ const transcript = smartTruncate(
1270
+ context.messages.map((m) => `${m.role}: ${m.content}`).join('\n'),
1271
+ MAX_TRANSCRIPT_CHARS,
1272
+ )
1273
+
1274
+ const sessionDate = context.sessionDate ?? new Date().toISOString().split('T')[0]
1275
+
1276
+ await brain.extractPipeline(transcript, context.channel ?? 'unknown', sessionDate)
1277
+
1278
+ log.info(
1279
+ { provider: providerName, chars: transcript.length },
1280
+ 'Hook pipelineExtract: complete',
1281
+ )
1282
+ } catch (err) {
1283
+ const errMsg = err instanceof Error ? err.message : String(err)
1284
+ log.warn({ error: errMsg }, 'Hook pipelineExtract: failed — continuing')
1285
+ }
1286
+ }
1287
+
1288
+ // ════════════════════════════════════════════════════════════
1289
+ // gitSync — Commit and push workspace changes
1290
+ // ════════════════════════════════════════════════════════════
1291
+
1292
+ /**
1293
+ * `gitSync` — Commit and push any workspace changes after session end.
1294
+ *
1295
+ * Runs: git add -A && git commit && git push in the workspace directory.
1296
+ * Best-effort: failures are logged but never block the flow.
1297
+ *
1298
+ * Fields: (none — uses ~/.lunar as workspace)
1299
+ * Events: afterSessionEnd (should be LAST hook)
1300
+ */
1301
+ const executeGitSync: ActionExecutor = async () => {
1302
+ const workspaceDir = join(process.env.HOME ?? '/home/mars', '.lunar')
1303
+
1304
+ try {
1305
+ // Check if there are changes to commit
1306
+ const status = execSync('git status --porcelain', {
1307
+ cwd: workspaceDir,
1308
+ encoding: 'utf-8',
1309
+ timeout: 10_000,
1310
+ }).trim()
1311
+
1312
+ if (!status) {
1313
+ log.debug({ action: 'gitSync' }, 'Hook gitSync: no changes — skipping')
1314
+ return
1315
+ }
1316
+
1317
+ const today = new Date().toISOString().split('T')[0]
1318
+ const commitMsg = `session log ${today} — auto-sync`
1319
+
1320
+ execSync('git add -A', { cwd: workspaceDir, timeout: 10_000 })
1321
+ execSync(`git commit -m "${commitMsg}"`, {
1322
+ cwd: workspaceDir,
1323
+ timeout: 15_000,
1324
+ })
1325
+ execSync('git push', { cwd: workspaceDir, timeout: 30_000 })
1326
+
1327
+ log.info({ workspace: workspaceDir }, 'Hook gitSync: committed and pushed')
1328
+ } catch (err) {
1329
+ const errMsg = err instanceof Error ? err.message : String(err)
1330
+ // Git sync failure is non-critical — don't block
1331
+ log.warn({ error: errMsg }, 'Hook gitSync: failed — continuing')
1332
+ }
1333
+ }
1334
+
1132
1335
  const ACTION_REGISTRY = new Map<string, ActionExecutor>([
1133
1336
  ['promote', executePromote],
1134
1337
  ['boost', executeBoost],
@@ -1140,6 +1343,9 @@ const ACTION_REGISTRY = new Map<string, ActionExecutor>([
1140
1343
  ['reconcile', executeReconcile],
1141
1344
  ['episodeClose', executeEpisodeClose],
1142
1345
  ['diaryRecall', executeDiaryRecall],
1346
+ ['sessionLog', executeSessionLog],
1347
+ ['pipelineExtract', executePipelineExtract],
1348
+ ['gitSync', executeGitSync],
1143
1349
  ])
1144
1350
 
1145
1351
  /**