@devinnn/docdrift 0.1.12 → 0.1.14

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,105 @@ 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 when structured_output is empty */
105
+ function getSessionTranscript(session) {
106
+ const s = session;
107
+ const topLevel = (typeof s.output === "string" ? s.output : null) ?? (typeof s.result === "string" ? s.result : null) ?? (typeof s.transcript === "string" ? s.transcript : null);
108
+ if (topLevel)
109
+ return topLevel;
110
+ const data = s.data;
111
+ const messages = (session.messages ?? data?.messages ?? s.events);
112
+ if (!Array.isArray(messages))
113
+ return "";
114
+ return messages
115
+ .map((m) => {
116
+ if (typeof m.content === "string")
117
+ return m.content;
118
+ if (typeof m.text === "string")
119
+ return m.text;
120
+ if (typeof m.message === "string")
121
+ return m.message;
122
+ if (typeof m.body === "string")
123
+ return m.body;
124
+ if (m.message && typeof m.message === "object" && typeof m.message.content === "string")
125
+ return m.message.content;
126
+ return "";
127
+ })
128
+ .filter(Boolean)
129
+ .join("\n");
130
+ }
131
+ /** Parse setup output from <docdrift_setup_output>...</docdrift_setup_output> JSON block in text */
132
+ function parseFromStrictTag(text) {
133
+ const openTag = `<${setup_prompt_1.DOCDRIFT_SETUP_OUTPUT_TAG}>`;
134
+ const closeTag = `</${setup_prompt_1.DOCDRIFT_SETUP_OUTPUT_TAG}>`;
135
+ const openIdx = text.indexOf(openTag);
136
+ const closeIdx = text.indexOf(closeTag, openIdx);
137
+ if (openIdx === -1 || closeIdx === -1)
138
+ return null;
139
+ const inner = text.slice(openIdx + openTag.length, closeIdx).trim();
140
+ try {
141
+ const o = JSON.parse(inner);
142
+ const yaml = o.docdriftYaml;
143
+ const summary = o.summary;
144
+ if (typeof yaml !== "string" || typeof summary !== "string")
145
+ return null;
146
+ return {
147
+ docdriftYaml: yaml,
148
+ docDriftMd: typeof o.docDriftMd === "string" && o.docDriftMd ? o.docDriftMd : undefined,
149
+ workflowYml: typeof o.workflowYml === "string" && o.workflowYml ? o.workflowYml : undefined,
150
+ summary,
151
+ sessionUrl: "",
152
+ };
153
+ }
154
+ catch {
107
155
  return null;
108
- const o = raw;
109
- const yaml = o.docdriftYaml;
110
- const summary = o.summary;
111
- if (typeof yaml !== "string" || typeof summary !== "string")
156
+ }
157
+ }
158
+ /** Fallback: parse from markdown blocks like **docdriftYaml:** ```yaml ... ``` */
159
+ function parseFromMarkdownBlocks(text) {
160
+ const yamlMatch = text.match(/\*\*docdriftYaml:\*\*[\s\S]*?```(?:yaml)?\s*([\s\S]*?)```/i);
161
+ const docMdMatch = text.match(/\*\*docDriftMd:\*\*[\s\S]*?```(?:markdown)?\s*([\s\S]*?)```/i);
162
+ const workflowMatch = text.match(/\*\*workflowYml:\*\*[\s\S]*?```(?:yaml)?\s*([\s\S]*?)```/i);
163
+ const summaryBlock = text.match(/\*\*summary:\*\*([\s\S]*?)(?=\n\n\*\*|$)/i)?.[1]?.trim();
164
+ const yaml = yamlMatch?.[1]?.trim();
165
+ const summary = (summaryBlock || "Inferred from repo analysis").slice(0, 500);
166
+ if (!yaml)
112
167
  return null;
113
168
  return {
114
169
  docdriftYaml: yaml,
115
- docDriftMd: typeof o.docDriftMd === "string" && o.docDriftMd ? o.docDriftMd : undefined,
116
- workflowYml: typeof o.workflowYml === "string" && o.workflowYml ? o.workflowYml : undefined,
170
+ docDriftMd: docMdMatch?.[1]?.trim() || undefined,
171
+ workflowYml: workflowMatch?.[1]?.trim() || undefined,
117
172
  summary,
118
173
  sessionUrl: "",
119
174
  };
120
175
  }
