codiedev 0.4.0 → 0.5.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.
package/dist/cli.js CHANGED
@@ -41,8 +41,12 @@ Artifacts:
41
41
  Fetch an artifact (stdout by default)
42
42
  codiedev promote <artifact-id> Promote an auto-extracted artifact
43
43
  to an authored one
44
- codiedev reverse-ticket <pr-url> Generate a Jira ticket from a merged PR
45
- (auto-matches spec + thread + transcript)
44
+ codiedev reverse-ticket [<pr-url>] Generate a Jira ticket draft.
45
+ No URL = branch mode (uses your current
46
+ local git branch, no PR needed).
47
+ Draft lands in /portal/agentic-ticketing.
48
+ codiedev reverse-ticket --base <ref> Override the base ref in branch mode
49
+ (defaults to origin/main).
46
50
 
47
51
  Messaging:
48
52
  codiedev ping <user> "<msg>" [--with <key>]
@@ -58,15 +62,26 @@ Other:
58
62
  codiedev docs Print the full usage guide
59
63
  codiedev version Show version
60
64
 
61
- Filename conventions:
62
- spec-*.md → spec review-*.md review
63
- decision-*.md → decision proposal-*.md → proposal
64
- bugfix-*.md → bugfix anything else → note
65
+ The 90% workflow:
66
+ 1. Name your file 'spec-<topic>.md' and push.
67
+ 2. Ping a teammate to pull review.
68
+ 3. They edit, push back. Versions stack. Done.
69
+
70
+ Other artifact types (rare — use the matching prefix):
71
+ review-*.md → review decision-*.md → decision
72
+ proposal-*.md → proposal bugfix-*.md → bugfix
73
+ anything else → note
74
+
75
+ Skills (slash commands):
76
+ Add 'name:' + 'description:' YAML frontmatter to any pushed file
77
+ and it shows up as /<name> in Claude Code / Codex. Filename
78
+ prefix doesn't matter — frontmatter is the signal.
65
79
 
66
80
  Examples:
67
- codiedev push docs/specs/spec-cart-clear.md
81
+ codiedev push spec-cart-clear.md # the typical case
82
+ codiedev push portal-design.md # with name:/description: → skill
68
83
  codiedev pull spec-cart-clear.md
69
- codiedev ping maya "thoughts on batch vs parallel?" --with spec-cart-clear.md
84
+ codiedev ping maya "thoughts?" --with spec-cart-clear.md
70
85
  codiedev inbox --unread
71
86
  codiedev note "idempotency key is worth a follow-up"
72
87
 
@@ -92,17 +107,23 @@ codiedev connect — link your agent CLI to CodieDev
92
107
  push: `
93
108
  codiedev push — author or update an artifact
94
109
 
95
- Usage:
96
- codiedev push <file.md> [--type spec|review|decision|proposal|bugfix|note]
110
+ Default path:
111
+ codiedev push spec-<topic>.md
97
112
 
98
- The file's basename becomes its key (unique per company). Pushing the
99
- same key again creates a new version — full history is preserved.
113
+ The file's basename becomes its key (unique per company). Pushing
114
+ the same key again versions it — full history is preserved.
100
115
 
101
- Type is inferred from the filename prefix unless you override with --type.
116
+ Other types: rename with the matching prefix (review-*, decision-*,
117
+ proposal-*, bugfix-*) or pass --type. Anything else lands as 'note'.
118
+
119
+ Skills (slash commands): add 'name:' + 'description:' YAML
120
+ frontmatter to the top of the file. It shows up as /<name> in
121
+ the Skills tab. Filename prefix doesn't matter for the skill role.
102
122
 
103
123
  Examples:
104
- codiedev push spec-cart-clear.md
105
- codiedev push docs/review-auth.md --type review
124
+ codiedev push spec-cart-clear.md # the typical case
125
+ codiedev push portal-design.md # with name:/description: → skill
126
+ codiedev push docs/review-auth.md
106
127
  codiedev push notes/random.md --type note
107
128
  `.trim(),
