@devinnn/docdrift 0.1.12 → 0.1.13

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/src/cli.js CHANGED
@@ -57,7 +57,7 @@ async function main() {
57
57
  " status Show run status [--since 24h]\n" +
58
58
  " sla-check Check SLA for unmerged PRs\n" +
59
59
  " setup Interactive setup (generates v2 docdrift.yaml)\n" +
60
- " generate-yaml Generate config [--output path] [--force]");
60
+ " generate-yaml Generate config [--output path] [--force] [--open-pr]");
61
61
  }
62
62
  if (command === "setup" || command === "generate-yaml") {
63
63
  require("dotenv").config();
@@ -65,6 +65,7 @@ async function main() {
65
65
  await runSetup({
66
66
  outputPath: getArg(args, "--output") ?? "docdrift.yaml",
67
67
  force: args.includes("--force"),
68
+ openPr: args.includes("--open-pr"),
68
69
  });
69
70
  return;
70
71
  }
@@ -101,23 +101,88 @@ async function runSetupLocal(options) {
101
101
  sessionUrl: "",
102
102
  };
103
103
  }
104
- function parseSetupOutput(session) {
105
- const raw = session?.structured_output ?? session?.data?.structured_output;
106
- if (!raw || typeof raw !== "object")
104
+ /** Extract transcript text from session for fallback parsing */
105
+ function getSessionTranscript(session) {
106
+ const messages = session.messages ?? session.data?.messages;
107
+ if (!Array.isArray(messages))
108
+ return "";
109
+ return messages
110
+ .map((m) => (typeof m.content === "string" ? m.content : m.text ?? ""))
111
+ .filter(Boolean)
112
+ .join("\n");
113
+ }
114
+ /** Parse setup output from <docdrift_setup_output>...</docdrift_setup_output> JSON block in text */
115
+ function parseFromStrictTag(text) {
116
+ const openTag = `<${setup_prompt_1.DOCDRIFT_SETUP_OUTPUT_TAG}>`;
117
+ const closeTag = `</${setup_prompt_1.DOCDRIFT_SETUP_OUTPUT_TAG}>`;
118
+ const openIdx = text.indexOf(openTag);
119
+ const closeIdx = text.indexOf(closeTag, openIdx);
120
+ if (openIdx === -1 || closeIdx === -1)
107
121
  return null;
108
- const o = raw;
109
- const yaml = o.docdriftYaml;
110
- const summary = o.summary;
111
- if (typeof yaml !== "string" || typeof summary !== "string")
122
+ const inner = text.slice(openIdx + openTag.length, closeIdx).trim();
123
+ try {
124
+ const o = JSON.parse(inner);
125
+ const yaml = o.docdriftYaml;
126
+ const summary = o.summary;
127
+ if (typeof yaml !== "string" || typeof summary !== "string")
128
+ return null;
129
+ return {
130
+ docdriftYaml: yaml,
131
+ docDriftMd: typeof o.docDriftMd === "string" && o.docDriftMd ? o.docDriftMd : undefined,
132
+ workflowYml: typeof o.workflowYml === "string" && o.workflowYml ? o.workflowYml : undefined,
133
+ summary,
134
+ sessionUrl: "",
135
+ };
136
+ }
137
+ catch {
138
+ return null;
139
+ }
140
+ }
141
+ /** Fallback: parse from markdown blocks like **docdriftYaml:** ```yaml ... ``` */
142
+ function parseFromMarkdownBlocks(text) {
143
+ const yamlMatch = text.match(/\*\*docdriftYaml:\*\*[\s\S]*?```(?:yaml)?\s*([\s\S]*?)```/i);
144
+ const docMdMatch = text.match(/\*\*docDriftMd:\*\*[\s\S]*?```(?:markdown)?\s*([\s\S]*?)```/i);
145
+ const workflowMatch = text.match(/\*\*workflowYml:\*\*[\s\S]*?```(?:yaml)?\s*([\s\S]*?)```/i);
146
+ const summaryBlock = text.match(/\*\*summary:\*\*([\s\S]*?)(?=\n\n\*\*|$)/i)?.[1]?.trim();
147
+ const yaml = yamlMatch?.[1]?.trim();
148
+ const summary = (summaryBlock || "Inferred from repo analysis").slice(0, 500);
149
+ if (!yaml)
112
150
  return null;
113
151
  return {
114
152
  docdriftYaml: yaml,
115
- docDriftMd: typeof o.docDriftMd === "string" && o.docDriftMd ? o.docDriftMd : undefined,
116
- workflowYml: typeof o.workflowYml === "string" && o.workflowYml ? o.workflowYml : undefined,
153
+ docDriftMd: docMdMatch?.[1]?.trim() || undefined,
154
+ workflowYml: workflowMatch?.[1]?.trim() || undefined,
117
155
  summary,
118
156
  sessionUrl: "",
119
157
  };
120
158
  }
159
+ function parseSetupOutput(session) {
160
+ const raw = session?.structured_output ?? session?.data?.structured_output;
161
+ if (raw && typeof raw === "object") {
162
+ const o = raw;
163
+ const yaml = o.docdriftYaml;
164
+ const summary = o.summary;
165
+ if (typeof yaml === "string" && typeof summary === "string") {
166
+ return {
167
+ docdriftYaml: yaml,
168
+ docDriftMd: typeof o.docDriftMd === "string" && o.docDriftMd ? o.docDriftMd : undefined,
169
+ workflowYml: typeof o.workflowYml === "string" && o.workflowYml ? o.workflowYml : undefined,
170
+ summary,
171
+ sessionUrl: "",
172
+ };
173
+ }
174
+ }
175
+ const transcript = getSessionTranscript(session);
176
+ if (transcript) {
177
+ const fromTag = parseFromStrictTag(transcript);
178
+ if (fromTag)
179
+ return fromTag;
180
+ const fromMarkdown = parseFromMarkdownBlocks(transcript);
181
+ if (fromMarkdown)
182
+ return fromMarkdown;
183
+ }
184
+ return null;
185
+ }
121
186
  async function runSetupDevin(options) {
122
187
  const cwd = options.cwd ?? process.cwd();
123
188
  const outputPath = node_path_1.default.resolve(cwd, options.outputPath ?? "docdrift.yaml");
@@ -139,7 +204,7 @@ async function runSetupDevin(options) {
139
204
  process.stdout.write("Uploading schema…\n");
140
205
  const schemaPath = getSchemaPath();
141
206
  const attachmentUrl = await (0, v1_1.devinUploadAttachment)(apiKey, schemaPath);
142
- const prompt = (0, setup_prompt_1.buildSetupPrompt)([attachmentUrl]);
207
+ const prompt = (0, setup_prompt_1.buildSetupPrompt)([attachmentUrl], { openPr: options.openPr });
143
208
  process.stdout.write("Creating Devin session…\n");
144
209
  const session = await (0, v1_1.devinCreateSession)(apiKey, {
145
210
  prompt,
@@ -160,7 +225,12 @@ async function runSetupDevin(options) {
160
225
  throw new Error("Devin session did not return valid setup output. Check the session for details: " + session.url);
161
226
  }
162
227
  result.sessionUrl = session.url;
163
- // Write files
228
+ const prUrl = finalSession.pull_request_url ??
229
+ finalSession.pr_url ??
230
+ finalSession.pull_request?.url;
231
+ if (prUrl)
232
+ result.prUrl = prUrl;
233
+ // Write files (always write for validation; when openPr, Devin also created PR)
164
234
  node_fs_1.default.mkdirSync(node_path_1.default.dirname(outputPath), { recursive: true });
165
235
  node_fs_1.default.writeFileSync(outputPath, result.docdriftYaml, "utf8");
166
236
  if (result.docDriftMd) {
@@ -113,6 +113,8 @@ async function runSetup(options = {}) {
113
113
  console.log("\nSummary: " + result.summary);
114
114
  if (result.sessionUrl)
115
115
  console.log("\nSession: " + result.sessionUrl);
116
+ if (result.prUrl)
117
+ console.log("PR: " + result.prUrl);
116
118
  console.log("\nNext steps:");
117
119
  const usedLocal = mode === "local" || (mode === "devin" && !hasDevinKey) || usedLocalFallback;
118
120
  if (usedLocal) {
@@ -5,11 +5,46 @@
5
5
  * Devin's Machine, so Devin has full context.
6
6
  */
7
7
  Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.DOCDRIFT_SETUP_OUTPUT_TAG = void 0;
8
9
  exports.buildSetupPrompt = buildSetupPrompt;
10
+ /** XML tag used as strict delimiter for fallback parsing from chat transcript */
11
+ exports.DOCDRIFT_SETUP_OUTPUT_TAG = "docdrift_setup_output";
9
12
  function attachmentBlock(urls) {
10
13
  return urls.map((url, i) => `- ATTACHMENT ${i + 1}: ${url}`).join("\n");
11
14
  }
12
- function buildSetupPrompt(attachmentUrls) {
15
+ function strictOutputBlock() {
16
+ return [
17
+ "",
18
+ "STRICT OUTPUT FORMAT (REQUIRED FOR PARSING):",
19
+ "You MUST include this exact block in your final message so we can reliably parse it.",
20
+ "Format: open with <" +
21
+ exports.DOCDRIFT_SETUP_OUTPUT_TAG +
22
+ ">, then valid JSON, then close with </" +
23
+ exports.DOCDRIFT_SETUP_OUTPUT_TAG +
24
+ ">.",
25
+ "Example (escape quotes in strings as \\\"):",
26
+ "",
27
+ `<${exports.DOCDRIFT_SETUP_OUTPUT_TAG}>`,
28
+ '{"docdriftYaml":"# yaml...","docDriftMd":"# DocDrift...","workflowYml":"name: docdrift...","summary":"OpenAPI at..."}',
29
+ `</${exports.DOCDRIFT_SETUP_OUTPUT_TAG}>`,
30
+ "",
31
+ "Rules: Valid JSON only. Newlines in YAML/yml strings become \\n. Escape \" as \\\".",
32
+ ].join("\n");
33
+ }
34
+ function buildSetupPrompt(attachmentUrls, options) {
35
+ const openPr = options?.openPr ?? false;
36
+ const createFilesBlock = openPr
37
+ ? [
38
+ "",
39
+ "CREATE A PULL REQUEST:",
40
+ "- Create branch docdrift/setup from main",
41
+ "- Create docdrift.yaml, .docdrift/DocDrift.md, .github/workflows/docdrift.yml in the repo",
42
+ "- Commit with message: [docdrift] Add docdrift configuration",
43
+ "- Push and open a PR to main with title: [docdrift] Add docdrift configuration",
44
+ "- In the PR description, explain what was inferred (openapi export, docsite path, verification commands)",
45
+ "- You MUST still emit the strict output block below so we can validate the config",
46
+ ].join("\n")
47
+ : "Do NOT create files in the repo. Only produce the structured output and the strict output block.";
13
48
  return [
14
49
  "You are Devin. Task: set up docdrift for this repository.",
15
50
  "",
@@ -43,12 +78,12 @@ function buildSetupPrompt(attachmentUrls) {
43
78
  " - Note: docdrift-sla-check.yml (daily cron for PRs open 7+ days) is added automatically",
44
79
  "",
45
80
  "OUTPUT:",
46
- "Emit your final output in the provided structured output schema.",
81
+ "Emit your final output in the provided structured output schema if possible.",
47
82
  "- docdriftYaml: complete YAML string (no leading/trailing comments about the task)",
48
83
  "- docDriftMd: content for .docdrift/DocDrift.md, or empty string to omit",
49
84
  "- workflowYml: content for .github/workflows/docdrift.yml, or empty string to omit",
50
85
  "- summary: what you inferred (openapi export, docsite path, verification commands)",
51
- "",
52
- "Do NOT create files in the repo. Only produce the structured output.",
86
+ createFilesBlock,
87
+ strictOutputBlock(),
53
88
  ].join("\n");
54
89
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devinnn/docdrift",
3
- "version": "0.1.12",
3
+ "version": "0.1.13",
4
4
  "private": false,
5
5
  "description": "Detect and remediate documentation drift with Devin sessions",
6
6
  "main": "dist/src/index.js",