wendkeep 0.38.0 → 0.38.2
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/CHANGELOG.md +19 -0
- package/hooks/brain-inject.mjs +74 -74
- package/hooks/obsidian-common.mjs +586 -586
- package/hooks/pricing.json +53 -53
- package/hooks/session-ensure.mjs +377 -377
- package/hooks/session-identity.mjs +22 -0
- package/hooks/session-observability.mjs +1 -1
- package/hooks/session-start.mjs +299 -299
- package/hooks/session-stop.mjs +1132 -1132
- package/hooks/subagent-stop.mjs +2 -2
- package/hooks/task-log.mjs +2 -2
- package/hooks/token-usage.mjs +949 -944
- package/hooks/vault-health.mjs +186 -186
- package/package.json +7 -2
- package/src/release-changelog.mjs +38 -0
package/hooks/session-ensure.mjs
CHANGED
|
@@ -1,313 +1,313 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { existsSync, readFileSync, renameSync, statSync, writeFileSync } from 'fs';
|
|
3
|
-
import { basename, dirname, join } from 'path';
|
|
4
|
-
import {
|
|
5
|
-
controlPath,
|
|
6
|
-
ensureDir,
|
|
7
|
-
formatDate,
|
|
8
|
-
formatHourMinute,
|
|
9
|
-
formatLocalIso,
|
|
10
|
-
formatTime,
|
|
11
|
-
getVaultBase,
|
|
12
|
-
warnIfDefaultVault,
|
|
13
|
-
debugLog,
|
|
14
|
-
readControl,
|
|
15
|
-
readHookInput,
|
|
16
|
-
readSessionRegistry,
|
|
17
|
-
sessionFileName,
|
|
18
|
-
sessionFolderRel,
|
|
19
|
-
sessionSummaryFromInput,
|
|
20
|
-
isUsableSummary,
|
|
21
|
-
providerMeta,
|
|
22
|
-
shouldReuseActiveSession,
|
|
23
|
-
isPlaceholderSessionFile,
|
|
24
|
-
toVaultRelative,
|
|
25
|
-
uniquePath,
|
|
26
|
-
upsertSessionRegistry,
|
|
27
|
-
VAULT_COMPLEMENT_RULES,
|
|
28
|
-
wikilinkFromRel,
|
|
29
|
-
writeControl,
|
|
30
|
-
writeHookOutput,
|
|
31
|
-
yamlQuote,
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { existsSync, readFileSync, renameSync, statSync, writeFileSync } from 'fs';
|
|
3
|
+
import { basename, dirname, join } from 'path';
|
|
4
|
+
import {
|
|
5
|
+
controlPath,
|
|
6
|
+
ensureDir,
|
|
7
|
+
formatDate,
|
|
8
|
+
formatHourMinute,
|
|
9
|
+
formatLocalIso,
|
|
10
|
+
formatTime,
|
|
11
|
+
getVaultBase,
|
|
12
|
+
warnIfDefaultVault,
|
|
13
|
+
debugLog,
|
|
14
|
+
readControl,
|
|
15
|
+
readHookInput,
|
|
16
|
+
readSessionRegistry,
|
|
17
|
+
sessionFileName,
|
|
18
|
+
sessionFolderRel,
|
|
19
|
+
sessionSummaryFromInput,
|
|
20
|
+
isUsableSummary,
|
|
21
|
+
providerMeta,
|
|
22
|
+
shouldReuseActiveSession,
|
|
23
|
+
isPlaceholderSessionFile,
|
|
24
|
+
toVaultRelative,
|
|
25
|
+
uniquePath,
|
|
26
|
+
upsertSessionRegistry,
|
|
27
|
+
VAULT_COMPLEMENT_RULES,
|
|
28
|
+
wikilinkFromRel,
|
|
29
|
+
writeControl,
|
|
30
|
+
writeHookOutput,
|
|
31
|
+
yamlQuote,
|
|
32
32
|
} from './obsidian-common.mjs';
|
|
33
33
|
import { resolveSessionIdentity } from './session-identity.mjs';
|
|
34
|
-
|
|
35
|
-
function sessionIdFromInput(input) {
|
|
36
|
-
return input.session_id || input.sessionId || input.codex_session_id || '';
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function buildSessionContent({ relPath, now, summary = 'session', sessionId = '', reason = 'Sessão criada automaticamente pelo hook UserPromptSubmit.' }) {
|
|
40
|
-
const date = formatDate(now);
|
|
41
|
-
const startedAt = formatLocalIso(now);
|
|
42
|
-
const titleTime = formatTime(now).slice(0, 5);
|
|
43
|
-
const objective = summary === 'session' ? 'Preencher durante a sessão.' : summary;
|
|
44
|
-
const provider = providerMeta();
|
|
45
|
-
|
|
46
|
-
return `---
|
|
47
|
-
type: session
|
|
48
|
-
date: ${date}
|
|
49
|
-
started_at: ${startedAt}
|
|
50
|
-
ended_at:
|
|
51
|
-
provider: ${provider.id}
|
|
52
|
-
session_id: ${sessionId ? yamlQuote(sessionId) : ''}
|
|
53
|
-
status: active
|
|
54
|
-
summary: ${yamlQuote(summary)}
|
|
55
|
-
cssclasses:
|
|
56
|
-
- topic-session
|
|
57
|
-
tags:
|
|
58
|
-
- sessao
|
|
59
|
-
- ${provider.tag}
|
|
60
|
-
- llm
|
|
61
|
-
source: ${provider.source}
|
|
62
|
-
related:
|
|
63
|
-
---
|
|
64
|
-
|
|
65
|
-
# ${titleTime} - ${summary}
|
|
66
|
-
|
|
67
|
-
## Metadados
|
|
68
|
-
|
|
69
|
-
- **Provider:** ${provider.label}
|
|
70
|
-
- **Início:** ${startedAt}
|
|
71
|
-
- **Fim:**
|
|
72
|
-
- **Status:** active
|
|
73
|
-
- **Arquivo:** \`${relPath}\`
|
|
74
|
-
|
|
75
|
-
## Objetivo da sessão
|
|
76
|
-
|
|
77
|
-
> ${objective}
|
|
78
|
-
|
|
79
|
-
## Resumo vivo
|
|
80
|
-
|
|
81
|
-
> Esta seção pode ser atualizada ao longo da sessão, mas o histórico de iterações deve ser preservado.
|
|
82
|
-
|
|
83
|
-
## Iterações
|
|
84
|
-
|
|
85
|
-
### ${titleTime} - Início da sessão
|
|
86
|
-
|
|
87
|
-
${reason}
|
|
88
|
-
|
|
89
|
-
## Decisões geradas nesta sessão
|
|
90
|
-
|
|
91
|
-
Nenhuma decisão registrada ainda.
|
|
92
|
-
|
|
93
|
-
## Bugs gerados nesta sessão
|
|
94
|
-
|
|
95
|
-
Nenhum bug registrado ainda.
|
|
96
|
-
|
|
97
|
-
## Aprendizados gerados nesta sessão
|
|
98
|
-
|
|
99
|
-
Nenhum aprendizado registrado ainda.
|
|
100
|
-
|
|
101
|
-
## Arquivos consultados
|
|
102
|
-
|
|
103
|
-
Nenhum arquivo registrado ainda.
|
|
104
|
-
|
|
105
|
-
## Arquivos criados ou alterados
|
|
106
|
-
|
|
107
|
-
Nenhum arquivo registrado ainda.
|
|
108
|
-
|
|
109
|
-
## Pendências
|
|
110
|
-
|
|
111
|
-
Nenhuma pendência identificada automaticamente.
|
|
112
|
-
|
|
113
|
-
## Encerramento
|
|
114
|
-
|
|
115
|
-
Sessão ainda em andamento.
|
|
116
|
-
`;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function allocateSessionPath(vaultBase, now, summary = 'session') {
|
|
120
|
-
const folderRel = sessionFolderRel(now, vaultBase);
|
|
121
|
-
const folderAbs = join(vaultBase, folderRel);
|
|
122
|
-
ensureDir(folderAbs);
|
|
123
|
-
|
|
124
|
-
const baseName = sessionFileName(now, summary);
|
|
125
|
-
const filePath = uniquePath(join(folderAbs, baseName));
|
|
126
|
-
return {
|
|
127
|
-
absPath: filePath,
|
|
128
|
-
relPath: toVaultRelative(vaultBase, filePath),
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function buildAdditionalContext({ relPath, startedAt, vaultBase }) {
|
|
133
|
-
const controlRel = toVaultRelative(vaultBase, controlPath(vaultBase));
|
|
134
|
-
return [
|
|
135
|
-
'<obsidian_session>',
|
|
136
|
-
`Sessão Obsidian ativa: ${relPath}`,
|
|
137
|
-
`Controle atualizado: ${controlRel}`,
|
|
138
|
-
`Início: ${startedAt}`,
|
|
139
|
-
'',
|
|
140
|
-
'Use a sessão ativa como log desta conversa.',
|
|
141
|
-
'Antes de registrar informações, leia `.brain/CURRENT_SESSION.md` no vault.',
|
|
142
|
-
'Nunca sobrescreva o histórico anterior. Registre cada iteração como `### HH:MM - Título` DENTRO da seção `## Iterações` (logo antes de `## Decisões geradas nesta sessão`). Cada iteração deve trazer contexto conversado suficiente: pedido do usuário, investigação/ações, evidências relevantes e estado final. NUNCA escreva iterações após `## Encerramento`.',
|
|
143
|
-
'',
|
|
144
|
-
...VAULT_COMPLEMENT_RULES,
|
|
145
|
-
'Não registre chaves, tokens, senhas ou segredos; substitua por `[REDACTED_SECRET]`.',
|
|
146
|
-
`Wikilink da sessão: ${wikilinkFromRel(relPath)}`,
|
|
147
|
-
'</obsidian_session>',
|
|
148
|
-
].join('\n');
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
function updateSessionFrontmatter(content) {
|
|
152
|
-
let next = content;
|
|
153
|
-
next = next.replace(/^status:.*$/m, 'status: active');
|
|
154
|
-
next = next.replace(/^ended_at:.*$/m, 'ended_at:');
|
|
155
|
-
return next;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
function upsertSummaryFrontmatter(content, summary) {
|
|
159
|
-
if (/^summary:/m.test(content)) return content.replace(/^summary:.*$/m, `summary: ${yamlQuote(summary)}`);
|
|
160
|
-
return content.replace(/^status:.*$/m, (line) => `${line}\nsummary: ${yamlQuote(summary)}`);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function updateSessionDescription(content, { relPath, summary, startedAt }) {
|
|
164
|
-
const startedDate = startedAt ? new Date(startedAt) : new Date();
|
|
165
|
-
const titleTime = Number.isFinite(startedDate.getTime()) ? formatTime(startedDate).slice(0, 5) : '';
|
|
166
|
-
let next = upsertSummaryFrontmatter(content, summary);
|
|
167
|
-
if (titleTime) {
|
|
168
|
-
next = next.replace(/^# .+$/m, `# ${titleTime} - ${summary}`);
|
|
169
|
-
}
|
|
170
|
-
next = next.replace(/- \*\*Arquivo:\*\* `[^`]+`/m, `- **Arquivo:** \`${relPath}\``);
|
|
171
|
-
next = next.replace(
|
|
172
|
-
/(## Objetivo da sessão\n\n)>[^\n]*/m,
|
|
173
|
-
`$1> ${summary === 'session' ? 'Preencher durante a sessão.' : summary}`,
|
|
174
|
-
);
|
|
175
|
-
return next;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
function maybeRetitleSession({ vaultBase, relPath, startedAt, input }) {
|
|
179
|
-
const summary = sessionSummaryFromInput(input);
|
|
180
|
-
if (!isUsableSummary(summary)) return { relPath, summary, changed: false };
|
|
181
|
-
|
|
182
|
-
const currentPath = join(vaultBase, relPath);
|
|
183
|
-
if (!existsSync(currentPath)) return { relPath, summary, changed: false };
|
|
184
|
-
|
|
185
|
-
let nextRelPath = relPath;
|
|
186
|
-
if (isPlaceholderSessionFile(relPath)) {
|
|
187
|
-
const startedDate = startedAt ? new Date(startedAt) : new Date();
|
|
188
|
-
const baseDate = Number.isFinite(startedDate.getTime()) ? startedDate : new Date();
|
|
189
|
-
const nextPath = uniquePath(join(dirname(currentPath), sessionFileName(baseDate, summary)));
|
|
190
|
-
if (nextPath !== currentPath) {
|
|
191
|
-
renameSync(currentPath, nextPath);
|
|
192
|
-
nextRelPath = toVaultRelative(vaultBase, nextPath);
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
const sessionPath = join(vaultBase, nextRelPath);
|
|
197
|
-
const content = readFileSync(sessionPath, 'utf-8');
|
|
198
|
-
const updated = updateSessionDescription(content, { relPath: nextRelPath, summary, startedAt });
|
|
199
|
-
if (updated !== content) writeFileSync(sessionPath, updated, 'utf-8');
|
|
200
|
-
|
|
201
|
-
return { relPath: nextRelPath, summary, changed: nextRelPath !== relPath || updated !== content };
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
function stripClosingSection(content) {
|
|
205
|
-
const marker = '\n## Encerramento';
|
|
206
|
-
const index = content.indexOf(marker);
|
|
207
|
-
if (index === -1) return content;
|
|
208
|
-
return `${content.slice(0, index).trimEnd()}\n`;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
function reopenSessionFile(sessionPath) {
|
|
212
|
-
const content = readFileSync(sessionPath, 'utf-8');
|
|
213
|
-
const reopened = stripClosingSection(updateSessionFrontmatter(content));
|
|
214
|
-
writeFileSync(sessionPath, reopened, 'utf-8');
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
function findSessionForInput(vaultBase, input, control) {
|
|
218
|
-
const sessionId = sessionIdFromInput(input);
|
|
219
|
-
const registry = readSessionRegistry(vaultBase);
|
|
220
|
-
const registered = sessionId ? registry.sessions[sessionId] : null;
|
|
221
|
-
|
|
222
|
-
if (registered?.session_file) {
|
|
223
|
-
return {
|
|
224
|
-
sessionId,
|
|
225
|
-
relPath: registered.session_file,
|
|
226
|
-
startedAt: registered.started_at || control.started_at || '',
|
|
227
|
-
fromRegistry: true,
|
|
228
|
-
};
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
if (sessionId && control.session_id === sessionId && control.last_session_file) {
|
|
232
|
-
return {
|
|
233
|
-
sessionId,
|
|
234
|
-
relPath: control.last_session_file,
|
|
235
|
-
startedAt: control.started_at || '',
|
|
236
|
-
fromRegistry: false,
|
|
237
|
-
};
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
return { sessionId, relPath: '', startedAt: '', fromRegistry: false };
|
|
241
|
-
}
|
|
242
|
-
|
|
34
|
+
|
|
35
|
+
function sessionIdFromInput(input) {
|
|
36
|
+
return input.session_id || input.sessionId || input.codex_session_id || '';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function buildSessionContent({ relPath, now, summary = 'session', sessionId = '', reason = 'Sessão criada automaticamente pelo hook UserPromptSubmit.' }) {
|
|
40
|
+
const date = formatDate(now);
|
|
41
|
+
const startedAt = formatLocalIso(now);
|
|
42
|
+
const titleTime = formatTime(now).slice(0, 5);
|
|
43
|
+
const objective = summary === 'session' ? 'Preencher durante a sessão.' : summary;
|
|
44
|
+
const provider = providerMeta();
|
|
45
|
+
|
|
46
|
+
return `---
|
|
47
|
+
type: session
|
|
48
|
+
date: ${date}
|
|
49
|
+
started_at: ${startedAt}
|
|
50
|
+
ended_at:
|
|
51
|
+
provider: ${provider.id}
|
|
52
|
+
session_id: ${sessionId ? yamlQuote(sessionId) : ''}
|
|
53
|
+
status: active
|
|
54
|
+
summary: ${yamlQuote(summary)}
|
|
55
|
+
cssclasses:
|
|
56
|
+
- topic-session
|
|
57
|
+
tags:
|
|
58
|
+
- sessao
|
|
59
|
+
- ${provider.tag}
|
|
60
|
+
- llm
|
|
61
|
+
source: ${provider.source}
|
|
62
|
+
related:
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
# ${titleTime} - ${summary}
|
|
66
|
+
|
|
67
|
+
## Metadados
|
|
68
|
+
|
|
69
|
+
- **Provider:** ${provider.label}
|
|
70
|
+
- **Início:** ${startedAt}
|
|
71
|
+
- **Fim:**
|
|
72
|
+
- **Status:** active
|
|
73
|
+
- **Arquivo:** \`${relPath}\`
|
|
74
|
+
|
|
75
|
+
## Objetivo da sessão
|
|
76
|
+
|
|
77
|
+
> ${objective}
|
|
78
|
+
|
|
79
|
+
## Resumo vivo
|
|
80
|
+
|
|
81
|
+
> Esta seção pode ser atualizada ao longo da sessão, mas o histórico de iterações deve ser preservado.
|
|
82
|
+
|
|
83
|
+
## Iterações
|
|
84
|
+
|
|
85
|
+
### ${titleTime} - Início da sessão
|
|
86
|
+
|
|
87
|
+
${reason}
|
|
88
|
+
|
|
89
|
+
## Decisões geradas nesta sessão
|
|
90
|
+
|
|
91
|
+
Nenhuma decisão registrada ainda.
|
|
92
|
+
|
|
93
|
+
## Bugs gerados nesta sessão
|
|
94
|
+
|
|
95
|
+
Nenhum bug registrado ainda.
|
|
96
|
+
|
|
97
|
+
## Aprendizados gerados nesta sessão
|
|
98
|
+
|
|
99
|
+
Nenhum aprendizado registrado ainda.
|
|
100
|
+
|
|
101
|
+
## Arquivos consultados
|
|
102
|
+
|
|
103
|
+
Nenhum arquivo registrado ainda.
|
|
104
|
+
|
|
105
|
+
## Arquivos criados ou alterados
|
|
106
|
+
|
|
107
|
+
Nenhum arquivo registrado ainda.
|
|
108
|
+
|
|
109
|
+
## Pendências
|
|
110
|
+
|
|
111
|
+
Nenhuma pendência identificada automaticamente.
|
|
112
|
+
|
|
113
|
+
## Encerramento
|
|
114
|
+
|
|
115
|
+
Sessão ainda em andamento.
|
|
116
|
+
`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function allocateSessionPath(vaultBase, now, summary = 'session') {
|
|
120
|
+
const folderRel = sessionFolderRel(now, vaultBase);
|
|
121
|
+
const folderAbs = join(vaultBase, folderRel);
|
|
122
|
+
ensureDir(folderAbs);
|
|
123
|
+
|
|
124
|
+
const baseName = sessionFileName(now, summary);
|
|
125
|
+
const filePath = uniquePath(join(folderAbs, baseName));
|
|
126
|
+
return {
|
|
127
|
+
absPath: filePath,
|
|
128
|
+
relPath: toVaultRelative(vaultBase, filePath),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function buildAdditionalContext({ relPath, startedAt, vaultBase }) {
|
|
133
|
+
const controlRel = toVaultRelative(vaultBase, controlPath(vaultBase));
|
|
134
|
+
return [
|
|
135
|
+
'<obsidian_session>',
|
|
136
|
+
`Sessão Obsidian ativa: ${relPath}`,
|
|
137
|
+
`Controle atualizado: ${controlRel}`,
|
|
138
|
+
`Início: ${startedAt}`,
|
|
139
|
+
'',
|
|
140
|
+
'Use a sessão ativa como log desta conversa.',
|
|
141
|
+
'Antes de registrar informações, leia `.brain/CURRENT_SESSION.md` no vault.',
|
|
142
|
+
'Nunca sobrescreva o histórico anterior. Registre cada iteração como `### HH:MM - Título` DENTRO da seção `## Iterações` (logo antes de `## Decisões geradas nesta sessão`). Cada iteração deve trazer contexto conversado suficiente: pedido do usuário, investigação/ações, evidências relevantes e estado final. NUNCA escreva iterações após `## Encerramento`.',
|
|
143
|
+
'',
|
|
144
|
+
...VAULT_COMPLEMENT_RULES,
|
|
145
|
+
'Não registre chaves, tokens, senhas ou segredos; substitua por `[REDACTED_SECRET]`.',
|
|
146
|
+
`Wikilink da sessão: ${wikilinkFromRel(relPath)}`,
|
|
147
|
+
'</obsidian_session>',
|
|
148
|
+
].join('\n');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function updateSessionFrontmatter(content) {
|
|
152
|
+
let next = content;
|
|
153
|
+
next = next.replace(/^status:.*$/m, 'status: active');
|
|
154
|
+
next = next.replace(/^ended_at:.*$/m, 'ended_at:');
|
|
155
|
+
return next;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function upsertSummaryFrontmatter(content, summary) {
|
|
159
|
+
if (/^summary:/m.test(content)) return content.replace(/^summary:.*$/m, `summary: ${yamlQuote(summary)}`);
|
|
160
|
+
return content.replace(/^status:.*$/m, (line) => `${line}\nsummary: ${yamlQuote(summary)}`);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function updateSessionDescription(content, { relPath, summary, startedAt }) {
|
|
164
|
+
const startedDate = startedAt ? new Date(startedAt) : new Date();
|
|
165
|
+
const titleTime = Number.isFinite(startedDate.getTime()) ? formatTime(startedDate).slice(0, 5) : '';
|
|
166
|
+
let next = upsertSummaryFrontmatter(content, summary);
|
|
167
|
+
if (titleTime) {
|
|
168
|
+
next = next.replace(/^# .+$/m, `# ${titleTime} - ${summary}`);
|
|
169
|
+
}
|
|
170
|
+
next = next.replace(/- \*\*Arquivo:\*\* `[^`]+`/m, `- **Arquivo:** \`${relPath}\``);
|
|
171
|
+
next = next.replace(
|
|
172
|
+
/(## Objetivo da sessão\n\n)>[^\n]*/m,
|
|
173
|
+
`$1> ${summary === 'session' ? 'Preencher durante a sessão.' : summary}`,
|
|
174
|
+
);
|
|
175
|
+
return next;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function maybeRetitleSession({ vaultBase, relPath, startedAt, input }) {
|
|
179
|
+
const summary = sessionSummaryFromInput(input);
|
|
180
|
+
if (!isUsableSummary(summary)) return { relPath, summary, changed: false };
|
|
181
|
+
|
|
182
|
+
const currentPath = join(vaultBase, relPath);
|
|
183
|
+
if (!existsSync(currentPath)) return { relPath, summary, changed: false };
|
|
184
|
+
|
|
185
|
+
let nextRelPath = relPath;
|
|
186
|
+
if (isPlaceholderSessionFile(relPath)) {
|
|
187
|
+
const startedDate = startedAt ? new Date(startedAt) : new Date();
|
|
188
|
+
const baseDate = Number.isFinite(startedDate.getTime()) ? startedDate : new Date();
|
|
189
|
+
const nextPath = uniquePath(join(dirname(currentPath), sessionFileName(baseDate, summary)));
|
|
190
|
+
if (nextPath !== currentPath) {
|
|
191
|
+
renameSync(currentPath, nextPath);
|
|
192
|
+
nextRelPath = toVaultRelative(vaultBase, nextPath);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const sessionPath = join(vaultBase, nextRelPath);
|
|
197
|
+
const content = readFileSync(sessionPath, 'utf-8');
|
|
198
|
+
const updated = updateSessionDescription(content, { relPath: nextRelPath, summary, startedAt });
|
|
199
|
+
if (updated !== content) writeFileSync(sessionPath, updated, 'utf-8');
|
|
200
|
+
|
|
201
|
+
return { relPath: nextRelPath, summary, changed: nextRelPath !== relPath || updated !== content };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function stripClosingSection(content) {
|
|
205
|
+
const marker = '\n## Encerramento';
|
|
206
|
+
const index = content.indexOf(marker);
|
|
207
|
+
if (index === -1) return content;
|
|
208
|
+
return `${content.slice(0, index).trimEnd()}\n`;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function reopenSessionFile(sessionPath) {
|
|
212
|
+
const content = readFileSync(sessionPath, 'utf-8');
|
|
213
|
+
const reopened = stripClosingSection(updateSessionFrontmatter(content));
|
|
214
|
+
writeFileSync(sessionPath, reopened, 'utf-8');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function findSessionForInput(vaultBase, input, control) {
|
|
218
|
+
const sessionId = sessionIdFromInput(input);
|
|
219
|
+
const registry = readSessionRegistry(vaultBase);
|
|
220
|
+
const registered = sessionId ? registry.sessions[sessionId] : null;
|
|
221
|
+
|
|
222
|
+
if (registered?.session_file) {
|
|
223
|
+
return {
|
|
224
|
+
sessionId,
|
|
225
|
+
relPath: registered.session_file,
|
|
226
|
+
startedAt: registered.started_at || control.started_at || '',
|
|
227
|
+
fromRegistry: true,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (sessionId && control.session_id === sessionId && control.last_session_file) {
|
|
232
|
+
return {
|
|
233
|
+
sessionId,
|
|
234
|
+
relPath: control.last_session_file,
|
|
235
|
+
startedAt: control.started_at || '',
|
|
236
|
+
fromRegistry: false,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return { sessionId, relPath: '', startedAt: '', fromRegistry: false };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
243
|
function activateExistingSession({ vaultBase, relPath, startedAt, sessionId, input, now, identity }) {
|
|
244
|
-
const sessionPath = join(vaultBase, relPath);
|
|
245
|
-
if (!existsSync(sessionPath)) return false;
|
|
246
|
-
|
|
247
|
-
reopenSessionFile(sessionPath);
|
|
248
|
-
const nextStartedAt = startedAt || formatLocalIso(now);
|
|
249
|
-
writeControl(vaultBase, {
|
|
250
|
-
status: 'active',
|
|
251
|
-
session_file: relPath,
|
|
252
|
-
last_session_file: relPath,
|
|
253
|
-
started_at: nextStartedAt,
|
|
254
|
-
ended_at: '',
|
|
255
|
-
session_id: sessionId,
|
|
256
|
-
last_logged_turn_id: '',
|
|
257
|
-
});
|
|
258
|
-
upsertSessionRegistry(vaultBase, sessionId, {
|
|
259
|
-
session_file: relPath,
|
|
260
|
-
status: 'active',
|
|
261
|
-
started_at: nextStartedAt,
|
|
262
|
-
ended_at: '',
|
|
244
|
+
const sessionPath = join(vaultBase, relPath);
|
|
245
|
+
if (!existsSync(sessionPath)) return false;
|
|
246
|
+
|
|
247
|
+
reopenSessionFile(sessionPath);
|
|
248
|
+
const nextStartedAt = startedAt || formatLocalIso(now);
|
|
249
|
+
writeControl(vaultBase, {
|
|
250
|
+
status: 'active',
|
|
251
|
+
session_file: relPath,
|
|
252
|
+
last_session_file: relPath,
|
|
253
|
+
started_at: nextStartedAt,
|
|
254
|
+
ended_at: '',
|
|
255
|
+
session_id: sessionId,
|
|
256
|
+
last_logged_turn_id: '',
|
|
257
|
+
});
|
|
258
|
+
upsertSessionRegistry(vaultBase, sessionId, {
|
|
259
|
+
session_file: relPath,
|
|
260
|
+
status: 'active',
|
|
261
|
+
started_at: nextStartedAt,
|
|
262
|
+
ended_at: '',
|
|
263
263
|
transcript_path: identity.transcriptPath,
|
|
264
264
|
transcript_id: identity.transcriptId,
|
|
265
265
|
provider: identity.provider,
|
|
266
|
-
});
|
|
267
|
-
return true;
|
|
268
|
-
}
|
|
269
|
-
|
|
266
|
+
});
|
|
267
|
+
return true;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
270
|
function createSession({ vaultBase, sessionId, input, now, identity }) {
|
|
271
|
-
const summary = sessionSummaryFromInput(input);
|
|
272
|
-
const { absPath, relPath } = allocateSessionPath(vaultBase, now, summary);
|
|
273
|
-
const startedAt = formatLocalIso(now);
|
|
274
|
-
writeFileSync(absPath, buildSessionContent({ relPath, now, summary, sessionId }), 'utf-8');
|
|
275
|
-
writeControl(vaultBase, {
|
|
276
|
-
status: 'active',
|
|
277
|
-
session_file: relPath,
|
|
278
|
-
last_session_file: relPath,
|
|
279
|
-
started_at: startedAt,
|
|
280
|
-
ended_at: '',
|
|
281
|
-
session_id: sessionId,
|
|
282
|
-
last_logged_turn_id: '',
|
|
283
|
-
});
|
|
284
|
-
upsertSessionRegistry(vaultBase, sessionId, {
|
|
285
|
-
session_file: relPath,
|
|
286
|
-
status: 'active',
|
|
287
|
-
started_at: startedAt,
|
|
288
|
-
ended_at: '',
|
|
271
|
+
const summary = sessionSummaryFromInput(input);
|
|
272
|
+
const { absPath, relPath } = allocateSessionPath(vaultBase, now, summary);
|
|
273
|
+
const startedAt = formatLocalIso(now);
|
|
274
|
+
writeFileSync(absPath, buildSessionContent({ relPath, now, summary, sessionId }), 'utf-8');
|
|
275
|
+
writeControl(vaultBase, {
|
|
276
|
+
status: 'active',
|
|
277
|
+
session_file: relPath,
|
|
278
|
+
last_session_file: relPath,
|
|
279
|
+
started_at: startedAt,
|
|
280
|
+
ended_at: '',
|
|
281
|
+
session_id: sessionId,
|
|
282
|
+
last_logged_turn_id: '',
|
|
283
|
+
});
|
|
284
|
+
upsertSessionRegistry(vaultBase, sessionId, {
|
|
285
|
+
session_file: relPath,
|
|
286
|
+
status: 'active',
|
|
287
|
+
started_at: startedAt,
|
|
288
|
+
ended_at: '',
|
|
289
289
|
transcript_path: identity.transcriptPath,
|
|
290
290
|
transcript_id: identity.transcriptId,
|
|
291
291
|
provider: identity.provider,
|
|
292
|
-
});
|
|
293
|
-
return { relPath, startedAt };
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
function outputActiveContext({ relPath, startedAt, vaultBase, message }) {
|
|
297
|
-
writeHookOutput({
|
|
298
|
-
hookSpecificOutput: {
|
|
299
|
-
hookEventName: 'UserPromptSubmit',
|
|
300
|
-
additionalContext: buildAdditionalContext({ relPath, startedAt, vaultBase }),
|
|
301
|
-
},
|
|
302
|
-
systemMessage: message,
|
|
303
|
-
});
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
function main() {
|
|
307
|
-
const input = readHookInput();
|
|
308
|
-
const vaultBase = getVaultBase(input);
|
|
309
|
-
warnIfDefaultVault(input);
|
|
310
|
-
const now = new Date();
|
|
292
|
+
});
|
|
293
|
+
return { relPath, startedAt };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function outputActiveContext({ relPath, startedAt, vaultBase, message }) {
|
|
297
|
+
writeHookOutput({
|
|
298
|
+
hookSpecificOutput: {
|
|
299
|
+
hookEventName: 'UserPromptSubmit',
|
|
300
|
+
additionalContext: buildAdditionalContext({ relPath, startedAt, vaultBase }),
|
|
301
|
+
},
|
|
302
|
+
systemMessage: message,
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function main() {
|
|
307
|
+
const input = readHookInput();
|
|
308
|
+
const vaultBase = getVaultBase(input);
|
|
309
|
+
warnIfDefaultVault(input);
|
|
310
|
+
const now = new Date();
|
|
311
311
|
const identity = resolveSessionIdentity(vaultBase, input, providerMeta().id);
|
|
312
312
|
if (identity.state !== 'resolved') {
|
|
313
313
|
writeHookOutput({
|
|
@@ -320,94 +320,94 @@ function main() {
|
|
|
320
320
|
return;
|
|
321
321
|
}
|
|
322
322
|
const sessionId = identity.canonicalConversationId;
|
|
323
|
-
|
|
324
|
-
// Fast path: skip all writes if control file touched < 5 min ago and session matches
|
|
325
|
-
try {
|
|
326
|
-
const ctrlPath = controlPath(vaultBase);
|
|
327
|
-
const { mtimeMs } = statSync(ctrlPath);
|
|
328
|
-
if ((now.getTime() - mtimeMs) / 1000 < 300) {
|
|
329
|
-
const ctrl = readControl(vaultBase);
|
|
330
|
-
if (
|
|
331
|
-
ctrl.status === 'active' &&
|
|
332
|
-
ctrl.session_file &&
|
|
333
|
-
!isPlaceholderSessionFile(ctrl.session_file) &&
|
|
334
|
-
existsSync(join(vaultBase, ctrl.session_file)) &&
|
|
323
|
+
|
|
324
|
+
// Fast path: skip all writes if control file touched < 5 min ago and session matches
|
|
325
|
+
try {
|
|
326
|
+
const ctrlPath = controlPath(vaultBase);
|
|
327
|
+
const { mtimeMs } = statSync(ctrlPath);
|
|
328
|
+
if ((now.getTime() - mtimeMs) / 1000 < 300) {
|
|
329
|
+
const ctrl = readControl(vaultBase);
|
|
330
|
+
if (
|
|
331
|
+
ctrl.status === 'active' &&
|
|
332
|
+
ctrl.session_file &&
|
|
333
|
+
!isPlaceholderSessionFile(ctrl.session_file) &&
|
|
334
|
+
existsSync(join(vaultBase, ctrl.session_file)) &&
|
|
335
335
|
ctrl.session_id === sessionId
|
|
336
|
-
) {
|
|
337
|
-
writeHookOutput({});
|
|
338
|
-
return;
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
} catch (err) {
|
|
342
|
-
debugLog('session-ensure fast-path skipped:', err);
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
const control = readControl(vaultBase);
|
|
346
|
-
|
|
347
|
-
if (control.status === 'active' && control.session_file) {
|
|
348
|
-
const activePath = join(vaultBase, control.session_file);
|
|
349
|
-
// Sem reuso-por-janela: só reaproveita a nota do control quando não há
|
|
350
|
-
// identidade pra checar (!sessionId) ou quando é a MESMA conversa. Conversa
|
|
351
|
-
// concorrente recente NÃO pode herdar a nota ativa do ponteiro global.
|
|
336
|
+
) {
|
|
337
|
+
writeHookOutput({});
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
} catch (err) {
|
|
342
|
+
debugLog('session-ensure fast-path skipped:', err);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const control = readControl(vaultBase);
|
|
346
|
+
|
|
347
|
+
if (control.status === 'active' && control.session_file) {
|
|
348
|
+
const activePath = join(vaultBase, control.session_file);
|
|
349
|
+
// Sem reuso-por-janela: só reaproveita a nota do control quando não há
|
|
350
|
+
// identidade pra checar (!sessionId) ou quando é a MESMA conversa. Conversa
|
|
351
|
+
// concorrente recente NÃO pode herdar a nota ativa do ponteiro global.
|
|
352
352
|
if (existsSync(activePath) && control.session_id === sessionId) {
|
|
353
|
-
const titled = maybeRetitleSession({
|
|
354
|
-
vaultBase,
|
|
355
|
-
relPath: control.session_file,
|
|
356
|
-
startedAt: control.started_at,
|
|
357
|
-
input,
|
|
358
|
-
});
|
|
359
|
-
const activeRelPath = titled.relPath;
|
|
360
|
-
if (titled.changed || control.session_file !== activeRelPath || control.session_id !== sessionId) {
|
|
361
|
-
writeControl(vaultBase, {
|
|
362
|
-
status: 'active',
|
|
363
|
-
session_file: activeRelPath,
|
|
364
|
-
last_session_file: activeRelPath,
|
|
365
|
-
started_at: control.started_at,
|
|
366
|
-
ended_at: '',
|
|
367
|
-
session_id: sessionId || control.session_id,
|
|
368
|
-
last_logged_turn_id: control.last_logged_turn_id || '',
|
|
369
|
-
});
|
|
370
|
-
}
|
|
371
|
-
upsertSessionRegistry(vaultBase, sessionId || control.session_id, {
|
|
372
|
-
session_file: activeRelPath,
|
|
373
|
-
status: 'active',
|
|
374
|
-
started_at: control.started_at,
|
|
375
|
-
ended_at: '',
|
|
353
|
+
const titled = maybeRetitleSession({
|
|
354
|
+
vaultBase,
|
|
355
|
+
relPath: control.session_file,
|
|
356
|
+
startedAt: control.started_at,
|
|
357
|
+
input,
|
|
358
|
+
});
|
|
359
|
+
const activeRelPath = titled.relPath;
|
|
360
|
+
if (titled.changed || control.session_file !== activeRelPath || control.session_id !== sessionId) {
|
|
361
|
+
writeControl(vaultBase, {
|
|
362
|
+
status: 'active',
|
|
363
|
+
session_file: activeRelPath,
|
|
364
|
+
last_session_file: activeRelPath,
|
|
365
|
+
started_at: control.started_at,
|
|
366
|
+
ended_at: '',
|
|
367
|
+
session_id: sessionId || control.session_id,
|
|
368
|
+
last_logged_turn_id: control.last_logged_turn_id || '',
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
upsertSessionRegistry(vaultBase, sessionId || control.session_id, {
|
|
372
|
+
session_file: activeRelPath,
|
|
373
|
+
status: 'active',
|
|
374
|
+
started_at: control.started_at,
|
|
375
|
+
ended_at: '',
|
|
376
376
|
transcript_path: identity.transcriptPath,
|
|
377
377
|
transcript_id: identity.transcriptId,
|
|
378
378
|
provider: identity.provider,
|
|
379
|
-
});
|
|
380
|
-
writeHookOutput({});
|
|
381
|
-
return;
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
|
|
379
|
+
});
|
|
380
|
+
writeHookOutput({});
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
385
|
const registered = readSessionRegistry(vaultBase).sessions?.[sessionId];
|
|
386
386
|
const resolvedTarget = registered?.session_file
|
|
387
387
|
? { sessionId, relPath: registered.session_file, startedAt: registered.started_at || '' }
|
|
388
388
|
: { sessionId, relPath: '', startedAt: '' };
|
|
389
389
|
if (resolvedTarget.relPath && activateExistingSession({ vaultBase, relPath: resolvedTarget.relPath, startedAt: resolvedTarget.startedAt, sessionId, input, now, identity })) {
|
|
390
|
-
outputActiveContext({
|
|
390
|
+
outputActiveContext({
|
|
391
391
|
relPath: resolvedTarget.relPath,
|
|
392
392
|
startedAt: resolvedTarget.startedAt || formatLocalIso(now),
|
|
393
|
-
vaultBase,
|
|
393
|
+
vaultBase,
|
|
394
394
|
message: `Sessão Obsidian reaberta em ${resolvedTarget.relPath}. ${basename(controlPath(vaultBase))} atualizado.`,
|
|
395
|
-
});
|
|
396
|
-
return;
|
|
397
|
-
}
|
|
398
|
-
|
|
395
|
+
});
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
399
|
const created = createSession({ vaultBase, sessionId, input, now, identity });
|
|
400
|
-
outputActiveContext({
|
|
401
|
-
relPath: created.relPath,
|
|
402
|
-
startedAt: created.startedAt,
|
|
403
|
-
vaultBase,
|
|
404
|
-
message: `Sessão Obsidian criada em ${created.relPath}. ${basename(controlPath(vaultBase))} atualizado.`,
|
|
405
|
-
});
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
try {
|
|
409
|
-
main();
|
|
410
|
-
} catch (error) {
|
|
411
|
-
process.stderr.write(`[wendkeep] UserPromptSubmit falhou: ${error.message}\n`);
|
|
412
|
-
writeHookOutput({});
|
|
413
|
-
}
|
|
400
|
+
outputActiveContext({
|
|
401
|
+
relPath: created.relPath,
|
|
402
|
+
startedAt: created.startedAt,
|
|
403
|
+
vaultBase,
|
|
404
|
+
message: `Sessão Obsidian criada em ${created.relPath}. ${basename(controlPath(vaultBase))} atualizado.`,
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
try {
|
|
409
|
+
main();
|
|
410
|
+
} catch (error) {
|
|
411
|
+
process.stderr.write(`[wendkeep] UserPromptSubmit falhou: ${error.message}\n`);
|
|
412
|
+
writeHookOutput({});
|
|
413
|
+
}
|