108
129
  pull: `
@@ -240,6 +261,25 @@ Artifacts have a status that auto-advances as PRs reference them:
240
261
  Artifacts stay editable at every stage, including after merge —
241
262
  lessons-learned and follow-up notes append as new versions.
242
263
 
264
+ ## Skills
265
+
266
+ Default path:
267
+
268
+ 1. Pick a name: e.g. portal-design
269
+ 2. Top the file with frontmatter:
270
+ ---
271
+ name: portal-design
272
+ description: Use when reviewing or building portal UI.
273
+ ---
274
+ # body...
275
+ 3. codiedev push portal-design.md
276
+
277
+ It's now /portal-design in Claude Code / Codex.
278
+
279
+ Filename prefix is irrelevant for skills — frontmatter is the only
280
+ signal. The stored artifact type still follows the filename rule
281
+ (so a skill in a spec file is both a 'spec' and a callable skill).
282
+
243
283
  ## Portal
244
284
 
245
285
  Everything you do in the CLI is visible at:
@@ -33,6 +33,7 @@ function ghRaw(path) {
33
33
  }
34
34
  function parseArgs(args) {
35
35
  let prUrl;
36
+ let base;
36
37
  let forcedKey;
37
38
  let noTruncate = false;
38
39
  for (let i = 0; i < args.length; i++) {
@@ -43,6 +44,12 @@ function parseArgs(args) {
43
44
  else if (a.startsWith("--with=")) {
44
45
  forcedKey = a.slice("--with=".length);
45
46
  }
47
+ else if ((a === "--base" || a === "-b") && i + 1 < args.length) {
48
+ base = args[++i];
49
+ }
50
+ else if (a.startsWith("--base=")) {
51
+ base = a.slice("--base=".length);
52
+ }
46
53
  else if (a === "--full") {
47
54
  noTruncate = true;
48
55
  }
@@ -50,19 +57,19 @@ function parseArgs(args) {
50
57
  prUrl = a;
51
58
  }
52
59
  }
53
- if (!prUrl) {
54
- console.error("Usage: codiedev reverse-ticket <pr-url> [--with <artifact-key>] [--full]");
55
- console.error("");
56
- console.error("Example: codiedev reverse-ticket https://github.com/signalandcode/repo/pull/42");
57
- process.exit(1);
58
- }
59
- return { prUrl, forcedKey, noTruncate };
60
+ return { prUrl, base, forcedKey, noTruncate };
60
61
  }
61
62
  async function runReverseTicket(args) {
62
- const { prUrl, forcedKey, noTruncate } = parseArgs(args);
63
+ const { prUrl, base, forcedKey, noTruncate } = parseArgs(args);
64
+ // No PR URL → branch mode (mid-session, no PR exists yet).
65
+ if (!prUrl) {
66
+ return runBranchMode({ explicitBase: base, forcedKey });
67
+ }
63
68
  const parsed = parsePrUrl(prUrl);
64
69
  if (!parsed) {
65
70
  console.error("Couldn't parse PR URL. Expected format: https://github.com/<owner>/<repo>/pull/<number>");
71
+ console.error("");
72
+ console.error("Run with no arguments for branch mode (uses your current local branch).");
66
73
  process.exit(1);
67
74
  }
68
75
  if (!checkGhCli()) {
@@ -148,3 +155,135 @@ async function runReverseTicket(args) {
148
155
  process.exit(1);
149
156
  }
150
157
  }
158
+ function git(cmd) {
159
+ try {
160
+ return (0, child_process_1.execSync)(`git ${cmd}`, {
161
+ stdio: ["pipe", "pipe", "pipe"],
162
+ maxBuffer: 32 * 1024 * 1024,
163
+ })
164
+ .toString("utf8")
165
+ .trim();
166
+ }
167
+ catch (err) {
168
+ throw new Error(`git ${cmd} failed: ${err.message}`);
169
+ }
170
+ }
171
+ function safeGit(fn) {
172
+ try {
173
+ return fn() || undefined;
174
+ }
175
+ catch {
176
+ return undefined;
177
+ }
178
+ }
179
+ async function runBranchMode(opts) {
180
+ // Confirm we're in a git repo.
181
+ try {
182
+ git("rev-parse --is-inside-work-tree");
183
+ }
184
+ catch {
185
+ console.error("Not inside a git repository. Run this from a checked-out repo.");
186
+ process.exit(1);
187
+ }
188
+ // Resolve base ref — explicit, then origin/main, then origin/master.
189
+ const candidates = opts.explicitBase
190
+ ? [opts.explicitBase]
191
+ : ["origin/main", "origin/master"];
192
+ let baseRef = null;
193
+ for (const c of candidates) {
194
+ try {
195
+ git(`rev-parse --verify ${c}`);
196
+ baseRef = c;
197
+ break;
198
+ }
199
+ catch {
200
+ // try next
201
+ }
202
+ }
203
+ if (!baseRef) {
204
+ console.error(`Couldn't find a base ref (tried ${candidates.join(", ")}). Pass --base <ref>.`);
205
+ process.exit(1);
206
+ }
207
+ const branch = git("rev-parse --abbrev-ref HEAD");
208
+ if (branch === "HEAD") {
209
+ console.error("Detached HEAD — checkout a branch first.");
210
+ process.exit(1);
211
+ }
212
+ const baseSha = git(`merge-base HEAD ${baseRef}`);
213
+ const headSha = git("rev-parse HEAD");
214
+ if (baseSha === headSha) {
215
+ console.error(`No commits on ${branch} ahead of ${baseRef} — make a commit first.`);
216
+ process.exit(1);
217
+ }
218
+ const filesRaw = git(`diff --name-only ${baseSha}...HEAD`);
219
+ const filesChanged = filesRaw ? filesRaw.split("\n").filter(Boolean) : [];
220
+ if (filesChanged.length === 0) {
221
+ console.error(`No files changed between ${baseRef} and ${branch} — nothing to write up.`);
222
+ process.exit(1);
223
+ }
224
+ let diff = "";
225
+ try {
226
+ diff = git(`diff ${baseSha}...HEAD`).slice(0, 12_000);
227
+ }
228
+ catch {
229
+ // best effort
230
+ }
231
+ let repo;
232
+ try {
233
+ const remoteUrl = git("config --get remote.origin.url");
234
+ const m = remoteUrl.match(/[:/]([^/:]+)\/([^/:]+?)(?:\.git)?$/);
235
+ if (!m) {
236
+ throw new Error(`Couldn't parse owner/repo from remote: ${remoteUrl}`);
237
+ }
238
+ repo = `${m[1]}/${m[2]}`;
239
+ }
240
+ catch (err) {
241
+ console.error(`Couldn't read git remote.origin.url: ${err.message}`);
242
+ process.exit(1);
243
+ }
244
+ const title = safeGit(() => git("log -1 --format=%s")) ?? `WIP: ${branch}`;
245
+ const author = safeGit(() => git("config --get user.name"));
246
+ const authorEmail = safeGit(() => git("config --get user.email"));
247
+ console.log(`Branch: ${branch} (${repo})`);
248
+ console.log(`Base: ${baseRef} @ ${baseSha.slice(0, 12)}`);
249
+ console.log(`Head: ${headSha.slice(0, 12)}`);
250
+ console.log(`Files: ${filesChanged.length}`);
251
+ console.log("");
252
+ console.log("Generating ticket draft…");
253
+ console.log("");
254
+ const config = (0, shared_1.requireConfig)();
255
+ try {
256
+ const res = await (0, shared_1.apiRequest)("POST", "/api/cli/reverseTicketFromBranch", {
257
+ config,
258
+ body: {
259
+ branch: {
260
+ repo,
261
+ branch,
262
+ baseSha,
263
+ headSha,
264
+ title,
265
+ author,
266
+ authorEmail,
267
+ filesChanged,
268
+ diff: diff || undefined,
269
+ },
270
+ forcedArtifactKey: opts.forcedKey,
271
+ },
272
+ });
273
+ if (!res.ok || !res.artifactId) {
274
+ console.error("Reverse-ticket generation returned no result. The writer may have skipped due to missing context.");
275
+ process.exit(1);
276
+ }
277
+ const verb = res.action === "overwritten" ? "Updated" : "Created";
278
+ console.log(`✓ ${verb} reverse-ticket draft.`);
279
+ console.log(` artifact: ${res.artifactKey ?? res.artifactId}`);
280
+ console.log(` portal: ${res.portalUrl}`);
281
+ console.log("");
282
+ console.log("Open in your portal to review, edit, and push to Jira.");
283
+ console.log("When the PR opens later, the webhook dedupes against this draft — one ticket, not two.");
284
+ }
285
+ catch (err) {
286
+ console.error(`Reverse-ticket failed: ${err.message}`);
287
+ process.exit(1);
288
+ }
289
+ }
package/dist/mcp.js CHANGED
@@ -52,7 +52,7 @@ const path = __importStar(require("path"));
52
52
  const utils_1 = require("./utils");
