safeword 0.35.1 → 0.35.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/package.json
CHANGED
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { execSync } from 'node:child_process';
|
|
12
|
-
import { readdirSync, readFileSync } from 'node:fs';
|
|
13
|
-
import { basename, dirname, join, relative } from 'node:path';
|
|
12
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
13
|
+
import { basename, dirname, join, relative, resolve } from 'node:path';
|
|
14
14
|
|
|
15
15
|
export interface Entry {
|
|
16
16
|
timestamp: string;
|
|
@@ -107,6 +107,34 @@ export function normalizeRelative(filePath: string, cwd: string): string {
|
|
|
107
107
|
return filePath;
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
+
/**
|
|
111
|
+
* Resolve the project root reliably, regardless of where the session's cwd drifted.
|
|
112
|
+
*
|
|
113
|
+
* Claude Code passes `input.cwd` = the session's current working directory, which
|
|
114
|
+
* is not necessarily the project root. Hooks that wrote `join(cwd, '.safeword-project')`
|
|
115
|
+
* blindly would silently mkdirSync a bogus nested `.safeword-project/` inside whatever
|
|
116
|
+
* subdir the session happened to be in (e.g. `<root>/.safeword-project/tickets/.safeword-project/`).
|
|
117
|
+
*
|
|
118
|
+
* Implementation: walk up from `cwd` looking for a `.git` marker. Pure (no subprocess),
|
|
119
|
+
* preserves the cwd path form (matters on macOS where `git rev-parse --show-toplevel`
|
|
120
|
+
* canonicalizes `/var/folders/...` symlinks to `/private/var/folders/...` and breaks
|
|
121
|
+
* downstream `startsWith` comparisons against absolute paths captured by other code).
|
|
122
|
+
*
|
|
123
|
+
* Returns null when no `.git/` ancestor exists — caller bails silently rather than
|
|
124
|
+
* write to a stray path. Note: we intentionally do NOT match on `.safeword-project/`
|
|
125
|
+
* during the walk because the bogus nested directories created by the old code would
|
|
126
|
+
* mislead the resolver.
|
|
127
|
+
*/
|
|
128
|
+
export function resolveProjectRoot(cwd: string): string | null {
|
|
129
|
+
let current = resolve(cwd);
|
|
130
|
+
while (true) {
|
|
131
|
+
if (existsSync(join(current, '.git'))) return current;
|
|
132
|
+
const parent = dirname(current);
|
|
133
|
+
if (parent === current) return null;
|
|
134
|
+
current = parent;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
110
138
|
export function detectConflictFiles(cwd: string, transcriptPath: string | undefined): string[] {
|
|
111
139
|
if (!transcriptPath) return [];
|
|
112
140
|
const dirtyFiles = new Set(getDirtyFiles(cwd));
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
import { existsSync, readFileSync } from 'node:fs';
|
|
20
20
|
import { join } from 'node:path';
|
|
21
21
|
|
|
22
|
-
import { detectConflictFiles, type Entry, parseLogLine } from './lib/re-entry';
|
|
22
|
+
import { detectConflictFiles, type Entry, parseLogLine, resolveProjectRoot } from './lib/re-entry';
|
|
23
23
|
|
|
24
24
|
interface HookInput {
|
|
25
25
|
session_id?: string;
|
|
@@ -54,7 +54,11 @@ async function main(): Promise<void> {
|
|
|
54
54
|
const { session_id, cwd, source, transcript_path } = input;
|
|
55
55
|
if (!session_id || !cwd) return;
|
|
56
56
|
|
|
57
|
-
|
|
57
|
+
// Same cwd-drift defense as stop-reentry: resolve real project root.
|
|
58
|
+
const projectRoot = resolveProjectRoot(cwd);
|
|
59
|
+
if (!projectRoot) return;
|
|
60
|
+
|
|
61
|
+
const logPath = join(projectRoot, '.safeword-project', 're-entry.md');
|
|
58
62
|
const logExists = existsSync(logPath);
|
|
59
63
|
const content = logExists ? readFileSync(logPath, 'utf8').trim() : '';
|
|
60
64
|
|
|
@@ -72,7 +76,7 @@ async function main(): Promise<void> {
|
|
|
72
76
|
briefBody = renderBrief([allEntries[allEntries.length - 1]], { fromAnotherSession: true });
|
|
73
77
|
}
|
|
74
78
|
|
|
75
|
-
const conflictFiles = detectConflictFiles(
|
|
79
|
+
const conflictFiles = detectConflictFiles(projectRoot, transcript_path);
|
|
76
80
|
const conflictWarning = renderConflictWarning(conflictFiles);
|
|
77
81
|
|
|
78
82
|
// Nothing to inject in either channel → stay silent.
|
|
@@ -14,6 +14,7 @@ import { appendFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs';
|
|
|
14
14
|
import { join } from 'node:path';
|
|
15
15
|
|
|
16
16
|
import { getActiveTicket } from './lib/active-ticket';
|
|
17
|
+
import { resolveProjectRoot } from './lib/re-entry';
|
|
17
18
|
|
|
18
19
|
interface HookInput {
|
|
19
20
|
session_id?: string;
|
|
@@ -96,18 +97,25 @@ async function main(): Promise<void> {
|
|
|
96
97
|
const { session_id, transcript_path, cwd } = input;
|
|
97
98
|
if (!session_id || !transcript_path || !cwd) return;
|
|
98
99
|
|
|
100
|
+
// Claude Code passes input.cwd = the session's current working directory,
|
|
101
|
+
// which can drift into a subdirectory. Resolve the real project root so we
|
|
102
|
+
// never write to a stray nested `.safeword-project/`. Bail silently if we
|
|
103
|
+
// can't find a git repo to anchor to.
|
|
104
|
+
const projectRoot = resolveProjectRoot(cwd);
|
|
105
|
+
if (!projectRoot) return;
|
|
106
|
+
|
|
99
107
|
const assistantText = readLastAssistantText(transcript_path);
|
|
100
108
|
if (!assistantText) return;
|
|
101
109
|
|
|
102
110
|
const imperative = extractLastNextImperative(assistantText);
|
|
103
111
|
if (!imperative) return;
|
|
104
112
|
|
|
105
|
-
const ticketField = resolveTicketField(
|
|
113
|
+
const ticketField = resolveTicketField(projectRoot);
|
|
106
114
|
|
|
107
115
|
const timestamp = new Date().toISOString();
|
|
108
116
|
const line = `${timestamp} ${session_id} ${ticketField} Next: ${imperative}\n`;
|
|
109
117
|
|
|
110
|
-
const projectDirectory = join(
|
|
118
|
+
const projectDirectory = join(projectRoot, '.safeword-project');
|
|
111
119
|
if (!existsSync(projectDirectory)) {
|
|
112
120
|
mkdirSync(projectDirectory, { recursive: true });
|
|
113
121
|
}
|