codiedev 0.7.6 → 0.7.7

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.
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const vitest_1 = require("vitest");
4
+ const connectFlow_1 = require("../connectFlow");
5
+ const SAVED = {
6
+ token: "cdv_savedToken_xxxx",
7
+ companyId: "co_1",
8
+ companyName: "Sonifi",
9
+ repos: [],
10
+ lastSynced: new Date().toISOString(),
11
+ backendUrl: "https://example.test",
12
+ };
13
+ (0, vitest_1.describe)("resolveConnectToken", () => {
14
+ (0, vitest_1.it)("uses saved token when present and force=false", () => {
15
+ const res = (0, connectFlow_1.resolveConnectToken)({ savedConfig: SAVED, force: false });
16
+ (0, vitest_1.expect)(res).toEqual({ source: "reused", token: SAVED.token });
17
+ });
18
+ (0, vitest_1.it)("requires a prompt when no config exists", () => {
19
+ const res = (0, connectFlow_1.resolveConnectToken)({ savedConfig: null, force: false });
20
+ (0, vitest_1.expect)(res).toEqual({ source: "prompt-required" });
21
+ });
22
+ (0, vitest_1.it)("requires a prompt when --force is set, even if a saved token exists", () => {
23
+ const res = (0, connectFlow_1.resolveConnectToken)({ savedConfig: SAVED, force: true });
24
+ (0, vitest_1.expect)(res).toEqual({ source: "prompt-required" });
25
+ });
26
+ (0, vitest_1.it)("requires a prompt when saved config has empty token", () => {
27
+ const res = (0, connectFlow_1.resolveConnectToken)({
28
+ savedConfig: { ...SAVED, token: "" },
29
+ force: false,
30
+ });
31
+ (0, vitest_1.expect)(res).toEqual({ source: "prompt-required" });
32
+ });
33
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const vitest_1 = require("vitest");
4
+ const detection_1 = require("../detection");
5
+ (0, vitest_1.describe)("detectClaudeCode", () => {
6
+ (0, vitest_1.it)("returns false when neither ~/.claude nor `claude` binary present", () => {
7
+ (0, vitest_1.expect)((0, detection_1.detectClaudeCode)({ dirExists: false, binOnPath: false })).toBe(false);
8
+ });
9
+ (0, vitest_1.it)("returns true when only ~/.claude exists", () => {
10
+ (0, vitest_1.expect)((0, detection_1.detectClaudeCode)({ dirExists: true, binOnPath: false })).toBe(true);
11
+ });
12
+ (0, vitest_1.it)("returns true when only `claude` is on PATH (CC installed but never launched)", () => {
13
+ (0, vitest_1.expect)((0, detection_1.detectClaudeCode)({ dirExists: false, binOnPath: true })).toBe(true);
14
+ });
15
+ (0, vitest_1.it)("returns true when both are present", () => {
16
+ (0, vitest_1.expect)((0, detection_1.detectClaudeCode)({ dirExists: true, binOnPath: true })).toBe(true);
17
+ });
18
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const vitest_1 = require("vitest");
4
+ const prUrl_1 = require("../prUrl");
5
+ (0, vitest_1.describe)("parsePrOrMrUrl", () => {
6
+ (0, vitest_1.it)("parses a GitHub PR url", () => {
7
+ (0, vitest_1.expect)((0, prUrl_1.parsePrOrMrUrl)("https://github.com/foo/bar/pull/123")).toEqual({
8
+ provider: "github",
9
+ host: "github.com",
10
+ owner: "foo",
11
+ repo: "bar",
12
+ number: 123,
13
+ });
14
+ });
15
+ (0, vitest_1.it)("parses a GitLab.com MR url", () => {
16
+ (0, vitest_1.expect)((0, prUrl_1.parsePrOrMrUrl)("https://gitlab.com/sonifi/platform/-/merge_requests/42")).toEqual({
17
+ provider: "gitlab",
18
+ host: "gitlab.com",
19
+ owner: "sonifi",
20
+ repo: "platform",
21
+ number: 42,
22
+ });
23
+ });
24
+ (0, vitest_1.it)("parses a self-hosted GitLab MR url", () => {
25
+ (0, vitest_1.expect)((0, prUrl_1.parsePrOrMrUrl)("https://git.sonifi.com/internal/platform/-/merge_requests/7")).toEqual({
26
+ provider: "gitlab",
27
+ host: "git.sonifi.com",
28
+ owner: "internal",
29
+ repo: "platform",
30
+ number: 7,
31
+ });
32
+ });
33
+ (0, vitest_1.it)("parses a GitLab MR url with a nested group path", () => {
34
+ (0, vitest_1.expect)((0, prUrl_1.parsePrOrMrUrl)("https://gitlab.com/sonifi/team-a/platform/-/merge_requests/9")).toEqual({
35
+ provider: "gitlab",
36
+ host: "gitlab.com",
37
+ owner: "sonifi/team-a",
38
+ repo: "platform",
39
+ number: 9,
40
+ });
41
+ });
42
+ (0, vitest_1.it)("returns null for unrecognized URLs", () => {
43
+ (0, vitest_1.expect)((0, prUrl_1.parsePrOrMrUrl)("https://example.com/foo")).toBe(null);
44
+ (0, vitest_1.expect)((0, prUrl_1.parsePrOrMrUrl)("not a url")).toBe(null);
45
+ (0, vitest_1.expect)((0, prUrl_1.parsePrOrMrUrl)("")).toBe(null);
46
+ });
47
+ (0, vitest_1.it)("returns null when the number segment isn't an integer", () => {
48
+ (0, vitest_1.expect)((0, prUrl_1.parsePrOrMrUrl)("https://github.com/foo/bar/pull/abc")).toBe(null);
49
+ });
50
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const vitest_1 = require("vitest");
4
+ const repoResolver_1 = require("../repoResolver");
5
+ const ghRepo = { _id: "rep_gh1", owner: "Signal-and-Code", name: "vm-demo", gitProvider: "github" };
6
+ const glRepo = { _id: "rep_gl1", owner: "sonifi", name: "platform", gitProvider: "gitlab" };
7
+ (0, vitest_1.describe)("resolveRepoForCapture", () => {
8
+ (0, vitest_1.it)("returns the provided repoId when it matches a current repo", () => {
9
+ const res = (0, repoResolver_1.resolveRepoForCapture)({
10
+ remoteUrl: "https://github.com/Signal-and-Code/vm-demo.git",
11
+ providedRepoId: "rep_gh1",
12
+ companyRepos: [ghRepo],
13
+ });
14
+ (0, vitest_1.expect)(res).toEqual({ repoId: "rep_gh1", source: "provided" });
15
+ });
16
+ (0, vitest_1.it)("falls back to remoteUrl resolution when providedRepoId is stale", () => {
17
+ const res = (0, repoResolver_1.resolveRepoForCapture)({
18
+ remoteUrl: "https://github.com/Signal-and-Code/vm-demo.git",
19
+ providedRepoId: "rep_deleted",
20
+ companyRepos: [ghRepo],
21
+ });
22
+ (0, vitest_1.expect)(res).toEqual({ repoId: "rep_gh1", source: "resolved" });
23
+ });
24
+ (0, vitest_1.it)("resolves from remoteUrl when no providedRepoId is given (HTTPS form)", () => {
25
+ const res = (0, repoResolver_1.resolveRepoForCapture)({
26
+ remoteUrl: "https://github.com/Signal-and-Code/vm-demo.git",
27
+ providedRepoId: null,
28
+ companyRepos: [ghRepo],
29
+ });
30
+ (0, vitest_1.expect)(res).toEqual({ repoId: "rep_gh1", source: "resolved" });
31
+ });
32
+ (0, vitest_1.it)("resolves from remoteUrl when no providedRepoId is given (SSH form)", () => {
33
+ const res = (0, repoResolver_1.resolveRepoForCapture)({
34
+ remoteUrl: "git@github.com:Signal-and-Code/vm-demo.git",
35
+ providedRepoId: null,
36
+ companyRepos: [ghRepo],
37
+ });
38
+ (0, vitest_1.expect)(res).toEqual({ repoId: "rep_gh1", source: "resolved" });
39
+ });
40
+ (0, vitest_1.it)("resolves a GitLab remote against a gitlab-provider repo", () => {
41
+ const res = (0, repoResolver_1.resolveRepoForCapture)({
42
+ remoteUrl: "https://gitlab.com/sonifi/platform.git",
43
+ providedRepoId: null,
44
+ companyRepos: [ghRepo, glRepo],
45
+ });
46
+ (0, vitest_1.expect)(res).toEqual({ repoId: "rep_gl1", source: "resolved" });
47
+ });
48
+ (0, vitest_1.it)("resolves SSH-form GitLab URL", () => {
49
+ const res = (0, repoResolver_1.resolveRepoForCapture)({
50
+ remoteUrl: "git@gitlab.com:sonifi/platform.git",
51
+ providedRepoId: null,
52
+ companyRepos: [glRepo],
53
+ });
54
+ (0, vitest_1.expect)(res).toEqual({ repoId: "rep_gl1", source: "resolved" });
55
+ });
56
+ (0, vitest_1.it)("returns unmatched when remoteUrl matches no repo (don't drop the session — let backend decide)", () => {
57
+ const res = (0, repoResolver_1.resolveRepoForCapture)({
58
+ remoteUrl: "https://github.com/some/other-repo.git",
59
+ providedRepoId: null,
60
+ companyRepos: [ghRepo, glRepo],
61
+ });
62
+ (0, vitest_1.expect)(res).toEqual({ repoId: null, source: "unmatched" });
63
+ });
64
+ (0, vitest_1.it)("matches case-insensitively on owner/name (GitHub is case-insensitive on org/repo names)", () => {
65
+ const res = (0, repoResolver_1.resolveRepoForCapture)({
66
+ remoteUrl: "https://github.com/SIGNAL-AND-CODE/vm-demo.git",
67
+ providedRepoId: null,
68
+ companyRepos: [ghRepo],
69
+ });
70
+ (0, vitest_1.expect)(res).toEqual({ repoId: "rep_gh1", source: "resolved" });
71
+ });
72
+ });
package/dist/cli.js CHANGED
@@ -363,7 +363,7 @@ async function main() {
363
363
  try {
364
364
  switch (command) {
365
365
  case "connect":
366
- await (0, connect_1.runConnect)();
366
+ await (0, connect_1.runConnect)(rest);
367
367
  return;
368
368
  case "doctor":
369
369
  case "verify":
@@ -54,9 +54,6 @@ function symbol(status) {
54
54
  return "!";
55
55
  return "✗";
56
56
  }
57
- function claudeCodeInstalled() {
58
- return fs.existsSync(path.join(os.homedir(), ".claude"));
59
- }
60
57
  function codexInstalled() {
61
58
  return fs.existsSync(path.join(os.homedir(), ".codex"));
62
59
  }
@@ -142,6 +139,15 @@ function hasGhCli() {
142
139
  return false;
143
140
  }
144
141
  }
142
+ function hasGlabCli() {
143
+ try {
144
+ (0, child_process_1.execSync)("glab --version", { stdio: ["pipe", "pipe", "pipe"] });
145
+ return true;
146
+ }
147
+ catch {
148
+ return false;
149
+ }
150
+ }
145
151
  async function runDoctor(_args) {
146
152
  const checks = [];
147
153
  console.log("\nCodieDev doctor — checking your setup…\n");
@@ -217,7 +223,7 @@ async function runDoctor(_args) {
217
223
  : "0 — link repos in the portal so sessions get captured",
218
224
  });
219
225
  // 5. Claude Code setup (if present)
220
- if (claudeCodeInstalled()) {
226
+ if ((0, utils_1.claudeCodeInstalled)()) {
221
227
  checks.push({
222
228
  name: "Claude Code detected",
223
229
  status: "pass",
@@ -310,13 +316,21 @@ async function runDoctor(_args) {
310
316
  detail: "not installed on this machine",
311
317
  });
312
318
  }
313
- // 8. gh CLI (optional — only needed for reverse-ticket)
319
+ // 8. gh / glab CLIs (optional — only needed for reverse-ticket from a
320
+ // specific PR/MR URL; branch mode works without either).
314
321
  checks.push({
315
- name: "GitHub CLI (`gh`) for reverse-ticket command",
322
+ name: "GitHub CLI (`gh`) for reverse-ticket from a PR url",
316
323
  status: hasGhCli() ? "pass" : "warn",
317
324
  detail: hasGhCli()
318
325
  ? "installed"
319
- : "not installed — only needed if you use `codiedev reverse-ticket <pr-url>`",
326
+ : "not installed — only needed for `codiedev reverse-ticket <github-pr-url>`",
327
+ });
328
+ checks.push({
329
+ name: "GitLab CLI (`glab`) for reverse-ticket from an MR url",
330
+ status: hasGlabCli() ? "pass" : "warn",
331
+ detail: hasGlabCli()
332
+ ? "installed"
333
+ : "not installed — only needed for `codiedev reverse-ticket <gitlab-mr-url>`",
320
334
  });
321
335
  report(checks);
322
336
  const failures = checks.filter((c) => c.status === "fail");
@@ -3,15 +3,90 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.runReverseTicket = runReverseTicket;
4
4
  const child_process_1 = require("child_process");
5
5
  const shared_1 = require("./shared");
6
- function parsePrUrl(url) {
7
- const m = url.match(/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)/);
8
- if (!m)
9
- return null;
10
- return { owner: m[1], repo: m[2], number: Number(m[3]) };
6
+ const prUrl_1 = require("../prUrl");
7
+ async function fetchPrOrMrBundle(parsed, rawUrl, noTruncate) {
8
+ console.log(`Fetching ${parsed.provider === "gitlab" ? "MR" : "PR"} ${parsed.owner}/${parsed.repo}#${parsed.number}…`);
9
+ if (parsed.provider === "github") {
10
+ let prJson;
11
+ let filesJson;
12
+ let diff;
13
+ try {
14
+ prJson = ghApi(`/repos/${parsed.owner}/${parsed.repo}/pulls/${parsed.number}`);
15
+ filesJson = ghApi(`/repos/${parsed.owner}/${parsed.repo}/pulls/${parsed.number}/files`);
16
+ try {
17
+ const rawDiff = ghRaw(`/repos/${parsed.owner}/${parsed.repo}/pulls/${parsed.number}`);
18
+ diff = noTruncate ? rawDiff : rawDiff.slice(0, 12_000);
19
+ }
20
+ catch {
21
+ // diff is nice-to-have
22
+ }
23
+ }
24
+ catch (err) {
25
+ console.error(`Failed to fetch PR via gh api: ${err.message}`);
26
+ process.exit(1);
27
+ }
28
+ return {
29
+ repo: `${parsed.owner}/${parsed.repo}`,
30
+ number: parsed.number,
31
+ title: prJson.title ?? "",
32
+ body: prJson.body ?? "",
33
+ author: prJson.user?.login ?? "unknown",
34
+ authorEmail: prJson.user?.email ?? undefined,
35
+ mergedAt: prJson.merged_at
36
+ ? Date.parse(prJson.merged_at)
37
+ : prJson.closed_at
38
+ ? Date.parse(prJson.closed_at)
39
+ : Date.now(),
40
+ filesChanged: (filesJson ?? []).map((f) => f.filename),
41
+ diff,
42
+ htmlUrl: prJson.html_url ?? rawUrl,
43
+ };
44
+ }
45
+ // GitLab path. `glab api projects/<encoded-path>/...` returns the same
46
+ // MR shape as the REST API. We pull the diff from `merge_requests/<n>/changes`
47
+ // and reassemble a unified diff out of each file's `diff` blob.
48
+ const projectPath = `${parsed.owner}/${parsed.repo}`;
49
+ let mrJson;
50
+ let changesJson;
51
+ try {
52
+ mrJson = glabApi(projectPath, `merge_requests/${parsed.number}`);
53
+ changesJson = glabApi(projectPath, `merge_requests/${parsed.number}/changes`);
54
+ }
55
+ catch (err) {
56
+ console.error(`Failed to fetch MR via glab api: ${err.message}`);
57
+ process.exit(1);
58
+ }
59
+ const changes = (changesJson?.changes ?? []);
60
+ const filesChanged = changes
61
+ .map((c) => c.new_path ?? c.old_path)
62
+ .filter((p) => Boolean(p));
63
+ const rawDiff = changes
64
+ .map((c) => {
65
+ const header = `diff --git a/${c.old_path ?? c.new_path} b/${c.new_path ?? c.old_path}`;
66
+ return [header, c.diff ?? ""].join("\n");
67
+ })
68
+ .join("\n");
69
+ const diff = noTruncate ? rawDiff : rawDiff.slice(0, 12_000);
70
+ return {
71
+ repo: projectPath,
72
+ number: parsed.number,
73
+ title: mrJson.title ?? "",
74
+ body: mrJson.description ?? "",
75
+ author: mrJson.author?.username ?? "unknown",
76
+ authorEmail: undefined,
77
+ mergedAt: mrJson.merged_at
78
+ ? Date.parse(mrJson.merged_at)
79
+ : mrJson.closed_at
80
+ ? Date.parse(mrJson.closed_at)
81
+ : Date.now(),
82
+ filesChanged,
83
+ diff,
84
+ htmlUrl: mrJson.web_url ?? rawUrl,
85
+ };
11
86
  }
12
- function checkGhCli() {
87
+ function checkCli(bin) {
13
88
  try {
14
- (0, child_process_1.execSync)("gh --version", { stdio: ["pipe", "pipe", "pipe"] });
89
+ (0, child_process_1.execSync)(`${bin} --version`, { stdio: ["pipe", "pipe", "pipe"] });
15
90
  return true;
16
91
  }
17
92
  catch {
@@ -31,6 +106,17 @@ function ghRaw(path) {
31
106
  maxBuffer: 50 * 1024 * 1024,
32
107
  }).toString("utf8");
33
108
  }
109
+ // glab api takes a project path (URL-encoded) or numeric id and a sub-path.
110
+ // For self-hosted GitLab, glab respects `GITLAB_HOST` env if the user has
111
+ // configured glab against their internal instance.
112
+ function glabApi(projectPath, subpath) {
113
+ const encoded = encodeURIComponent(projectPath);
114
+ const out = (0, child_process_1.execSync)(`glab api "projects/${encoded}/${subpath}"`, {
115
+ stdio: ["pipe", "pipe", "pipe"],
116
+ maxBuffer: 50 * 1024 * 1024,
117
+ }).toString("utf8");
118
+ return JSON.parse(out);
119
+ }
34
120
  function parseArgs(args) {
35
121
  let prUrl;
36
122
  let base;
@@ -65,56 +151,31 @@ async function runReverseTicket(args) {
65
151
  if (!prUrl) {
66
152
  return runBranchMode({ explicitBase: base, forcedKey });
67
153
  }
68
- const parsed = parsePrUrl(prUrl);
154
+ const parsed = (0, prUrl_1.parsePrOrMrUrl)(prUrl);
69
155
  if (!parsed) {
70
- console.error("Couldn't parse PR URL. Expected format: https://github.com/<owner>/<repo>/pull/<number>");
156
+ console.error("Couldn't parse PR/MR URL. Expected formats:");
157
+ console.error(" https://github.com/<owner>/<repo>/pull/<number>");
158
+ console.error(" https://gitlab.com/<group>/<repo>/-/merge_requests/<number>");
159
+ console.error(" (self-hosted GitLab works too — same shape, your host)");
71
160
  console.error("");
72
161
  console.error("Run with no arguments for branch mode (uses your current local branch).");
73
162
  process.exit(1);
74
163
  }
75
- if (!checkGhCli()) {
76
- console.error("This command needs the `gh` CLI installed and authenticated.");
77
- console.error(" brew install gh (macOS)");
78
- console.error(" gh auth login");
79
- process.exit(1);
80
- }
81
- const config = (0, shared_1.requireConfig)();
82
- console.log(`Fetching PR ${parsed.owner}/${parsed.repo}#${parsed.number}…`);
83
- let prJson;
84
- let filesJson;
85
- let diff;
86
- try {
87
- prJson = ghApi(`/repos/${parsed.owner}/${parsed.repo}/pulls/${parsed.number}`);
88
- filesJson = ghApi(`/repos/${parsed.owner}/${parsed.repo}/pulls/${parsed.number}/files`);
89
- // Diff is nice-to-have; truncate if huge.
90
- try {
91
- const rawDiff = ghRaw(`/repos/${parsed.owner}/${parsed.repo}/pulls/${parsed.number}`);
92
- diff = noTruncate ? rawDiff : rawDiff.slice(0, 12_000);
164
+ const requiredCli = parsed.provider === "gitlab" ? "glab" : "gh";
165
+ if (!checkCli(requiredCli)) {
166
+ console.error(`This command needs the \`${requiredCli}\` CLI installed and authenticated.`);
167
+ if (requiredCli === "gh") {
168
+ console.error(" brew install gh (macOS)");
169
+ console.error(" gh auth login");
93
170
  }
94
- catch {
95
- // Skip diff on failure — the writer works without it.
171
+ else {
172
+ console.error(" brew install glab (macOS)");
173
+ console.error(" glab auth login");
96
174
  }
97
- }
98
- catch (err) {
99
- console.error(`Failed to fetch PR via gh api: ${err.message}`);
100
175
  process.exit(1);
101
176
  }
102
- const bundle = {
103
- repo: `${parsed.owner}/${parsed.repo}`,
104
- number: parsed.number,
105
- title: prJson.title ?? "",
106
- body: prJson.body ?? "",
107
- author: prJson.user?.login ?? "unknown",
108
- authorEmail: prJson.user?.email ?? undefined,
109
- mergedAt: prJson.merged_at
110
- ? Date.parse(prJson.merged_at)
111
- : prJson.closed_at
112
- ? Date.parse(prJson.closed_at)
113
- : Date.now(),
114
- filesChanged: (filesJson ?? []).map((f) => f.filename),
115
- diff,
116
- htmlUrl: prJson.html_url ?? prUrl,
117
- };
177
+ const config = (0, shared_1.requireConfig)();
178
+ const bundle = await fetchPrOrMrBundle(parsed, prUrl, noTruncate);
118
179
  console.log(` title: ${bundle.title}`);
119
180
  console.log(` author: ${bundle.author}`);
120
181
  console.log(` files: ${bundle.filesChanged.length}`);
package/dist/connect.d.ts CHANGED
@@ -1 +1 @@
1
- export declare function runConnect(): Promise<void>;
1
+ export declare function runConnect(args?: string[]): Promise<void>;
package/dist/connect.js CHANGED
@@ -38,6 +38,7 @@ const readline = __importStar(require("readline"));
38
38
  const https = __importStar(require("https"));
39
39
  const http = __importStar(require("http"));
40
40
  const utils_1 = require("./utils");
41
+ const connectFlow_1 = require("./connectFlow");
41
42
  const BACKEND_URL = process.env.CODIEDEV_URL || "https://judicious-falcon-861.convex.site";
42
43
  function prompt(rl, question) {
43
44
  return new Promise((resolve) => {
@@ -86,24 +87,38 @@ function postJson(url, body) {
86
87
  req.end();
87
88
  });
88
89
  }
89
- async function runConnect() {
90
- const rl = readline.createInterface({
91
- input: process.stdin,
92
- output: process.stdout,
93
- });
90
+ async function runConnect(args = []) {
91
+ // `--force` (or `-f`) re-prompts for a token even if `~/.codiedev/config.json`
92
+ // already has one. Default behavior reuses the saved token so users can
93
+ // re-run `connect` (e.g., after installing Claude Code) without having to
94
+ // mint a fresh token from the portal — which is what bit Sonifi's first
95
+ // engineer when he interpreted "no visible token" as "the old one is gone".
96
+ const force = args.includes("--force") || args.includes("-f");
94
97
  console.log("\nWelcome to CodieDev CLI\n");
95
98
  console.log("This will connect your coding agent to your CodieDev workspace and install");
96
99
  console.log("the session capture hook for org-wide session sharing.\n");
100
+ const saved = (0, utils_1.readConfig)();
101
+ const decision = (0, connectFlow_1.resolveConnectToken)({ savedConfig: saved, force });
97
102
  let token;
98
- try {
99
- token = await prompt(rl, "Enter your CodieDev API token: ");
100
- }
101
- finally {
102
- rl.close();
103
+ if (decision.source === "reused") {
104
+ token = decision.token;
105
+ console.log(`Reusing API token from ~/.codiedev/config.json (run with --force to use a new one).\n`);
103
106
  }
104
- if (!token) {
105
- console.error("Error: API token is required.");
106
- process.exit(1);
107
+ else {
108
+ const rl = readline.createInterface({
109
+ input: process.stdin,
110
+ output: process.stdout,
111
+ });
112
+ try {
113
+ token = await prompt(rl, "Enter your CodieDev API token: ");
114
+ }
115
+ finally {
116
+ rl.close();
117
+ }
118
+ if (!token) {
119
+ console.error("Error: API token is required.");
120
+ process.exit(1);
121
+ }
107
122
  }
108
123
  console.log("\nValidating token...");
109
124
  let responseData;
@@ -225,8 +240,9 @@ async function runConnect() {
225
240
  }
226
241
  }
227
242
  if (!hasClaude && !hasCodex && !hasCursor && !hasVSCodeCopilot) {
228
- console.warn("\nNo Claude Code (~/.claude), Codex (~/.codex), Cursor (~/.cursor), or VS Code Copilot install detected.");
229
- console.warn("Config saved. Install one, then re-run `npx codiedev connect` to wire up capture hooks.");
243
+ console.warn("\nNo Claude Code, Codex, Cursor, or VS Code Copilot install detected.");
244
+ console.warn("If you just installed Claude Code, launch it once (so ~/.claude is created),");
245
+ console.warn("then re-run `npx codiedev connect` to wire up the capture hook.");
230
246
  }
231
247
  console.log(`\nConnected to ${companyName}`);
232
248
  console.log(`Tracking ${repos.length} repo${repos.length === 1 ? "" : "s"}`);
@@ -0,0 +1,13 @@
1
+ import type { CodiedevConfig } from "./utils";
2
+ export type ConnectTokenSource = "reused" | "prompt-required";
3
+ export interface ConnectTokenInput {
4
+ savedConfig: CodiedevConfig | null;
5
+ force: boolean;
6
+ }
7
+ export type ConnectTokenResult = {
8
+ source: "reused";
9
+ token: string;
10
+ } | {
11
+ source: "prompt-required";
12
+ };
13
+ export declare function resolveConnectToken(input: ConnectTokenInput): ConnectTokenResult;
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ // Pure helper for `npx codiedev connect` — decides where the API token
3
+ // comes from. Splitting this out lets us TDD the policy (re-use vs. re-prompt)
4
+ // without touching readline or fs at all.
5
+ //
6
+ // Motivation: Jackson hit a confusing flow where he had a working token in
7
+ // `~/.codiedev/config.json`, re-ran `connect` (to pick up Claude Code that
8
+ // he'd just installed), and was forced to paste a token again. Since the
9
+ // portal only displays the plaintext on creation, his only option was to
10
+ // generate a new one — leaving him thinking the old one had been lost.
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.resolveConnectToken = resolveConnectToken;
13
+ function resolveConnectToken(input) {
14
+ if (input.force)
15
+ return { source: "prompt-required" };
16
+ if (!input.savedConfig)
17
+ return { source: "prompt-required" };
18
+ if (!input.savedConfig.token)
19
+ return { source: "prompt-required" };
20
+ return { source: "reused", token: input.savedConfig.token };
21
+ }
@@ -0,0 +1,12 @@
1
+ export interface ClaudeCodeProbe {
2
+ dirExists: boolean;
3
+ binOnPath: boolean;
4
+ }
5
+ /**
6
+ * Claude Code is "installed" if either ~/.claude exists (the user has
7
+ * launched the app at least once) OR the `claude` binary is on PATH (the
8
+ * user installed via npm/brew but hasn't launched yet). Either signal is
9
+ * sufficient — we want to wire up the hook the moment the user has
10
+ * obviously committed to the tool.
11
+ */
12
+ export declare function detectClaudeCode(probe: ClaudeCodeProbe): boolean;
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ // Pure detection helpers — runtime probes (fs / PATH lookup) live in utils.ts
3
+ // and feed these. Splitting them out makes the truth-table testable without
4
+ // mocking fs or process.
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.detectClaudeCode = detectClaudeCode;
7
+ /**
8
+ * Claude Code is "installed" if either ~/.claude exists (the user has
9
+ * launched the app at least once) OR the `claude` binary is on PATH (the
10
+ * user installed via npm/brew but hasn't launched yet). Either signal is
11
+ * sufficient — we want to wire up the hook the moment the user has
12
+ * obviously committed to the tool.
13
+ */
14
+ function detectClaudeCode(probe) {
15
+ return probe.dirExists || probe.binOnPath;
16
+ }
package/dist/hook.js CHANGED
@@ -133,13 +133,11 @@ async function main() {
133
133
  if (!remoteUrl) {
134
134
  process.exit(0);
135
135
  }
136
+ // Best-effort local match — backend re-resolves from `remoteUrl` against
137
+ // the company's current repo list, so a stale or empty local cache
138
+ // doesn't drop the session. (Pre-0.7.7 we exited 0 here, which silently
139
+ // dropped sessions for repos added after the user's last `connect`.)
136
140
  const matchedRepo = (0, utils_1.matchRepo)(remoteUrl, config.repos);
137
- if (!matchedRepo) {
138
- process.exit(0);
139
- }
140
- // Cursor's transcript format is undocumented — skip stats parsing for now
141
- // and let the backend handle it once we have a real session to inspect.
142
- // The transcript itself still uploads.
143
141
  const stats = mode === "codex"
144
142
  ? (0, utils_1.parseCodexStats)(transcriptContent)
145
143
  : mode === "cursor"
@@ -149,7 +147,7 @@ async function main() {
149
147
  const payload = {
150
148
  sessionId: session_id,
151
149
  repoUrl: remoteUrl,
152
- repoId: matchedRepo.repoId,
150
+ repoId: matchedRepo?.repoId,
153
151
  companyId: config.companyId,
154
152
  transcript: transcriptBase64,
155
153
  messageCount: stats.messageCount,
@@ -0,0 +1,9 @@
1
+ export type GitProvider = "github" | "gitlab";
2
+ export interface PrOrMrRef {
3
+ provider: GitProvider;
4
+ host: string;
5
+ owner: string;
6
+ repo: string;
7
+ number: number;
8
+ }
9
+ export declare function parsePrOrMrUrl(url: string): PrOrMrRef | null;
package/dist/prUrl.js ADDED
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ // PR/MR URL parser. GitHub uses `/pull/<n>`, GitLab uses `/-/merge_requests/<n>`.
3
+ // Self-hosted GitLab keeps the same path shape on a different host (Sonifi
4
+ // runs git.sonifi.com behind a Cloudflare tunnel, for example). Nested groups
5
+ // land in `owner` as a slash-joined path so we can rebuild the full project
6
+ // path when calling the GitLab API.
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.parsePrOrMrUrl = parsePrOrMrUrl;
9
+ function parsePrOrMrUrl(url) {
10
+ if (!url)
11
+ return null;
12
+ const gh = url.match(/^https?:\/\/([^/]+)\/([^/]+)\/([^/]+)\/pull\/(\d+)(?:[/?#]|$)/);
13
+ if (gh && /github/i.test(gh[1])) {
14
+ return {
15
+ provider: "github",
16
+ host: gh[1],
17
+ owner: gh[2],
18
+ repo: gh[3],
19
+ number: Number(gh[4]),
20
+ };
21
+ }
22
+ const gl = url.match(/^https?:\/\/([^/]+)\/(.+)\/([^/]+)\/-\/merge_requests\/(\d+)(?:[/?#]|$)/);
23
+ if (gl) {
24
+ return {
25
+ provider: "gitlab",
26
+ host: gl[1],
27
+ owner: gl[2],
28
+ repo: gl[3],
29
+ number: Number(gl[4]),
30
+ };
31
+ }
32
+ return null;
33
+ }
@@ -0,0 +1,17 @@
1
+ export type ResolveSource = "provided" | "resolved" | "unmatched";
2
+ export interface RepoLite {
3
+ _id: string;
4
+ owner: string;
5
+ name: string;
6
+ gitProvider?: "github" | "gitlab";
7
+ }
8
+ export interface ResolveInput {
9
+ remoteUrl: string;
10
+ providedRepoId: string | null;
11
+ companyRepos: ReadonlyArray<RepoLite>;
12
+ }
13
+ export interface ResolveResult {
14
+ repoId: string | null;
15
+ source: ResolveSource;
16
+ }
17
+ export declare function resolveRepoForCapture(input: ResolveInput): ResolveResult;
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ // Pure resolver — given a captured remoteUrl, an optional client-cached
3
+ // repoId, and the company's current repo list, decide which repoId (if any)
4
+ // the session belongs to. Used both inline by the CLI hook and (mirrored
5
+ // in convex/repoResolver.ts) by the /api/captureSession endpoint so that
6
+ // repos added after the user's last `connect` still get matched.
7
+ //
8
+ // "unmatched" is intentional — the backend will accept the session anyway
9
+ // and surface it for triage rather than silently dropping it.
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.resolveRepoForCapture = resolveRepoForCapture;
12
+ function parseRemoteUrl(url) {
13
+ let s = url.trim();
14
+ if (s.endsWith(".git"))
15
+ s = s.slice(0, -4);
16
+ const ssh = s.match(/^git@([^:]+):(.+)$/);
17
+ if (ssh) {
18
+ return { host: ssh[1].toLowerCase(), ownerName: ssh[2].toLowerCase() };
19
+ }
20
+ const https = s.match(/^https?:\/\/([^/]+)\/(.+)$/);
21
+ if (https) {
22
+ return { host: https[1].toLowerCase(), ownerName: https[2].toLowerCase() };
23
+ }
24
+ return null;
25
+ }
26
+ function resolveRepoForCapture(input) {
27
+ const { remoteUrl, providedRepoId, companyRepos } = input;
28
+ if (providedRepoId) {
29
+ const hit = companyRepos.find((r) => r._id === providedRepoId);
30
+ if (hit)
31
+ return { repoId: hit._id, source: "provided" };
32
+ }
33
+ const parsed = parseRemoteUrl(remoteUrl);
34
+ if (!parsed) {
35
+ return { repoId: null, source: "unmatched" };
36
+ }
37
+ const match = companyRepos.find((r) => `${r.owner}/${r.name}`.toLowerCase() === parsed.ownerName);
38
+ if (match)
39
+ return { repoId: match._id, source: "resolved" };
40
+ return { repoId: null, source: "unmatched" };
41
+ }
package/dist/utils.js CHANGED
@@ -60,6 +60,7 @@ const fs = __importStar(require("fs"));
60
60
  const path = __importStar(require("path"));
61
61
  const os = __importStar(require("os"));
62
62
  const child_process_1 = require("child_process");
63
+ const detection_1 = require("./detection");
63
64
  const CONFIG_DIR = path.join(os.homedir(), ".codiedev");
64
65
  const CONFIG_PATH = path.join(CONFIG_DIR, "config.json");
65
66
  function readConfig() {
@@ -190,8 +191,20 @@ function isCodiedevHookCommand(cmd) {
190
191
  return false;
191
192
  return cmd.includes("codiedev-hook") || /codiedev[\\/]dist[\\/]hook/.test(cmd);
192
193
  }
194
+ function claudeBinOnPath() {
195
+ try {
196
+ (0, child_process_1.execSync)("command -v claude", { stdio: ["pipe", "pipe", "pipe"] });
197
+ return true;
198
+ }
199
+ catch {
200
+ return false;
201
+ }
202
+ }
193
203
  function claudeCodeInstalled() {
194
- return fs.existsSync(CLAUDE_DIR);
204
+ return (0, detection_1.detectClaudeCode)({
205
+ dirExists: fs.existsSync(CLAUDE_DIR),
206
+ binOnPath: claudeBinOnPath(),
207
+ });
195
208
  }
196
209
  function codexInstalled() {
197
210
  return fs.existsSync(CODEX_DIR);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codiedev",
3
- "version": "0.7.6",
3
+ "version": "0.7.7",
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",