53
53
  const shared_1 = require("./commands/shared");
54
54
  const PKG_NAME = "codiedev";
55
- const PKG_VERSION = "0.4.0";
55
+ const PKG_VERSION = "0.5.0";
56
56
  // ─────────────────────────────────────────────────────────────────────────────
57
57
  // Tool definitions — descriptions tuned so Claude/Codex resolve natural-language
58
58
  // requests into the right tool without manual steering.
@@ -372,6 +372,41 @@ const TOOLS = [
372
372
  },
373
373
  },
374
374
  },
375
+ {
376
+ name: "codiedev_reverse_ticket",
377
+ description: "Generate a Jira-ready reverse-ticket draft from the work the user has " +
378
+ "done in their CURRENT git branch — mid-session, before any PR exists. " +
379
+ "Reads local git state (current branch, divergence from main, diff, " +
380
+ "commits, files changed) and runs it through the codiedev reverse-" +
381
+ "ticket writer. The draft lands in the user's portal at " +
382
+ "/portal/agentic-ticketing where they can edit the title, description, " +
383
+ "and scope, then push to Jira. Use when the user asks to 'create a " +
384
+ "reverse ticket', 'draft a Jira ticket from this work', 'write up what " +
385
+ "I've done in this branch', or runs `codiedev reverse-ticket` directly. " +
386
+ "The webhook on PR-open later dedupes against this draft so the user " +
387
+ "never gets a double ticket. The draft is intentionally not auto-pushed " +
388
+ "to Jira — trust comes from the user reviewing in the portal first.",
389
+ inputSchema: {
390
+ type: "object",
391
+ properties: {
392
+ title: {
393
+ type: "string",
394
+ description: "Optional working title for the ticket. Defaults to the most " +
395
+ "recent commit subject on the branch.",
396
+ },
397
+ base: {
398
+ type: "string",
399
+ description: "Optional base ref to diff against. Defaults to 'origin/main', " +
400
+ "falls back to 'origin/master'.",
401
+ },
402
+ forcedArtifactKey: {
403
+ type: "string",
404
+ description: "Optional — force the writer to link to a specific artifact key " +
405
+ "for context (e.g. 'spec-auth.md').",
406
+ },
407
+ },
408
+ },
409
+ },
375
410
  ];
