codiedev 0.9.16 → 0.9.18

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.
@@ -15,6 +15,17 @@ export interface CaptureReadiness {
15
15
  headSha: string | null;
16
16
  }
17
17
  export declare function assessCaptureReadiness<R>(cwd: string, repos: R[], deps: CaptureReadinessDeps<R>): CaptureReadiness;
18
+ export interface WorkspaceRepoScanDeps<R = unknown> {
19
+ listChildDirs: (cwd: string) => string[];
20
+ isRepo: (dir: string) => boolean;
21
+ getRemoteUrl: (dir: string) => string | null;
22
+ matchRepo: (url: string, repos: R[]) => R | null;
23
+ }
24
+ export interface WorkspaceRepoScan {
25
+ repoCount: number;
26
+ matchedCount: number;
27
+ }
28
+ export declare function assessWorkspaceRepos<R>(cwd: string, repos: R[], deps: WorkspaceRepoScanDeps<R>): WorkspaceRepoScan;
18
29
  export declare function formatRelativeAge(ms: number): string;
19
30
  export interface LastCaptureSummary {
20
31
  status: "pass" | "warn";
@@ -8,6 +8,7 @@
8
8
  // production wiring (in doctor.ts) plugs in execSync + the real git helpers.
9
9
  Object.defineProperty(exports, "__esModule", { value: true });
10
10
  exports.assessCaptureReadiness = assessCaptureReadiness;
11
+ exports.assessWorkspaceRepos = assessWorkspaceRepos;
11
12
  exports.formatRelativeAge = formatRelativeAge;
12
13
  exports.summarizeLastCapture = summarizeLastCapture;
13
14
  exports.summarizeServerCapture = summarizeServerCapture;
@@ -41,6 +42,26 @@ function assessCaptureReadiness(cwd, repos, deps) {
41
42
  headSha: deps.getHeadSha(cwd),
42
43
  };
43
44
  }
45
+ function assessWorkspaceRepos(cwd, repos, deps) {
46
+ let childDirs;
47
+ try {
48
+ childDirs = deps.listChildDirs(cwd);
49
+ }
50
+ catch {
51
+ return { repoCount: 0, matchedCount: 0 };
52
+ }
53
+ let repoCount = 0;
54
+ let matchedCount = 0;
55
+ for (const dir of childDirs) {
56
+ if (!deps.isRepo(dir))
57
+ continue;
58
+ repoCount++;
59
+ const url = deps.getRemoteUrl(dir);
60
+ if (url && deps.matchRepo(url, repos) !== null)
61
+ matchedCount++;
62
+ }
63
+ return { repoCount, matchedCount };
64
+ }
44
65
  function formatRelativeAge(ms) {
45
66
  if (ms < 0)
46
67
  ms = 0;
@@ -224,6 +224,23 @@ async function runDoctor(_args) {
224
224
  getBranch: utils_1.getGitBranch,
225
225
  getHeadSha: utils_1.getGitHeadSha,
226
226
  });