176
+ function parseSetupOutput(session) {
177
+ const raw = session?.structured_output ?? session?.data?.structured_output;
178
+ if (raw && typeof raw === "object") {
179
+ const o = raw;
180
+ const yaml = o.docdriftYaml;
181
+ const summary = o.summary;
182
+ if (typeof yaml === "string" && typeof summary === "string") {
183
+ return {
184
+ docdriftYaml: yaml,
185
+ docDriftMd: typeof o.docDriftMd === "string" && o.docDriftMd ? o.docDriftMd : undefined,
186
+ workflowYml: typeof o.workflowYml === "string" && o.workflowYml ? o.workflowYml : undefined,
187
+ summary,
188
+ sessionUrl: "",
189
+ };
190
+ }
191
+ }
192
+ const transcript = getSessionTranscript(session);
193
+ if (transcript) {
194
+ const fromTag = parseFromStrictTag(transcript);
195
+ if (fromTag)
196
+ return fromTag;
197
+ const fromMarkdown = parseFromMarkdownBlocks(transcript);
198
+ if (fromMarkdown)
199
+ return fromMarkdown;
200
+ }
201
+ return null;
202
+ }
121
203
  async function runSetupDevin(options) {
122
204
  const cwd = options.cwd ?? process.cwd();
123
205
  const outputPath = node_path_1.default.resolve(cwd, options.outputPath ?? "docdrift.yaml");
@@ -139,7 +221,7 @@ async function runSetupDevin(options) {
139
221
  process.stdout.write("Uploading schema…\n");
140
222
  const schemaPath = getSchemaPath();
141
223
  const attachmentUrl = await (0, v1_1.devinUploadAttachment)(apiKey, schemaPath);
142
- const prompt = (0, setup_prompt_1.buildSetupPrompt)([attachmentUrl]);
224
+ const prompt = (0, setup_prompt_1.buildSetupPrompt)([attachmentUrl], { openPr: options.openPr });
143
225
  process.stdout.write("Creating Devin session…\n");
144
226
  const session = await (0, v1_1.devinCreateSession)(apiKey, {
145
227
  prompt,
@@ -147,20 +229,26 @@ async function runSetupDevin(options) {
147
229
  max_acu_limit: 2,
148
230
  tags: ["docdrift", "setup"],
149
231
  attachments: [attachmentUrl],
150
- structured_output: {
151
- schema: schemas_1.SetupOutputSchema,
152
- },
232
+ structured_output_schema: schemas_1.SetupOutputSchema,
153
233
  metadata: { purpose: "docdrift-setup" },
154
234
  });
155
235
  process.stdout.write("Devin is analyzing the repo and generating config…\n");
156
236
  process.stdout.write(`Session: ${session.url}\n`);
157
237
  const finalSession = await (0, v1_1.pollUntilTerminal)(apiKey, session.session_id, 15 * 60_000);
158
238
  const result = parseSetupOutput(finalSession);
239
+ if (result?.summary && !(finalSession.structured_output ?? finalSession.data?.structured_output)) {
240
+ process.stdout.write(" (Parsed from session transcript — structured_output was empty; using strict-tag fallback)\n");
241
+ }
159
242
  if (!result) {
160
243
  throw new Error("Devin session did not return valid setup output. Check the session for details: " + session.url);
161
244
  }
162
245
  result.sessionUrl = session.url;
163
- // Write files
246
+ const prUrl = finalSession.pull_request_url ??
247
+ finalSession.pr_url ??
248
+ finalSession.pull_request?.url;
249
+ if (prUrl)
250
+ result.prUrl = prUrl;
251
+ // Write files (always write for validation; when openPr, Devin also created PR)
164
252
  node_fs_1.default.mkdirSync(node_path_1.default.dirname(outputPath), { recursive: true });
165
253
  node_fs_1.default.writeFileSync(outputPath, result.docdriftYaml, "utf8");
166
254
  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.14",
4
4
  "private": false,
5
5
  "description": "Detect and remediate documentation drift with Devin sessions",
6
6
  "main": "dist/src/index.js",