codiedev 0.9.64 → 0.9.66

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/mcp.js CHANGED
@@ -237,29 +237,6 @@ const TOOLS = [
237
237
  required: ["body"],
238
238
  },
239
239
  },
240
- {
241
- name: "codiedev_promote",
242
- description: "Promote an auto-extracted Codiedev artifact (spec/bugfix/decision " +
243
- "produced by the session-capture pipeline) to an authored artifact " +
244
- "so teammates can collaborate on it. Use when the user says 'publish " +
245
- "the extracted spec', 'promote this', or 'make the auto-extracted X " +
246
- "shareable'.",
247
- inputSchema: {
248
- type: "object",
249
- properties: {
250
- artifactId: {
251
- type: "string",
252
- description: "The extracted artifact's id from Codiedev.",
253
- },
254
- keyOverride: {
255
- type: "string",
256
- description: "Optional filename key for the promoted artifact. If omitted, " +
257
- "one is generated from the artifact's type + session id.",
258
- },
259
- },
260
- required: ["artifactId"],
261
- },
262
- },
263
240
  {
264
241
  name: "codiedev_share_with",
265
242
  description: "Grant a teammate persistent read (or edit) access to an artifact " +
@@ -306,27 +283,6 @@ const TOOLS = [
306
283
  required: ["filename", "to"],
307
284
  },
308
285
  },
309
- {
310
- name: "codiedev_react",
311
- description: "React to a feed post with an emoji. Use when the user says 'react " +
312
- "to that post with X', 'mark as used', or 'upvote'. The 🛠 " +
313
- "reaction is the signal for 'I used this' and bumps the linked " +
314
- "artifact's reuse count. Pass the post id (get it from " +
315
- "codiedev_get_library or a previous feed response).",
316
- inputSchema: {
317
- type: "object",
318
- properties: {
319
- postId: { type: "string", description: "Feed post id." },
320
- emoji: {
321
- type: "string",
322
- enum: ["👍", "🔥", "💡", "🛠", "👑"],
323
- description: "Reaction emoji. 🛠 = 'I used this' (counts heaviest toward " +
324
- "the author's score and the artifact's reuse count).",
325
- },
326
- },
327
- required: ["postId", "emoji"],
328
- },
329
- },
330
286
  {
331
287
  name: "codiedev_search",
332
288
  description: "Search the team's artifact library by natural-language query. Use " +
@@ -413,41 +369,6 @@ const TOOLS = [
413
369
  },
414
370
  },
415
371
  },
416
- {
417
- name: "codiedev_reverse_ticket",
418
- description: "Generate a Jira-ready reverse-ticket draft from the work the user has " +
419
- "done in their CURRENT git branch — mid-session, before any PR exists. " +
420
- "Reads local git state (current branch, divergence from main, diff, " +
421
- "commits, files changed) and runs it through the codiedev reverse-" +
422
- "ticket writer. The draft lands in the user's portal at " +
423
- "/portal/agentic-ticketing where they can edit the title, description, " +
424
- "and scope, then push to Jira. Use when the user asks to 'create a " +
425
- "reverse ticket', 'draft a Jira ticket from this work', 'write up what " +
426
- "I've done in this branch', or runs `codiedev reverse-ticket` directly. " +
427
- "The webhook on PR-open later dedupes against this draft so the user " +
428
- "never gets a double ticket. The draft is intentionally not auto-pushed " +
429
- "to Jira — trust comes from the user reviewing in the portal first.",
430
- inputSchema: {
431
- type: "object",
432
- properties: {
433
- title: {
434
- type: "string",
435
- description: "Optional working title for the ticket. Defaults to the most " +
436
- "recent commit subject on the branch.",
437
- },
438
- base: {
439
- type: "string",
440
- description: "Optional base ref to diff against. Defaults to 'origin/main', " +
441
- "falls back to 'origin/master'.",
442
- },
443
- forcedArtifactKey: {
444
- type: "string",
445
- description: "Optional — force the writer to link to a specific artifact key " +
446
- "for context (e.g. 'spec-auth.md').",
447
- },
448
- },
449
- },
450
- },
451
372
  ];