376
411
  // ─────────────────────────────────────────────────────────────────────────────
377
412
  // Server
@@ -380,6 +415,26 @@ const server = new index_js_1.Server({ name: PKG_NAME, version: PKG_VERSION }, {
380
415
  server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({
381
416
  tools: TOOLS,
382
417
  }));
418
+ /**
419
+ * Fire-and-forget telemetry — POST one event per tool invocation. Never blocks,
420
+ * never throws into the user-facing path. Drop silently on any failure.
421
+ */
422
+ function emitTelemetry(config, event) {
423
+ if (!config)
424
+ return;
425
+ (0, shared_1.apiRequest)("POST", "/api/telemetry/mcp-event", {
426
+ config,
427
+ body: {
428
+ tool: event.tool,
429
+ ok: event.ok,
430
+ latencyMs: event.latencyMs,
431
+ error: event.error,
432
+ mcpVersion: PKG_VERSION,
433
+ },
434
+ }).catch(() => {
435
+ // Swallow — telemetry should never affect the user's session.
436
+ });
437
+ }
383
438
  server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
384
439
  const config = (0, utils_1.readConfig)();
385
440
  if (!config) {
@@ -397,40 +452,26 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
397
452
  }
398
453
  const { name, arguments: argsRaw } = request.params;
399
454
  const args = (argsRaw ?? {});
455
+ const start = Date.now();
400
456
  try {
401
- switch (name) {
402
- case "codiedev_push":
403
- return await handlePush(args, config);
404
- case "codiedev_pull":
405
- return await handlePull(args, config);
406
- case "codiedev_ping":
407
- return await handlePing(args, config);
408
- case "codiedev_inbox":
409
- return await handleInbox(args, config);
410
- case "codiedev_note":
411
- return await handleNote(args, config);
412
- case "codiedev_promote":
413
- return await handlePromote(args, config);
414
- case "codiedev_post_to_feed":
415
- return await handlePostToFeed(args, config);
416
- case "codiedev_share_with":
417
- return await handleShareWith(args, config);
418
- case "codiedev_send_to":
419
- return await handleSendTo(args, config);
420
- case "codiedev_react":
421
- return await handleReact(args, config);
422
- case "codiedev_search":
423
- return await handleSearch(args, config);
424
- case "codiedev_get_library":
425
- return await handleGetLibrary(args, config);
426
- default:
427
- return {
428
- isError: true,
429
- content: [{ type: "text", text: `Unknown tool: ${name}` }],
430
- };
431
- }
457
+ const result = await dispatchTool(name, args, config);
458
+ emitTelemetry(config, {
459
+ tool: name,
460
+ ok: !result?.isError,
461
+ latencyMs: Date.now() - start,
462
+ error: result?.isError
463
+ ? (result?.content?.[0]?.text ?? "").slice(0, 200) || "unknown error"
464
+ : undefined,
465
+ });
466
+ return result;
432
467
  }
433
468
  catch (err) {
469
+ emitTelemetry(config, {
470
+ tool: name,
471
+ ok: false,
472
+ latencyMs: Date.now() - start,
473
+ error: (err.message ?? "unknown").slice(0, 200),
474
+ });
434
475
  return {
435
476
  isError: true,
436
477
  content: [
@@ -442,6 +483,41 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
442
483
  };
443
484
  }
444
485
  });
