codiedev 0.9.4 → 0.9.5
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/dist/hook.js +15 -13
- package/dist/repoResolver.d.ts +1 -1
- package/dist/repoResolver.js +1 -1
- package/dist/utils.d.ts +14 -0
- package/dist/utils.js +124 -0
- package/package.json +1 -1
package/dist/hook.js
CHANGED
|
@@ -236,31 +236,33 @@ async function main() {
|
|
|
236
236
|
if (!remoteUrl) {
|
|
237
237
|
remoteUrl = (0, utils_1.getGitRemoteUrl)(workspaceRoot);
|
|
238
238
|
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
//
|
|
243
|
-
//
|
|
244
|
-
//
|
|
245
|
-
//
|
|
246
|
-
|
|
239
|
+
// Capture-first: the transcript is the asset; the repo is just a tag. We do
|
|
240
|
+
// NOT bail when `remoteUrl` is null — that previously dropped EVERY session
|
|
241
|
+
// for users whose workspace couldn't resolve a remote (no git binary on lean
|
|
242
|
+
// Alpine images, no `origin`, or a submodule whose parent .git isn't mounted),
|
|
243
|
+
// with no error and a green `doctor`. Upload regardless; the backend accepts
|
|
244
|
+
// an unmatched capture (repoId null) and surfaces it for triage.
|
|
245
|
+
//
|
|
246
|
+
// Best-effort local match — backend re-resolves from `remoteUrl` against the
|
|
247
|
+
// company's current repo list, so a stale/empty local cache doesn't matter.
|
|
248
|
+
const matchedRepo = remoteUrl ? (0, utils_1.matchRepo)(remoteUrl, config.repos) : null;
|
|
247
249
|
const stats = mode === "codex"
|
|
248
250
|
? (0, utils_1.parseCodexStats)(transcriptContent)
|
|
249
251
|
: mode === "cursor"
|
|
250
252
|
? { messageCount: 0, toolCallCount: 0 }
|
|
251
253
|
: (0, utils_1.parseClaudeCodeStats)(transcriptContent);
|
|
252
254
|
const transcriptBase64 = Buffer.from(transcriptContent, "utf8").toString("base64");
|
|
253
|
-
const payload = {
|
|
255
|
+
const payload = (0, utils_1.buildCapturePayload)({
|
|
254
256
|
sessionId: session_id,
|
|
255
|
-
|
|
256
|
-
|
|
257
|
+
transcriptBase64,
|
|
258
|
+
remoteUrl,
|
|
259
|
+
matchedRepoId: matchedRepo?.repoId,
|
|
257
260
|
companyId: config.companyId,
|
|
258
|
-
transcript: transcriptBase64,
|
|
259
261
|
messageCount: stats.messageCount,
|
|
260
262
|
toolCallCount: stats.toolCallCount,
|
|
261
263
|
capturedAt: Date.now(),
|
|
262
264
|
source: mode,
|
|
263
|
-
};
|
|
265
|
+
});
|
|
264
266
|
// Fire the upload from a fully detached child and exit immediately. Awaiting
|
|
265
267
|
// it inline let Claude Code's SessionEnd shutdown window elapse and kill the
|
|
266
268
|
// still-running hook ("Hook cancelled") on large transcripts — even though
|
package/dist/repoResolver.d.ts
CHANGED
package/dist/repoResolver.js
CHANGED
|
@@ -30,7 +30,7 @@ function resolveRepoForCapture(input) {
|
|
|
30
30
|
if (hit)
|
|
31
31
|
return { repoId: hit._id, source: "provided" };
|
|
32
32
|
}
|
|
33
|
-
const parsed = parseRemoteUrl(remoteUrl);
|
|
33
|
+
const parsed = remoteUrl ? parseRemoteUrl(remoteUrl) : null;
|
|
34
34
|
if (!parsed) {
|
|
35
35
|
return { repoId: null, source: "unmatched" };
|
|
36
36
|
}
|
package/dist/utils.d.ts
CHANGED
|
@@ -14,6 +14,8 @@ export interface CodiedevConfig {
|
|
|
14
14
|
}
|
|
15
15
|
export declare function readConfig(): CodiedevConfig | null;
|
|
16
16
|
export declare function writeConfig(config: CodiedevConfig): void;
|
|
17
|
+
export declare function parseGitRemoteFromConfig(configText: string): string | null;
|
|
18
|
+
export declare function readGitRemoteFromConfig(startDir: string): string | null;
|
|
17
19
|
export declare function getGitRemoteUrl(cwd: string): string | null;
|
|
18
20
|
export declare function normalizeRepoUrl(url: string): string;
|
|
19
21
|
export declare function matchRepo(remoteUrl: string, repos: Array<{
|
|
@@ -25,6 +27,18 @@ export declare function matchRepo(remoteUrl: string, repos: Array<{
|
|
|
25
27
|
url: string;
|
|
26
28
|
fullName: string;
|
|
27
29
|
} | null;
|
|
30
|
+
export interface CapturePayloadInput {
|
|
31
|
+
sessionId?: string;
|
|
32
|
+
transcriptBase64: string;
|
|
33
|
+
remoteUrl: string | null;
|
|
34
|
+
matchedRepoId?: string;
|
|
35
|
+
companyId: string;
|
|
36
|
+
messageCount: number;
|
|
37
|
+
toolCallCount: number;
|
|
38
|
+
capturedAt: number;
|
|
39
|
+
source: string;
|
|
40
|
+
}
|
|
41
|
+
export declare function buildCapturePayload(input: CapturePayloadInput): Record<string, unknown>;
|
|
28
42
|
export declare function hashToken(token: string): string;
|
|
29
43
|
export declare function claudeCodeInstalled(): boolean;
|
|
30
44
|
export declare function codexInstalled(): boolean;
|
package/dist/utils.js
CHANGED
|
@@ -35,9 +35,12 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.readConfig = readConfig;
|
|
37
37
|
exports.writeConfig = writeConfig;
|
|
38
|
+
exports.parseGitRemoteFromConfig = parseGitRemoteFromConfig;
|
|
39
|
+
exports.readGitRemoteFromConfig = readGitRemoteFromConfig;
|
|
38
40
|
exports.getGitRemoteUrl = getGitRemoteUrl;
|
|
39
41
|
exports.normalizeRepoUrl = normalizeRepoUrl;
|
|
40
42
|
exports.matchRepo = matchRepo;
|
|
43
|
+
exports.buildCapturePayload = buildCapturePayload;
|
|
41
44
|
exports.hashToken = hashToken;
|
|
42
45
|
exports.claudeCodeInstalled = claudeCodeInstalled;
|
|
43
46
|
exports.codexInstalled = codexInstalled;
|
|
@@ -89,7 +92,115 @@ function writeConfig(config) {
|
|
|
89
92
|
}
|
|
90
93
|
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), "utf8");
|
|
91
94
|
}
|
|
95
|
+
// Parse a remote URL out of git-config INI text WITHOUT invoking git. Prefers
|
|
96
|
+
// the "origin" remote; falls back to the first remote found. Returns null if
|
|
97
|
+
// there are no remote sections.
|
|
98
|
+
function parseGitRemoteFromConfig(configText) {
|
|
99
|
+
const remotes = {};
|
|
100
|
+
let current = null;
|
|
101
|
+
for (const raw of configText.split(/\r?\n/)) {
|
|
102
|
+
const line = raw.trim();
|
|
103
|
+
const remoteHeader = line.match(/^\[\s*remote\s+"([^"]+)"\s*\]$/);
|
|
104
|
+
if (remoteHeader) {
|
|
105
|
+
current = remoteHeader[1];
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (line.startsWith("[")) {
|
|
109
|
+
// Any other section header ends the current remote context, so a
|
|
110
|
+
// `url =` under e.g. [core] or [some-section] is never mistaken for one.
|
|
111
|
+
current = null;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (current) {
|
|
115
|
+
const kv = line.match(/^url\s*=\s*(.+)$/);
|
|
116
|
+
if (kv && remotes[current] === undefined) {
|
|
117
|
+
remotes[current] = kv[1].trim();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (remotes.origin)
|
|
122
|
+
return remotes.origin;
|
|
123
|
+
const first = Object.values(remotes)[0];
|
|
124
|
+
return first ?? null;
|
|
125
|
+
}
|
|
126
|
+
// Locate the git directory for `startDir`, walking up parent directories.
|
|
127
|
+
// Handles a `.git` directory (normal repo), a `.git` FILE containing
|
|
128
|
+
// `gitdir: <path>` (submodules, some devcontainer/worktree layouts), and stops
|
|
129
|
+
// at the filesystem root. Returns the resolved git dir path, or null.
|
|
130
|
+
function resolveGitDir(startDir) {
|
|
131
|
+
let dir = path.resolve(startDir);
|
|
132
|
+
// eslint-disable-next-line no-constant-condition
|
|
133
|
+
while (true) {
|
|
134
|
+
const dotGit = path.join(dir, ".git");
|
|
135
|
+
try {
|
|
136
|
+
const stat = fs.statSync(dotGit);
|
|
137
|
+
if (stat.isDirectory())
|
|
138
|
+
return dotGit;
|
|
139
|
+
if (stat.isFile()) {
|
|
140
|
+
const m = fs.readFileSync(dotGit, "utf8").match(/^gitdir:\s*(.+)\s*$/m);
|
|
141
|
+
if (m) {
|
|
142
|
+
const target = m[1].trim();
|
|
143
|
+
return path.isAbsolute(target) ? target : path.resolve(dir, target);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
// No .git here — keep walking up.
|
|
149
|
+
}
|
|
150
|
+
const parent = path.dirname(dir);
|
|
151
|
+
if (parent === dir)
|
|
152
|
+
return null;
|
|
153
|
+
dir = parent;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
// Ordered list of config files that may hold the remotes for a git dir. Linked
|
|
157
|
+
// worktrees keep their remotes in the COMMON (main) git dir, pointed at by a
|
|
158
|
+
// `commondir` file, so we check that first, then the git dir's own config.
|
|
159
|
+
function gitConfigCandidates(gitDir) {
|
|
160
|
+
const candidates = [];
|
|
161
|
+
try {
|
|
162
|
+
const commondirFile = path.join(gitDir, "commondir");
|
|
163
|
+
if (fs.existsSync(commondirFile)) {
|
|
164
|
+
const rel = fs.readFileSync(commondirFile, "utf8").trim();
|
|
165
|
+
const commonDir = path.isAbsolute(rel) ? rel : path.resolve(gitDir, rel);
|
|
166
|
+
candidates.push(path.join(commonDir, "config"));
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
// ignore — fall through to the plain config below
|
|
171
|
+
}
|
|
172
|
+
candidates.push(path.join(gitDir, "config"));
|
|
173
|
+
return candidates;
|
|
174
|
+
}
|
|
175
|
+
// Resolve a repo's remote URL by reading .git/config directly — no `git`
|
|
176
|
+
// binary required. This is the primary path so lean container images (Alpine
|
|
177
|
+
// devcontainers without git) still capture sessions instead of silently
|
|
178
|
+
// dropping them.
|
|
179
|
+
function readGitRemoteFromConfig(startDir) {
|
|
180
|
+
const gitDir = resolveGitDir(startDir);
|
|
181
|
+
if (!gitDir)
|
|
182
|
+
return null;
|
|
183
|
+
for (const cfg of gitConfigCandidates(gitDir)) {
|
|
184
|
+
try {
|
|
185
|
+
const url = parseGitRemoteFromConfig(fs.readFileSync(cfg, "utf8"));
|
|
186
|
+
if (url)
|
|
187
|
+
return url;
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
// try the next candidate
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
92
195
|
function getGitRemoteUrl(cwd) {
|
|
196
|
+
// Primary: read .git/config directly. Works even when the `git` binary is
|
|
197
|
+
// absent (lean Alpine/devcontainer images), which previously made the
|
|
198
|
+
// SessionEnd hook silently drop every session.
|
|
199
|
+
const fromConfig = readGitRemoteFromConfig(cwd);
|
|
200
|
+
if (fromConfig)
|
|
201
|
+
return fromConfig;
|
|
202
|
+
// Fallback: ask git itself, for setups the direct read doesn't cover
|
|
203
|
+
// (insteadOf rewrites, includeIf'd remotes, unusual gitdir indirection).
|
|
93
204
|
try {
|
|
94
205
|
const url = (0, child_process_1.execSync)("git remote get-url origin", {
|
|
95
206
|
cwd,
|
|
@@ -136,6 +247,19 @@ function matchRepo(remoteUrl, repos) {
|
|
|
136
247
|
}
|
|
137
248
|
return null;
|
|
138
249
|
}
|
|
250
|
+
function buildCapturePayload(input) {
|
|
251
|
+
return {
|
|
252
|
+
sessionId: input.sessionId,
|
|
253
|
+
repoUrl: input.remoteUrl ?? "",
|
|
254
|
+
repoId: input.matchedRepoId,
|
|
255
|
+
companyId: input.companyId,
|
|
256
|
+
transcript: input.transcriptBase64,
|
|
257
|
+
messageCount: input.messageCount,
|
|
258
|
+
toolCallCount: input.toolCallCount,
|
|
259
|
+
capturedAt: input.capturedAt,
|
|
260
|
+
source: input.source,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
139
263
|
function hashToken(token) {
|
|
140
264
|
let hash = 0;
|
|
141
265
|
for (let i = 0; i < token.length; i++) {
|
package/package.json
CHANGED