452
373
  // ─────────────────────────────────────────────────────────────────────────────
453
374
  // Server
@@ -538,22 +459,16 @@ async function dispatchTool(name, args, config) {
538
459
  return await handleInbox(args, config);
539
460
  case "codiedev_note":
540
461
  return await handleNote(args, config);
541
- case "codiedev_promote":
542
- return await handlePromote(args, config);
543
462
  case "codiedev_share_with":
544
463
  return await handleShareWith(args, config);
545
464
  case "codiedev_send_to":
546
465
  return await handleSendTo(args, config);
547
- case "codiedev_react":
548
- return await handleReact(args, config);
549
466
  case "codiedev_ask":
550
467
  return await handleAsk(args, config);
551
468
  case "codiedev_search":
552
469
  return await handleSearch(args, config);
553
470
  case "codiedev_get_library":
554
471
  return await handleGetLibrary(args, config);
555
- case "codiedev_reverse_ticket":
556
- return await handleReverseTicket(args, config);
557
472
  default:
558
473
  return {
559
474
  isError: true,
@@ -705,21 +620,6 @@ async function handleNote(args, config) {
705
620
  content: [{ type: "text", text: `✓ Noted (${res.key})` }],
706
621
  };
707
622
  }
708
- async function handlePromote(args, config) {
709
- const artifactId = asString(args.artifactId);
710
- const keyOverride = asStringOrUndefined(args.keyOverride);
711
- if (!artifactId)
712
- throw new Error("artifactId required");
713
- const res = await (0, shared_1.apiRequest)("POST", "/api/cli/promote", { config, body: { artifactId, keyOverride } });
714
- return {
715
- content: [
716
- {
717
- type: "text",
718
- text: `✓ Promoted to authored artifact ${res.key} (id=${res.artifactId}).`,
719
- },
720
- ],
721
- };
722
- }
723
623
  async function handleShareWith(args, config) {
724
624
  const filename = asString(args.filename);
725
625
  const to = asString(args.to);
@@ -810,26 +710,6 @@ async function handleSendTo(args, config) {
810
710
  return formatAmbiguousRecipient(err, to);
811
711
  }
812
712
  }
813
- async function handleReact(args, config) {
814
- const postId = asString(args.postId);
815
- const emoji = asString(args.emoji);
816
- if (!postId)
817
- throw new Error("postId required");
818
- if (!emoji)
819
- throw new Error("emoji required");
820
- const res = await (0, shared_1.apiRequest)("POST", "/api/cli/react", {
821
- config,
822
- body: { postId, emoji },
823
- });
824
- const verb = res.action === "added"
825
- ? "Reacted"
826
- : res.action === "removed"
827
- ? "Removed reaction"
828
- : "Swapped reaction to";
829
- return {
830
- content: [{ type: "text", text: `✓ ${verb} ${res.emoji}` }],
831
- };
832
- }
833
713
  async function handleSearch(args, config) {
834
714
  const query = asString(args.query);
835
715
  const limit = asIntOrUndefined(args.limit);
@@ -937,223 +817,6 @@ function formatAmbiguousRecipient(err, to) {
937
817
  }
938
818
  throw err;
939
819
  }
940
- async function handleReverseTicket(args, config) {
941
- const explicitTitle = asStringOrUndefined(args.title);
942
- const explicitBase = asStringOrUndefined(args.base);
943
- const forcedKey = asStringOrUndefined(args.forcedArtifactKey);
944
- // Helper that runs git from cwd with a hard-fail on error.
945
- const { execSync } = await Promise.resolve().then(() => __importStar(require("child_process")));
946
- const git = (cmd) => {
947
- try {
948
- return execSync(`git ${cmd}`, {
949
- stdio: ["pipe", "pipe", "pipe"],
950
- maxBuffer: 32 * 1024 * 1024,
951
- })
952
- .toString("utf8")
953
- .trim();
954
- }
955
- catch (err) {
956
- throw new Error(`git ${cmd} failed: ${err.message}`);
957
- }
958
- };
959
- // Confirm we're in a git repo.
960
- try {
961
- git("rev-parse --is-inside-work-tree");
962
- }
963
- catch {
964
- throw new Error("Not inside a git repository. Run this from a checked-out repo.");
965
- }
966
- // Resolve base ref — explicit, then origin/main, then origin/master.
967
- const candidates = explicitBase
968
- ? [explicitBase]
969
- : ["origin/main", "origin/master"];
970
- let baseRef = null;
971
- for (const c of candidates) {
972
- try {
973
- git(`rev-parse --verify ${c}`);
974
- baseRef = c;
975
- break;
976
- }
977
- catch {
978
- // try next
979
- }
980
- }
981
- if (!baseRef) {
982
- throw new Error(`Couldn't find a base ref (tried ${candidates.join(", ")}). ` +
983
- `Pass an explicit base via the 'base' argument.`);
984
- }
985
- const branch = git("rev-parse --abbrev-ref HEAD");
986
- if (branch === "HEAD") {
987
- throw new Error("Detached HEAD — checkout a branch first.");
988
- }
989
- const baseSha = git(`merge-base HEAD ${baseRef}`);
990
- const headSha = git("rev-parse HEAD");
991
- // Filter generated/lockfile noise so the LLM's diff budget gets spent on
992
- // real source. The --stat block (collected separately below) still includes
993
- // every file untouched, so excluded files still appear in the ticket's
994
- // "Other files touched" section if relevant.
995
- const isNoise = (p) => /(^|\/)(node_modules|dist|build|\.next|\.turbo|coverage|out)\//.test(p) ||
996
- /(^|\/)package-lock\.json$/.test(p) ||
997
- /(^|\/)yarn\.lock$/.test(p) ||
998
- /(^|\/)pnpm-lock\.yaml$/.test(p) ||
999
- /(^|\/)bun\.lock(b)?$/.test(p) ||
1000
- /(^|\/)Cargo\.lock$/.test(p) ||
1001
- /(^|\/)Gemfile\.lock$/.test(p) ||
1002
- /(^|\/)poetry\.lock$/.test(p) ||
1003
- /(^|\/)go\.sum$/.test(p);
1004
- // Helper: run a shell command tolerating non-zero exit codes (git diff
1005
- // --no-index exits 1 when files differ, which is the normal case).
1006
- // Reuses the execSync already imported above for the git() helper.
1007
- const shellTolerant = (cmd) => {
1008
- try {
1009
- return execSync(`${cmd} || true`, {
1010
- stdio: ["pipe", "pipe", "pipe"],
1011
- maxBuffer: 32 * 1024 * 1024,
1012
- })
1013
- .toString("utf8")
1014
- .trim();
1015
- }
1016
- catch {
1017
- return "";
1018
- }
1019
- };
1020
- const shQuote = (s) => `'${s.replace(/'/g, "'\\''")}'`;
1021
- // Capture all work the developer has done relative to the base ref:
1022
- // - committed changes between base and HEAD
1023
- // - uncommitted modifications to tracked files
1024
- // - new untracked files (path listed + content via synthetic diff)
1025
- // Matches the user's intent ("ticket my current changes") rather than the
1026
- // narrower "only what's been committed" interpretation.
1027
- const trackedRaw = git(`diff --name-only ${baseSha}`);
1028
- const trackedFiles = trackedRaw ? trackedRaw.split("\n").filter(Boolean) : [];
1029
- const untrackedRaw = safeGit(() => git("ls-files --others --exclude-standard"));
1030
- const untrackedFiles = untrackedRaw
1031
- ? untrackedRaw.split("\n").filter(Boolean)
1032
- : [];
1033
- const filesChanged = Array.from(new Set([...trackedFiles, ...untrackedFiles]));
1034
- if (filesChanged.length === 0) {
1035
- throw new Error(`No changes detected between ${baseRef} and your working tree on ${branch}.`);
1036
- }
1037
- // --stat block: compact ground truth for "every file touched". Tracked
1038
- // changes from `git diff --stat baseSha`; untracked files appended as
1039
- // synthesized stat rows so the LLM sees them with line counts.
1040
- let stat = "";
1041
- try {
1042
- const trackedStat = git(`diff --stat=200 ${baseSha}`);
1043
- const untrackedStatLines = untrackedFiles.map((f) => {
1044
- const wc = shellTolerant(`wc -l < ${shQuote(f)}`);
1045
- const n = parseInt(wc.split(/\s+/)[0] || "0", 10) || 0;
1046
- return ` ${f} | ${n} ++++++++++ (new file)`;
1047
- });
1048
- stat = [trackedStat, ...untrackedStatLines]
1049
- .filter(Boolean)
1050
- .join("\n")
1051
- .slice(0, 8_000);
1052
- }
1053
- catch {
1054
- // Best effort.
1055
- }
1056
- // Diff body: 40k cap, noise-filtered. Tracked changes via `git diff baseSha`,
1057
- // plus synthetic add-only diffs for untracked files via `git diff --no-index`.
1058
- let diff = "";
1059
- try {
1060
- const sourceTracked = trackedFiles.filter((f) => !isNoise(f));
1061
- const trackedDiff = sourceTracked.length > 0
1062
- ? git(`diff ${baseSha} -- ${sourceTracked.map(shQuote).join(" ")}`)
1063
- : git(`diff ${baseSha}`);
1064
- const sourceUntracked = untrackedFiles.filter((f) => !isNoise(f));
1065
- const untrackedDiffs = sourceUntracked.map((f) => shellTolerant(`git diff --no-index --no-color -- /dev/null ${shQuote(f)}`));
1066
- diff = [trackedDiff, ...untrackedDiffs]
1067
- .filter(Boolean)
1068
- .join("\n")
1069
- .slice(0, 40_000);
1070
- }
1071
- catch {
1072
- // Best effort.
1073
- }
1074
- // Repo name from origin remote.
1075
- let repo;
1076
- try {
1077
- const remoteUrl = git("config --get remote.origin.url");
1078
- const m = remoteUrl.match(/[:/]([^/:]+)\/([^/:]+?)(?:\.git)?$/);
1079
- if (!m) {
1080
- throw new Error(`Couldn't parse owner/repo from remote: ${remoteUrl}`);
1081
- }
1082
- repo = `${m[1]}/${m[2]}`;
1083
- }
1084
- catch (err) {
1085
- throw new Error(`Couldn't read git remote.origin.url: ${err.message}`);
1086
- }
1087
- const title = explicitTitle ?? safeGit(() => git("log -1 --format=%s")) ?? `WIP: ${branch}`;
1088
- const author = safeGit(() => git("config --get user.name"));
1089
- const authorEmail = safeGit(() => git("config --get user.email"));
1090
- // Post to backend.
1091
- const res = await (0, shared_1.apiRequest)("POST", "/api/cli/reverseTicketFromBranch", {
1092
- config,
1093
- body: {
1094
- branch: {
1095
- repo,
1096
- branch,
1097
- baseSha,
1098
- headSha,
1099
- title,
1100
- author,
1101
- authorEmail,
1102
- filesChanged,
1103
- diff: diff || undefined,
1104
- stat: stat || undefined,
1105
- },
1106
- forcedArtifactKey: forcedKey,
1107
- },
1108
- });
1109
- if (!res.ok || !res.artifactId) {
1110
- return {
1111
- isError: true,
1112
- content: [
1113
- {
1114
- type: "text",
1115
- text: "Reverse-ticket generation returned no result. The writer may have " +
1116
- "skipped due to missing context (no captured sessions, etc.). Try " +
1117
- "again after working on the branch a bit more, or check the " +
1118
- "server logs.",
1119
- },
1120
- ],
1121
- };
1122
- }
1123
- // Always return an absolute URL so MCP clients (Copilot Chat, Claude Code,
1124
- // etc.) can render the link correctly without guessing a host. Default to
1125
- // codiedev.com; allow override via CODIEDEV_PORTAL_URL for self-hosted.
1126
- const portalHost = (process.env.CODIEDEV_PORTAL_URL || "https://codiedev.com").replace(/\/$/, "");
1127
- const portalLink = res.portalUrl.startsWith("http")
1128
- ? res.portalUrl
1129
- : `${portalHost}${res.portalUrl.startsWith("/") ? "" : "/"}${res.portalUrl}`;
1130
- const verb = res.action === "overwritten" ? "Updated" : "Created";
1131
- return {
1132
- content: [
1133
- {
1134
- type: "text",
1135
- text: `✓ ${verb} reverse-ticket draft for branch \`${branch}\` (${repo}).\n` +
1136
- ` files changed: ${filesChanged.length}\n` +
1137
- ` base: ${baseSha.slice(0, 12)} → head: ${headSha.slice(0, 12)} (${baseRef})\n` +
1138
- ` diff size: ${diff.length} chars\n` +
1139
- ` artifact: ${res.artifactKey ?? res.artifactId}\n\n` +
1140
- `Open in your portal to review, edit, and push to Jira:\n` +
1141
- ` ${portalLink}\n\n` +
1142
- `Each call creates a fresh draft — if you've made several for the ` +
1143
- `same branch, clean them up from the portal.`,
1144
- },
1145
- ],
1146
- };
1147
- }
1148
- // Run a git invocation; return undefined instead of throwing.
1149
- function safeGit(fn) {
1150
- try {
1151
- return fn();
1152
- }
1153
- catch {
1154
- return undefined;
1155
- }
1156
- }
1157
820
  // ─────────────────────────────────────────────────────────────────────────────
1158
821
  // Helpers
1159
822
  // ─────────────────────────────────────────────────────────────────────────────
@@ -0,0 +1,49 @@
1
+ export type SelfUpdatePlan = {
2
+ kind: "npm";
3
+ prefix: string;
4
+ } | {
5
+ kind: "pnpm";
6
+ } | {
7
+ kind: "bun";
8
+ } | {
9
+ kind: "skip";
10
+ reason: string;
11
+ };
12
+ export interface PlanDeps {
13
+ hookScriptReal: string;
14
+ env: NodeJS.ProcessEnv;
15
+ platform: NodeJS.Platform;
16
+ isContainer: boolean;
17
+ isWritable: (dir: string) => boolean;
18
+ viaLauncher?: boolean;
19
+ }
20
+ /**
21
+ * Decide which package manager owns this install, from where hook.js really
22
+ * lives. We only ever upgrade the install we are running from: running
23
+ * whatever `npm` is first on PATH would, under nvm/Homebrew mixes, upgrade a
24
+ * different global tree than the one the harness config points at.
25
+ */
26
+ export declare function planSelfUpdate(deps: PlanDeps): SelfUpdatePlan;
27
+ export interface SelfUpdateState {
28
+ lastAttemptAt: number;
29
+ lastResult?: string;
30
+ fromVersion?: string;
31
+ toVersion?: string;
32
+ }
33
+ export declare function isSelfUpdateDue(state: SelfUpdateState | null, nowMs: number): boolean;
34
+ export declare function readSelfUpdateState(): SelfUpdateState | null;
35
+ /**
36
+ * Called on every hook invocation, so the not-due path is one small file read.
37
+ * Stamps the attempt before spawning so a burst of parallel hooks spawns one
38
+ * child, not one each; the child's lock covers whatever still slips through.
39
+ */
40
+ export declare function maybeSpawnSelfUpdate(hookEntry: string | undefined): void;
41
+ /**
42
+ * Where the just-installed copy lives. npm/bun reinstall in place. pnpm's
43
+ * `root -g` is <root>/node_modules up to v10 but the bare global/v11 parent
44
+ * from v11 on, with each install in a hashed child — so check both shapes and
45
+ * take the copy that is actually at the version we just installed.
46
+ */
47
+ export declare function findInstalledHookScript(plan: SelfUpdatePlan, hookEntry: string, version: string, pnpmRoot?: () => string | null): string | null;
48
+ /** Body of the detached `codiedev-hook self-update` child. Never throws. */
49
+ export declare function runSelfUpdate(hookEntry: string): Promise<SelfUpdateState>;