486
+ async function dispatchTool(name, args, config) {
487
+ switch (name) {
488
+ case "codiedev_push":
489
+ return await handlePush(args, config);
490
+ case "codiedev_pull":
491
+ return await handlePull(args, config);
492
+ case "codiedev_ping":
493
+ return await handlePing(args, config);
494
+ case "codiedev_inbox":
495
+ return await handleInbox(args, config);
496
+ case "codiedev_note":
497
+ return await handleNote(args, config);
498
+ case "codiedev_promote":
499
+ return await handlePromote(args, config);
500
+ case "codiedev_post_to_feed":
501
+ return await handlePostToFeed(args, config);
502
+ case "codiedev_share_with":
503
+ return await handleShareWith(args, config);
504
+ case "codiedev_send_to":
505
+ return await handleSendTo(args, config);
506
+ case "codiedev_react":
507
+ return await handleReact(args, config);
508
+ case "codiedev_search":
509
+ return await handleSearch(args, config);
510
+ case "codiedev_get_library":
511
+ return await handleGetLibrary(args, config);
512
+ case "codiedev_reverse_ticket":
513
+ return await handleReverseTicket(args, config);
514
+ default:
515
+ return {
516
+ isError: true,
517
+ content: [{ type: "text", text: `Unknown tool: ${name}` }],
518
+ };
519
+ }
520
+ }
445
521
  async function handlePush(args, config) {
446
522
  const filename = asString(args.filename);
447
523
  const markdown = asString(args.markdown);
@@ -763,6 +839,153 @@ function formatAmbiguousRecipient(err, to) {
763
839
  }
764
840
  throw err;
765
841
  }
