wendkeep 0.37.0 → 0.38.1

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.
@@ -1,317 +1,340 @@
1
- #!/usr/bin/env node
2
- import { existsSync, writeFileSync } from 'fs';
3
- import { basename, join } from 'path';
4
- import { pathToFileURL } from 'url';
5
- import {
6
- controlPath,
7
- ensureDir,
8
- findActiveSessionByTranscript,
9
- formatDate,
10
- formatHourMinute,
11
- formatLocalIso,
12
- formatTime,
13
- getVaultBase,
14
- warnIfDefaultVault,
15
- providerMeta,
16
- readControl,
17
- readHookInput,
18
- readSessionRegistry,
19
- sessionFileName,
20
- sessionFolderRel,
21
- sessionSummaryFromInput,
22
- shouldReuseActiveSession,
23
- sweepStaleSessionsFile,
24
- toVaultRelative,
25
- uniquePath,
26
- upsertSessionRegistry,
27
- VAULT_COMPLEMENT_RULES,
28
- wikilinkFromRel,
29
- writeControl,
30
- writeHookOutput,
31
- yamlQuote,
32
- } from './obsidian-common.mjs';
33
-
34
- export function buildSessionContent({ relPath, now, summary = 'session', provider: providerId, sessionId = '' }) {
35
- const date = formatDate(now);
36
- const startedAt = formatLocalIso(now);
37
- const titleTime = formatTime(now).slice(0, 5);
38
- const objective = summary === 'session' ? 'Preencher durante a sessão.' : summary;
39
- // Import passes the transcript's provider so a Codex note is tagged codex (not the
40
- // ambient default); undefined falls back to providerMeta's own detection.
41
- const provider = providerMeta(providerId);
42
-
43
- return `---
44
- type: session
45
- date: ${date}
46
- started_at: ${startedAt}
47
- ended_at:
48
- provider: ${provider.id}
49
- session_id: ${sessionId ? yamlQuote(sessionId) : ''}
50
- status: active
51
- summary: ${yamlQuote(summary)}
52
- cssclasses:
53
- - topic-session
54
- tags:
55
- - sessao
56
- - ${provider.tag}
57
- - llm
58
- source: ${provider.source}
59
- related:
60
- ---
61
-
62
- # ${titleTime} - ${summary}
63
-
64
- ## Metadados
65
-
66
- - **Provider:** ${provider.label}
67
- - **Início:** ${startedAt}
68
- - **Fim:**
69
- - **Status:** active
70
- - **Arquivo:** \`${relPath}\`
71
-
72
- ## Objetivo da sessão
73
-
74
- > ${objective}
75
-
76
- ## Resumo vivo
77
-
78
- > Esta seção pode ser atualizada ao longo da sessão, mas o histórico de iterações deve ser preservado.
79
-
80
- ## Iterações
81
-
82
- ### ${titleTime} - Início da sessão
83
-
84
- Sessão iniciada automaticamente pelo hook de início (${provider.label}).
85
-
86
- ## Decisões geradas nesta sessão
87
-
88
- Nenhuma decisão registrada ainda.
89
-
90
- ## Bugs gerados nesta sessão
91
-
92
- Nenhum bug registrado ainda.
93
-
94
- ## Aprendizados gerados nesta sessão
95
-
96
- Nenhum aprendizado registrado ainda.
97
-
98
- ## Arquivos consultados
99
-
100
- Nenhum arquivo registrado ainda.
101
-
102
- ## Arquivos criados ou alterados
103
-
104
- Nenhum arquivo registrado ainda.
105
-
106
- ## Pendências
107
-
108
- - [ ] Revisar resumo da sessão
109
- - [ ] Verificar se houve decisões a registrar
110
- - [ ] Verificar se houve bugs a registrar
111
- - [ ] Verificar se houve aprendizados a registrar
112
-
113
- ## Encerramento
114
-
115
- Sessão ainda em andamento.
116
- `;
117
- }
118
-
119
- export 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 main() {
152
- const input = readHookInput();
153
- const vaultBase = getVaultBase(input);
154
- warnIfDefaultVault(input);
155
- const now = new Date();
156
- const sessionId = input.session_id || input.sessionId || '';
157
- const control = readControl(vaultBase);
158
-
159
- // Fecha sessões `active` órfãs (sem evento de fim — janela fechada/crash) antes
160
- // de seguir. Preserva a deste transcript: pode ser reaproveitada logo abaixo.
161
- try {
162
- sweepStaleSessionsFile(vaultBase, now, undefined, input.transcript_path || input.transcriptPath || '');
163
- } catch (error) {
164
- process.stderr.write(`[wendkeep] sweep de sessões falhou: ${error.message}\n`);
165
- }
166
-
167
- // Reuso da nota apontada pelo CURRENT_SESSION só quando é a MESMA conversa
168
- // (session_id idêntico). NÃO reusar por janela de tempo: o ponteiro global é
169
- // racy e uma conversa concorrente recente faria esta adotar a nota da outra.
170
- // Resume/compactação (session_id novo, mesmo transcript) é tratado abaixo por
171
- // identidade de transcript (findActiveSessionByTranscript).
172
- if (control.status === 'active' && control.session_file && control.session_id === sessionId) {
173
- const activePath = join(vaultBase, control.session_file);
174
- if (existsSync(activePath)) {
175
- upsertSessionRegistry(vaultBase, sessionId, {
176
- session_file: control.session_file,
177
- status: 'active',
178
- started_at: control.started_at,
179
- ended_at: '',
180
- });
181
- writeHookOutput({
182
- hookSpecificOutput: {
183
- hookEventName: 'SessionStart',
184
- additionalContext: buildAdditionalContext({
185
- relPath: control.session_file,
186
- startedAt: control.started_at,
187
- vaultBase,
188
- }),
189
- },
190
- });
191
- return;
192
- }
193
- }
194
-
195
- // Reuso DETERMINISTICO por session_id: o id e estavel em todo o ciclo da conversa
196
- // (inclusive resume/compactacao apos a janela de 10min e virada de dia). O ponteiro
197
- // CURRENT_SESSION e racy — sessoes Codex intercaladas o clobberam — entao olhamos o
198
- // registry direto pelo session_id antes de criar nota nova. Previne o split (mesma
199
- // sessao em duas notas). Recria o esqueleto no MESMO caminho se a nota sumiu.
200
- if (sessionId) {
201
- const known = readSessionRegistry(vaultBase).sessions?.[sessionId];
202
- if (known && known.status === 'active' && known.session_file) {
203
- const knownAbs = join(vaultBase, known.session_file);
204
- if (!existsSync(knownAbs)) {
205
- ensureDir(join(vaultBase, known.session_file.split('/').slice(0, -1).join('/')));
206
- writeFileSync(knownAbs, buildSessionContent({ relPath: known.session_file, now, summary: sessionSummaryFromInput(input), sessionId }), 'utf-8');
207
- }
208
- const startedAt = known.started_at || control.started_at || formatLocalIso(now);
209
- writeControl(vaultBase, {
210
- status: 'active',
211
- session_file: known.session_file,
212
- last_session_file: known.session_file,
213
- started_at: startedAt,
214
- ended_at: '',
215
- session_id: sessionId,
216
- last_logged_turn_id: control.last_logged_turn_id || '',
217
- });
218
- upsertSessionRegistry(vaultBase, sessionId, {
219
- session_file: known.session_file,
220
- status: 'active',
221
- started_at: startedAt,
222
- ended_at: '',
223
- transcript_path: input.transcript_path || input.transcriptPath || known.transcript_path || '',
224
- });
225
- writeHookOutput({
226
- hookSpecificOutput: {
227
- hookEventName: 'SessionStart',
228
- additionalContext: buildAdditionalContext({ relPath: known.session_file, startedAt, vaultBase }),
229
- },
230
- });
231
- return;
232
- }
233
- }
234
-
235
- // Re-init da conversa (compactação/resume) traz um session_id novo e cai fora
236
- // da janela de reuso; o transcript continua o mesmo. Reaproveita a sessão ativa
237
- // desse transcript em vez de criar um placeholder `HH-MM-codex`.
238
- const transcriptPath = input.transcript_path || input.transcriptPath || '';
239
- if (transcriptPath) {
240
- const match = findActiveSessionByTranscript(vaultBase, transcriptPath);
241
- if (match) {
242
- // fail-safe: a nota do registro pode ter sumido do disco (git stash/checkout/
243
- // sync removeram o arquivo). Em vez de mintar uma nota nova (split de sessão),
244
- // recria o esqueleto no MESMO caminho e segue reaproveitando.
245
- const matchAbs = join(vaultBase, match.session_file);
246
- if (!existsSync(matchAbs)) {
247
- ensureDir(join(vaultBase, match.session_file.split('/').slice(0, -1).join('/')));
248
- writeFileSync(matchAbs, buildSessionContent({ relPath: match.session_file, now, summary: sessionSummaryFromInput(input), sessionId: sessionId || match.sessionId }), 'utf-8');
249
- }
250
- const startedAt = match.started_at || control.started_at || formatLocalIso(now);
251
- writeControl(vaultBase, {
252
- status: 'active',
253
- session_file: match.session_file,
254
- last_session_file: match.session_file,
255
- started_at: startedAt,
256
- ended_at: '',
257
- session_id: sessionId || match.sessionId,
258
- last_logged_turn_id: control.last_logged_turn_id || '',
259
- });
260
- upsertSessionRegistry(vaultBase, sessionId || match.sessionId, {
261
- session_file: match.session_file,
262
- status: 'active',
263
- started_at: startedAt,
264
- ended_at: '',
265
- transcript_path: transcriptPath,
266
- });
267
- writeHookOutput({
268
- hookSpecificOutput: {
269
- hookEventName: 'SessionStart',
270
- additionalContext: buildAdditionalContext({ relPath: match.session_file, startedAt, vaultBase }),
271
- },
272
- });
273
- return;
274
- }
275
- }
276
-
277
- const summary = sessionSummaryFromInput(input);
278
- const { absPath, relPath } = allocateSessionPath(vaultBase, now, summary);
279
- const startedAt = formatLocalIso(now);
280
- writeFileSync(absPath, buildSessionContent({ relPath, now, summary, sessionId }), 'utf-8');
281
- writeControl(vaultBase, {
282
- status: 'active',
283
- session_file: relPath,
284
- last_session_file: relPath,
285
- started_at: startedAt,
286
- session_id: sessionId,
287
- });
288
- upsertSessionRegistry(vaultBase, sessionId, {
289
- session_file: relPath,
290
- status: 'active',
291
- started_at: startedAt,
292
- ended_at: '',
293
- });
294
-
295
- writeHookOutput({
296
- hookSpecificOutput: {
297
- hookEventName: 'SessionStart',
298
- additionalContext: buildAdditionalContext({ relPath, startedAt, vaultBase }),
299
- },
300
- systemMessage: [
301
- `Sessão ${providerMeta().label} criada em ${relPath}.`,
302
- `${basename(controlPath(vaultBase))} atualizado.`,
303
- 'Iterações devem ser anexadas, nunca sobrescritas.',
304
- ].join(' '),
305
- });
306
- }
307
-
308
- if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
309
- try {
310
- main();
311
- } catch (error) {
312
- process.stderr.write(`[wendkeep] SessionStart falhou: ${error.message}\n`);
313
- writeHookOutput({
314
- systemMessage: `[wendkeep] Não foi possível criar a sessão Obsidian: ${error.message}`,
315
- });
316
- }
317
- }
1
+ #!/usr/bin/env node
2
+ import { existsSync, writeFileSync } from 'fs';
3
+ import { basename, join } from 'path';
4
+ import { pathToFileURL } from 'url';
5
+ import {
6
+ controlPath,
7
+ ensureDir,
8
+ findActiveSessionByTranscript,
9
+ formatDate,
10
+ formatHourMinute,
11
+ formatLocalIso,
12
+ formatTime,
13
+ getVaultBase,
14
+ warnIfDefaultVault,
15
+ providerMeta,
16
+ readControl,
17
+ readHookInput,
18
+ readSessionRegistry,
19
+ sessionFileName,
20
+ sessionFolderRel,
21
+ sessionSummaryFromInput,
22
+ shouldReuseActiveSession,
23
+ sweepStaleSessionsFile,
24
+ toVaultRelative,
25
+ uniquePath,
26
+ upsertSessionRegistry,
27
+ VAULT_COMPLEMENT_RULES,
28
+ wikilinkFromRel,
29
+ writeControl,
30
+ writeHookOutput,
31
+ yamlQuote,
32
+ } from './obsidian-common.mjs';
33
+ import { resolveSessionIdentity } from './session-identity.mjs';
34
+
35
+ export function buildSessionContent({ relPath, now, summary = 'session', provider: providerId, sessionId = '' }) {
36
+ const date = formatDate(now);
37
+ const startedAt = formatLocalIso(now);
38
+ const titleTime = formatTime(now).slice(0, 5);
39
+ const objective = summary === 'session' ? 'Preencher durante a sessão.' : summary;
40
+ // Import passes the transcript's provider so a Codex note is tagged codex (not the
41
+ // ambient default); undefined falls back to providerMeta's own detection.
42
+ const provider = providerMeta(providerId);
43
+
44
+ return `---
45
+ type: session
46
+ date: ${date}
47
+ started_at: ${startedAt}
48
+ ended_at:
49
+ provider: ${provider.id}
50
+ session_id: ${sessionId ? yamlQuote(sessionId) : ''}
51
+ status: active
52
+ summary: ${yamlQuote(summary)}
53
+ cssclasses:
54
+ - topic-session
55
+ tags:
56
+ - sessao
57
+ - ${provider.tag}
58
+ - llm
59
+ source: ${provider.source}
60
+ related:
61
+ ---
62
+
63
+ # ${titleTime} - ${summary}
64
+
65
+ ## Metadados
66
+
67
+ - **Provider:** ${provider.label}
68
+ - **Início:** ${startedAt}
69
+ - **Fim:**
70
+ - **Status:** active
71
+ - **Arquivo:** \`${relPath}\`
72
+
73
+ ## Objetivo da sessão
74
+
75
+ > ${objective}
76
+
77
+ ## Resumo vivo
78
+
79
+ > Esta seção pode ser atualizada ao longo da sessão, mas o histórico de iterações deve ser preservado.
80
+
81
+ ## Iterações
82
+
83
+ ### ${titleTime} - Início da sessão
84
+
85
+ Sessão iniciada automaticamente pelo hook de início (${provider.label}).
86
+
87
+ ## Decisões geradas nesta sessão
88
+
89
+ Nenhuma decisão registrada ainda.
90
+
91
+ ## Bugs gerados nesta sessão
92
+
93
+ Nenhum bug registrado ainda.
94
+
95
+ ## Aprendizados gerados nesta sessão
96
+
97
+ Nenhum aprendizado registrado ainda.
98
+
99
+ ## Arquivos consultados
100
+
101
+ Nenhum arquivo registrado ainda.
102
+
103
+ ## Arquivos criados ou alterados
104
+
105
+ Nenhum arquivo registrado ainda.
106
+
107
+ ## Pendências
108
+
109
+ - [ ] Revisar resumo da sessão
110
+ - [ ] Verificar se houve decisões a registrar
111
+ - [ ] Verificar se houve bugs a registrar
112
+ - [ ] Verificar se houve aprendizados a registrar
113
+
114
+ ## Encerramento
115
+
116
+ Sessão ainda em andamento.
117
+ `;
118
+ }
119
+
120
+ export function allocateSessionPath(vaultBase, now, summary = 'session') {
121
+ const folderRel = sessionFolderRel(now, vaultBase);
122
+ const folderAbs = join(vaultBase, folderRel);
123
+ ensureDir(folderAbs);
124
+
125
+ const baseName = sessionFileName(now, summary);
126
+ const filePath = uniquePath(join(folderAbs, baseName));
127
+ return {
128
+ absPath: filePath,
129
+ relPath: toVaultRelative(vaultBase, filePath),
130
+ };
131
+ }
132
+
133
+ function buildAdditionalContext({ relPath, startedAt, vaultBase }) {
134
+ const controlRel = toVaultRelative(vaultBase, controlPath(vaultBase));
135
+ return [
136
+ '<obsidian_session>',
137
+ `Sessão Obsidian ativa: ${relPath}`,
138
+ `Controle atualizado: ${controlRel}`,
139
+ `Início: ${startedAt}`,
140
+ '',
141
+ 'Use a sessão ativa como log desta conversa.',
142
+ 'Antes de registrar informações, leia `.brain/CURRENT_SESSION.md` no vault.',
143
+ '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`.',
144
+ '',
145
+ ...VAULT_COMPLEMENT_RULES,
146
+ 'Não registre chaves, tokens, senhas ou segredos; substitua por `[REDACTED_SECRET]`.',
147
+ `Wikilink da sessão: ${wikilinkFromRel(relPath)}`,
148
+ '</obsidian_session>',
149
+ ].join('\n');
150
+ }
151
+
152
+ function main() {
153
+ const input = readHookInput();
154
+ const vaultBase = getVaultBase(input);
155
+ warnIfDefaultVault(input);
156
+ const now = new Date();
157
+ const provider = providerMeta();
158
+ const identity = resolveSessionIdentity(vaultBase, input, provider.id);
159
+ if (identity.state !== 'resolved') {
160
+ writeHookOutput({
161
+ hookSpecificOutput: {
162
+ hookEventName: 'SessionStart',
163
+ additionalContext: `<obsidian_session_deferred>Memória global disponível, mas nenhuma escrita de sessão foi feita: ${identity.diagnostics.join('; ')}.</obsidian_session_deferred>`,
164
+ },
165
+ systemMessage: `[wendkeep] Identidade de sessão adiada: ${identity.diagnostics.join('; ')}`,
166
+ });
167
+ return;
168
+ }
169
+ const sessionId = identity.canonicalConversationId;
170
+ const transcriptPath = identity.transcriptPath;
171
+ const control = readControl(vaultBase);
172
+
173
+ // Fecha sessões `active` órfãs (sem evento de fim — janela fechada/crash) antes
174
+ // de seguir. Preserva a deste transcript: pode ser reaproveitada logo abaixo.
175
+ try {
176
+ sweepStaleSessionsFile(vaultBase, now, undefined, transcriptPath);
177
+ } catch (error) {
178
+ process.stderr.write(`[wendkeep] sweep de sessões falhou: ${error.message}\n`);
179
+ }
180
+
181
+ // Reuso da nota apontada pelo CURRENT_SESSION só quando é a MESMA conversa
182
+ // (session_id idêntico). NÃO reusar por janela de tempo: o ponteiro global é
183
+ // racy e uma conversa concorrente recente faria esta adotar a nota da outra.
184
+ // Resume/compactação (session_id novo, mesmo transcript) é tratado abaixo por
185
+ // identidade de transcript (findActiveSessionByTranscript).
186
+ if (control.status === 'active' && control.session_file && control.session_id === sessionId) {
187
+ const activePath = join(vaultBase, control.session_file);
188
+ if (existsSync(activePath)) {
189
+ upsertSessionRegistry(vaultBase, sessionId, {
190
+ session_file: control.session_file,
191
+ status: 'active',
192
+ started_at: control.started_at,
193
+ ended_at: '',
194
+ provider: provider.id,
195
+ transcript_path: transcriptPath,
196
+ transcript_id: identity.transcriptId,
197
+ });
198
+ writeHookOutput({
199
+ hookSpecificOutput: {
200
+ hookEventName: 'SessionStart',
201
+ additionalContext: buildAdditionalContext({
202
+ relPath: control.session_file,
203
+ startedAt: control.started_at,
204
+ vaultBase,
205
+ }),
206
+ },
207
+ });
208
+ return;
209
+ }
210
+ }
211
+
212
+ // Reuso DETERMINISTICO por session_id: o id e estavel em todo o ciclo da conversa
213
+ // (inclusive resume/compactacao apos a janela de 10min e virada de dia). O ponteiro
214
+ // CURRENT_SESSION e racy — sessoes Codex intercaladas o clobberam — entao olhamos o
215
+ // registry direto pelo session_id antes de criar nota nova. Previne o split (mesma
216
+ // sessao em duas notas). Recria o esqueleto no MESMO caminho se a nota sumiu.
217
+ if (sessionId) {
218
+ const known = readSessionRegistry(vaultBase).sessions?.[sessionId];
219
+ if (known && known.status === 'active' && known.session_file) {
220
+ const knownAbs = join(vaultBase, known.session_file);
221
+ if (!existsSync(knownAbs)) {
222
+ ensureDir(join(vaultBase, known.session_file.split('/').slice(0, -1).join('/')));
223
+ writeFileSync(knownAbs, buildSessionContent({ relPath: known.session_file, now, summary: sessionSummaryFromInput(input), sessionId }), 'utf-8');
224
+ }
225
+ const startedAt = known.started_at || control.started_at || formatLocalIso(now);
226
+ writeControl(vaultBase, {
227
+ status: 'active',
228
+ session_file: known.session_file,
229
+ last_session_file: known.session_file,
230
+ started_at: startedAt,
231
+ ended_at: '',
232
+ session_id: sessionId,
233
+ last_logged_turn_id: control.last_logged_turn_id || '',
234
+ });
235
+ upsertSessionRegistry(vaultBase, sessionId, {
236
+ session_file: known.session_file,
237
+ status: 'active',
238
+ started_at: startedAt,
239
+ ended_at: '',
240
+ transcript_path: transcriptPath || known.transcript_path || '',
241
+ transcript_id: identity.transcriptId,
242
+ provider: provider.id,
243
+ });
244
+ writeHookOutput({
245
+ hookSpecificOutput: {
246
+ hookEventName: 'SessionStart',
247
+ additionalContext: buildAdditionalContext({ relPath: known.session_file, startedAt, vaultBase }),
248
+ },
249
+ });
250
+ return;
251
+ }
252
+ }
253
+
254
+ // Re-init da conversa (compactação/resume) traz um session_id novo e cai fora
255
+ // da janela de reuso; o transcript continua o mesmo. Reaproveita a sessão ativa
256
+ // desse transcript em vez de criar um placeholder `HH-MM-codex`.
257
+ if (transcriptPath) {
258
+ const match = findActiveSessionByTranscript(vaultBase, transcriptPath);
259
+ if (match) {
260
+ // fail-safe: a nota do registro pode ter sumido do disco (git stash/checkout/
261
+ // sync removeram o arquivo). Em vez de mintar uma nota nova (split de sessão),
262
+ // recria o esqueleto no MESMO caminho e segue reaproveitando.
263
+ const matchAbs = join(vaultBase, match.session_file);
264
+ if (!existsSync(matchAbs)) {
265
+ ensureDir(join(vaultBase, match.session_file.split('/').slice(0, -1).join('/')));
266
+ writeFileSync(matchAbs, buildSessionContent({ relPath: match.session_file, now, summary: sessionSummaryFromInput(input), sessionId: sessionId || match.sessionId }), 'utf-8');
267
+ }
268
+ const startedAt = match.started_at || control.started_at || formatLocalIso(now);
269
+ writeControl(vaultBase, {
270
+ status: 'active',
271
+ session_file: match.session_file,
272
+ last_session_file: match.session_file,
273
+ started_at: startedAt,
274
+ ended_at: '',
275
+ session_id: sessionId || match.sessionId,
276
+ last_logged_turn_id: control.last_logged_turn_id || '',
277
+ });
278
+ upsertSessionRegistry(vaultBase, sessionId || match.sessionId, {
279
+ session_file: match.session_file,
280
+ status: 'active',
281
+ started_at: startedAt,
282
+ ended_at: '',
283
+ transcript_path: transcriptPath,
284
+ transcript_id: identity.transcriptId,
285
+ provider: provider.id,
286
+ });
287
+ writeHookOutput({
288
+ hookSpecificOutput: {
289
+ hookEventName: 'SessionStart',
290
+ additionalContext: buildAdditionalContext({ relPath: match.session_file, startedAt, vaultBase }),
291
+ },
292
+ });
293
+ return;
294
+ }
295
+ }
296
+
297
+ const summary = sessionSummaryFromInput(input);
298
+ const { absPath, relPath } = allocateSessionPath(vaultBase, now, summary);
299
+ const startedAt = formatLocalIso(now);
300
+ writeFileSync(absPath, buildSessionContent({ relPath, now, summary, sessionId, provider: provider.id }), 'utf-8');
301
+ writeControl(vaultBase, {
302
+ status: 'active',
303
+ session_file: relPath,
304
+ last_session_file: relPath,
305
+ started_at: startedAt,
306
+ session_id: sessionId,
307
+ });
308
+ upsertSessionRegistry(vaultBase, sessionId, {
309
+ session_file: relPath,
310
+ status: 'active',
311
+ started_at: startedAt,
312
+ ended_at: '',
313
+ provider: provider.id,
314
+ transcript_path: transcriptPath,
315
+ transcript_id: identity.transcriptId,
316
+ });
317
+
318
+ writeHookOutput({
319
+ hookSpecificOutput: {
320
+ hookEventName: 'SessionStart',
321
+ additionalContext: buildAdditionalContext({ relPath, startedAt, vaultBase }),
322
+ },
323
+ systemMessage: [
324
+ `Sessão ${provider.label} criada em ${relPath}.`,
325
+ `${basename(controlPath(vaultBase))} atualizado.`,
326
+ 'Iterações devem ser anexadas, nunca sobrescritas.',
327
+ ].join(' '),
328
+ });
329
+ }
330
+
331
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
332
+ try {
333
+ main();
334
+ } catch (error) {
335
+ process.stderr.write(`[wendkeep] SessionStart falhou: ${error.message}\n`);
336
+ writeHookOutput({
337
+ systemMessage: `[wendkeep] Não foi possível criar a sessão Obsidian: ${error.message}`,
338
+ });
339
+ }
340
+ }