227
+ // Workspace-root mode: the cwd isn't a repo itself, but git repos sit
228
+ // directly under it (one `.devcontainer/` at a parent folder of several
229
+ // cloned repos). Capture tags the sub-repo a session edits, so report that
230
+ // instead of flatly warning "not a work tree / no remote" — the exact output
231
+ // that made working workspace-root setups read as broken.
232
+ const workspaceScan = readiness.gitOnPath && !readiness.insideWorkTree
233
+ ? (0, captureReadiness_1.assessWorkspaceRepos)(cwd, config.repos ?? [], {
234
+ listChildDirs: (dir) => fs
235
+ .readdirSync(dir, { withFileTypes: true })
236
+ .filter((e) => e.isDirectory())
237
+ .map((e) => path.join(dir, e.name)),
238
+ isRepo: (dir) => fs.existsSync(path.join(dir, ".git")),
239
+ getRemoteUrl: utils_1.getGitRemoteUrl,
240
+ matchRepo: utils_1.matchRepo,
241
+ })
242
+ : { repoCount: 0, matchedCount: 0 };
243
+ const workspaceMode = workspaceScan.repoCount > 0;
227
244
  checks.push({
228
245
  name: "git on PATH",
229
246
  status: readiness.gitOnPath ? "pass" : "fail",
@@ -233,26 +250,34 @@ async function runDoctor(_args) {
233
250
  });
234
251
  checks.push({
235
252
  name: "Inside a git work tree (current dir)",
236
- status: readiness.insideWorkTree ? "pass" : "warn",
253
+ status: readiness.insideWorkTree || workspaceMode ? "pass" : "warn",
237
254
  detail: readiness.insideWorkTree
238
255
  ? undefined
239
- : "current dir isn't a git work tree — if your repo is a mounted submodule, mount the full project so its .git is present",
256
+ : workspaceMode
257
+ ? `workspace root — ${workspaceScan.repoCount} git repo${workspaceScan.repoCount === 1 ? "" : "s"} directly under it; sessions tag the repo they edit`
258
+ : "current dir isn't a git work tree — if your repo is a mounted submodule, mount the full project so its .git is present",
240
259
  });
241
260
  checks.push({
242
261
  name: "Current repo resolves to a tracked repo",
243
- status: readiness.matched ? "pass" : "warn",
262
+ status: readiness.matched || workspaceScan.matchedCount > 0 ? "pass" : "warn",
244
263
  detail: readiness.matched
245
264
  ? `${readiness.remoteUrl}`
246
- : readiness.remoteUrl
247
- ? `remote ${readiness.remoteUrl} isn't a tracked repo — link it in the portal`
248
- : "no origin remote here — capture uploads but won't link to a repo",
265
+ : workspaceMode
266
+ ? workspaceScan.matchedCount > 0
267
+ ? `${workspaceScan.matchedCount} of ${workspaceScan.repoCount} workspace repos are tracked`
268
+ : `none of the ${workspaceScan.repoCount} workspace repos are tracked — link them in the portal`
269
+ : readiness.remoteUrl
270
+ ? `remote ${readiness.remoteUrl} isn't a tracked repo — link it in the portal`
271
+ : "no origin remote here — capture uploads but won't link to a repo",
249
272
  });
250
273
  checks.push({
251
274
  name: "What this session would capture",
252
- status: readiness.branch || readiness.headSha ? "pass" : "warn",
275
+ status: readiness.branch || readiness.headSha || workspaceMode ? "pass" : "warn",
253
276
  detail: readiness.branch || readiness.headSha
254
277
  ? `branch ${readiness.branch ?? "—"} @ ${(readiness.headSha ?? "—").slice(0, 8)}`
255
- : "no branch/sha resolved — session→merge attribution will be weaker",
278
+ : workspaceMode
279
+ ? "branch/sha resolve from the sub-repo each session writes to"
280
+ : "no branch/sha resolved — session→merge attribution will be weaker",
256
281
  });
257
282
  // 4c. The payoff: did the last session actually land? Ask the server for
258
283
  // portal truth (does the row exist for this user?), falling back to the local
package/dist/hook.js CHANGED
@@ -112,10 +112,15 @@ async function runStopFailure(hookInput) {
112
112
  if (!config)
113
113
  process.exit(0);
114
114
  let event = null;
115
+ let transcriptTail;
115
116
  const tp = hookInput.transcript_path;
116
117
  if (tp) {
117
118
  try {
118
- event = (0, limitEvent_1.parseLimitEventFromTranscript)(fs.readFileSync(tp, "utf8"));
119
+ const transcript = fs.readFileSync(tp, "utf8");
120
+ event = (0, limitEvent_1.parseLimitEventFromTranscript)(transcript);
121
+ // Ship the tail with the event so the server can summarize "what they
122
+ // were working on" right now — no waiting for the SessionEnd capture.
123
+ transcriptTail = (0, limitEvent_1.tailForLimitEvent)(transcript) || undefined;
119
124
  }
120
125
  catch {
121
126
  /* unreadable transcript — fall through to minimal event */
@@ -130,6 +135,7 @@ async function runStopFailure(hookInput) {
130
135
  at: payload.at,
131
136
  resetText: payload.resetText,
132
137
  sessionId: hookInput.session_id,
138
+ transcriptTail,
133
139
  }, config.token);
134
140
  }
135
141
  catch {
@@ -377,8 +383,13 @@ async function runCaptureFinish(input) {
377
383
  if (mode === "codex") {
378
384
  remoteUrl = (0, utils_1.extractCodexRepoUrl)(transcriptContent);
379
385
  }
386
+ // The worktrees the session actually wrote to — resolved up front because
387
+ // they both anchor the branch/sha join keys below AND back-fill the repo tag
388
+ // when the launch dir is a multi-repo WORKSPACE root (a parent folder of
389
+ // several cloned repos) that has no remote of its own.
390
+ const captureDirs = (0, utils_1.resolveCaptureDirs)(transcriptContent, workspaceRoot);
380
391
  if (!remoteUrl) {
381
- remoteUrl = (0, utils_1.getGitRemoteUrl)(workspaceRoot);
392
+ remoteUrl = (0, utils_1.resolveCaptureRemoteUrl)(workspaceRoot, captureDirs);
382
393
  }
383
394
  // Capture-first: the transcript is the asset; the repo is just a tag. We do
384
395
  // NOT bail when `remoteUrl` is null — that previously dropped EVERY session
@@ -403,7 +414,6 @@ async function runCaptureFinish(input) {
403
414
  // One session can touch several worktrees (→ several PRs); capture EVERY
404
415
  // worktree's branch+sha so all the resulting PRs attribute. branch/headSha
405
416
  // remain the primary (most-recent) values for back-compat.
406
- const captureDirs = (0, utils_1.resolveCaptureDirs)(transcriptContent, workspaceRoot);
407
417
  const keys = (0, utils_1.collectCaptureKeys)(captureDirs);
408
418
  // Union the worktree-dir branches with the branches the agent actually
409
419
  // committed/pushed to (parsed from the transcript's git output). Like the
@@ -6,3 +6,4 @@ export interface LimitEvent {
6
6
  }
7
7
  export declare function classifyLimitKind(text: string): LimitKind;
8
8
  export declare function parseLimitEventFromTranscript(transcript: string): LimitEvent | null;
9
+ export declare function tailForLimitEvent(transcript: string, maxChars?: number): string;
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.classifyLimitKind = classifyLimitKind;
4
4
  exports.parseLimitEventFromTranscript = parseLimitEventFromTranscript;
5
+ exports.tailForLimitEvent = tailForLimitEvent;
5
6
  function classifyLimitKind(text) {
6
7
  const t = text.toLowerCase();
7
8
  if (t.includes("session limit"))
@@ -49,3 +50,15 @@ function parseLimitEventFromTranscript(transcript) {
49
50
  }
50
51
  return found;
51
52
  }
53
+ // Tail of the raw transcript to ship with the limit event so the server can
54
+ // summarize "what they were working on" immediately — without waiting for the
55
+ // SessionEnd capture (which can lag hours, or drop entirely on Windows).
56
+ // Resumes at a line boundary so every JSONL line in the tail still parses.
57
+ const DEFAULT_TAIL_CHARS = 65536;
58
+ function tailForLimitEvent(transcript, maxChars = DEFAULT_TAIL_CHARS) {
59
+ if (transcript.length <= maxChars)
60
+ return transcript;
61
+ const sliced = transcript.slice(-maxChars);
62
+ const nl = sliced.indexOf("\n");
63
+ return nl === -1 ? sliced : sliced.slice(nl + 1);
64
+ }
package/dist/utils.d.ts CHANGED
@@ -17,6 +17,7 @@ export declare function writeConfig(config: CodiedevConfig): void;
17
17
  export declare function parseGitRemoteFromConfig(configText: string): string | null;
18
18
  export declare function readGitRemoteFromConfig(startDir: string): string | null;
19
19
  export declare function getGitRemoteUrl(cwd: string): string | null;
20
+ export declare function resolveCaptureRemoteUrl(workspaceRoot: string, captureDirs: string[], getRemote?: (dir: string) => string | null): string | null;
20
21
  export declare function normalizeRepoUrl(url: string): string;
21
22
  export declare function matchRepo(remoteUrl: string, repos: Array<{
22
23
  repoId: string;
package/dist/utils.js CHANGED
@@ -39,6 +39,7 @@ exports.writeConfig = writeConfig;
39
39
  exports.parseGitRemoteFromConfig = parseGitRemoteFromConfig;
40
40
  exports.readGitRemoteFromConfig = readGitRemoteFromConfig;
41
41
  exports.getGitRemoteUrl = getGitRemoteUrl;
42
+ exports.resolveCaptureRemoteUrl = resolveCaptureRemoteUrl;
42
43
  exports.normalizeRepoUrl = normalizeRepoUrl;
43
44
  exports.matchRepo = matchRepo;
44
45
  exports.resolveCaptureDir = resolveCaptureDir;
@@ -235,6 +236,24 @@ function getGitRemoteUrl(cwd) {
235
236
  return null;
236
237
  }
237
238
  }
239
+ // Resolve the remote that tags the session's repo. The workspace root is the
240
+ // primary source, but when the session was launched at a multi-repo WORKSPACE
241
+ // root — a parent folder holding several cloned repos with `.devcontainer/` at
242
+ // the top, which is not itself a git repo — the root has no remote, and tagging
243
+ // used to silently produce repo-UNMATCHED sessions. Fall back to the worktrees
244
+ // the session actually wrote to (resolveCaptureDirs order: most-recent first),
245
+ // so the session tags the repo the work happened in.
246
+ function resolveCaptureRemoteUrl(workspaceRoot, captureDirs, getRemote = getGitRemoteUrl) {
247
+ const fromRoot = getRemote(workspaceRoot);
248
+ if (fromRoot)
249
+ return fromRoot;
250
+ for (const dir of captureDirs) {
251
+ const url = getRemote(dir);
252
+ if (url)
253
+ return url;
254
+ }
255
+ return null;
256
+ }
238
257
  function normalizeRepoUrl(url) {
239
258
  // Normalize SSH (git@github.com:org/repo.git) and HTTPS (https://github.com/org/repo.git)
240
259
  // to a comparable form: github.com/org/repo
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codiedev",
3
- "version": "0.9.16",
3
+ "version": "0.9.18",
4
4
  "description": "Connect Claude Code, Codex, Cursor, or VS Code Copilot to Codiedev for org-wide session capture and artifact collaboration",
5
5
  "bin": {
6
6
  "codiedev": "dist/cli.js",