842
+ async function handleReverseTicket(args, config) {
843
+ const explicitTitle = asStringOrUndefined(args.title);
844
+ const explicitBase = asStringOrUndefined(args.base);
845
+ const forcedKey = asStringOrUndefined(args.forcedArtifactKey);
846
+ // Helper that runs git from cwd with a hard-fail on error.
847
+ const { execSync } = await Promise.resolve().then(() => __importStar(require("child_process")));
848
+ const git = (cmd) => {
849
+ try {
850
+ return execSync(`git ${cmd}`, {
851
+ stdio: ["pipe", "pipe", "pipe"],
852
+ maxBuffer: 32 * 1024 * 1024,
853
+ })
854
+ .toString("utf8")
855
+ .trim();
856
+ }
857
+ catch (err) {
858
+ throw new Error(`git ${cmd} failed: ${err.message}`);
859
+ }
860
+ };
861
+ // Confirm we're in a git repo.
862
+ try {
863
+ git("rev-parse --is-inside-work-tree");
864
+ }
865
+ catch {
866
+ throw new Error("Not inside a git repository. Run this from a checked-out repo.");
867
+ }
868
+ // Resolve base ref — explicit, then origin/main, then origin/master.
869
+ const candidates = explicitBase
870
+ ? [explicitBase]
871
+ : ["origin/main", "origin/master"];
872
+ let baseRef = null;
873
+ for (const c of candidates) {
874
+ try {
875
+ git(`rev-parse --verify ${c}`);
876
+ baseRef = c;
877
+ break;
878
+ }
879
+ catch {
880
+ // try next
881
+ }
882
+ }
883
+ if (!baseRef) {
884
+ throw new Error(`Couldn't find a base ref (tried ${candidates.join(", ")}). ` +
885
+ `Pass an explicit base via the 'base' argument.`);
886
+ }
887
+ const branch = git("rev-parse --abbrev-ref HEAD");
888
+ if (branch === "HEAD") {
889
+ throw new Error("Detached HEAD — checkout a branch first.");
890
+ }
891
+ const baseSha = git(`merge-base HEAD ${baseRef}`);
892
+ const headSha = git("rev-parse HEAD");
893
+ if (baseSha === headSha) {
894
+ throw new Error(`No commits on ${branch} ahead of ${baseRef} — make a commit first.`);
895
+ }
896
+ const filesRaw = git(`diff --name-only ${baseSha}...HEAD`);
897
+ const filesChanged = filesRaw ? filesRaw.split("\n").filter(Boolean) : [];
898
+ if (filesChanged.length === 0) {
899
+ throw new Error(`No files changed between ${baseRef} and ${branch} — nothing to write up.`);
900
+ }
901
+ // Truncated diff (12k chars) — writer works without it but it sharpens output.
902
+ let diff = "";
903
+ try {
904
+ diff = git(`diff ${baseSha}...HEAD`).slice(0, 12_000);
905
+ }
906
+ catch {
907
+ // Best effort.
908
+ }
909
+ // Repo name from origin remote.
910
+ let repo;
911
+ try {
912
+ const remoteUrl = git("config --get remote.origin.url");
913
+ const m = remoteUrl.match(/[:/]([^/:]+)\/([^/:]+?)(?:\.git)?$/);
914
+ if (!m) {
915
+ throw new Error(`Couldn't parse owner/repo from remote: ${remoteUrl}`);
916
+ }
917
+ repo = `${m[1]}/${m[2]}`;
918
+ }
919
+ catch (err) {
920
+ throw new Error(`Couldn't read git remote.origin.url: ${err.message}`);
921
+ }
922
+ const title = explicitTitle ?? safeGit(() => git("log -1 --format=%s")) ?? `WIP: ${branch}`;
923
+ const author = safeGit(() => git("config --get user.name"));
924
+ const authorEmail = safeGit(() => git("config --get user.email"));
925
+ // Post to backend.
926
+ const res = await (0, shared_1.apiRequest)("POST", "/api/cli/reverseTicketFromBranch", {
927
+ config,
928
+ body: {
929
+ branch: {
930
+ repo,
931
+ branch,
932
+ baseSha,
933
+ headSha,
934
+ title,
935
+ author,
936
+ authorEmail,
937
+ filesChanged,
938
+ diff: diff || undefined,
939
+ },
940
+ forcedArtifactKey: forcedKey,
941
+ },
942
+ });
943
+ if (!res.ok || !res.artifactId) {
944
+ return {
945
+ isError: true,
946
+ content: [
947
+ {
948
+ type: "text",
949
+ text: "Reverse-ticket generation returned no result. The writer may have " +
950
+ "skipped due to missing context (no captured sessions, etc.). Try " +
951
+ "again after working on the branch a bit more, or check the " +
952
+ "server logs.",
953
+ },
954
+ ],
955
+ };
956
+ }
957
+ const portalBase = (config.backendUrl || "")
958
+ .replace(/\/$/, "")
959
+ .replace(/\.convex\.cloud$/, ".convex.cloud"); // backendUrl is the Convex site; portal is separate
960
+ // Best effort — surface the portal path; the user can prefix with their portal host.
961
+ const portalPath = res.portalUrl;
962
+ const verb = res.action === "overwritten" ? "Updated" : "Created";
963
+ return {
964
+ content: [
965
+ {
966
+ type: "text",
967
+ text: `✓ ${verb} reverse-ticket draft for branch \`${branch}\` (${repo}).\n` +
968
+ ` files changed: ${filesChanged.length}\n` +
969
+ ` base: ${baseSha.slice(0, 12)} → head: ${headSha.slice(0, 12)} (${baseRef})\n` +
970
+ ` diff size: ${diff.length} chars\n` +
971
+ ` artifact: ${res.artifactKey ?? res.artifactId}\n\n` +
972
+ `Open in your portal to review, edit, and push to Jira:\n` +
973
+ ` ${portalPath}\n\n` +
974
+ `When the PR opens later, the webhook dedupes against this draft — ` +
975
+ `you'll get one ticket, not two.`,
976
+ },
977
+ ],
978
+ };
979
+ }
980
+ // Run a git invocation; return undefined instead of throwing.
981
+ function safeGit(fn) {
982
+ try {
983
+ return fn();
984
+ }
985
+ catch {
986
+ return undefined;
987
+ }
988
+ }
766
989
  // ─────────────────────────────────────────────────────────────────────────────
767
990
  // Helpers
768
991
  // ─────────────────────────────────────────────────────────────────────────────
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codiedev",
3
- "version": "0.4.0",
3
+ "version": "0.5.1",
4
4
  "description": "Connect Claude Code or Codex to CodieDev for org-wide session capture and artifact collaboration",
5
5
  "bin": {
6
6
  "codiedev": "./dist/cli.js",