@aidriftline/driftline 0.1.11 → 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/README.md CHANGED
@@ -76,6 +76,9 @@ If you are an agent (Claude Code, Cursor, Copilot, Codex…) changing code that
76
76
 
77
77
  Driftline also runs as an [MCP](https://modelcontextprotocol.io) server, so your agent can ask it directly. Tools: `check_models` (status, date and replacement for a list of IDs), `scan_project` (retired and retiring IDs in a folder, with file and line) and `upcoming_retirements` (what retires in the next N days). It reads the daily-updated list from aidriftline.com and falls back to the copy in the package when offline.
78
78
 
79
+ [![Add to Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en/install-mcp?name=driftline&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIkBhaWRyaWZ0bGluZS9kcmlmdGxpbmUiLCJtY3AiXX0%3D)
80
+ [![Install in VS Code](https://img.shields.io/badge/VS_Code-Install_Server-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://insiders.vscode.dev/redirect?url=vscode%3Amcp%2Finstall%3F%257B%2522name%2522%253A%2522driftline%2522%252C%2522command%2522%253A%2522npx%2522%252C%2522args%2522%253A%255B%2522-y%2522%252C%2522%2540aidriftline%252Fdriftline%2522%252C%2522mcp%2522%255D%257D)
81
+
79
82
  Claude Code:
80
83
 
81
84
  ```sh
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aidriftline/driftline",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
4
4
  "description": "Finds OpenAI, Anthropic and Google model IDs in your code and warns before they are retired. CLI, GitHub Action and MCP server.",
5
5
  "mcpName": "io.github.AI-Driftline/driftline",
6
6
  "type": "module",
package/src/cli.js CHANGED
@@ -28,10 +28,10 @@ function accountFromGit() {
28
28
  const HELP = `Driftline: finds AI model IDs in your code and warns before they are retired.
29
29
 
30
30
  Usage:
31
- npx github:AI-Driftline/driftline [directory] scan and print the report
32
- npx github:AI-Driftline/driftline activate <license-key> register this GitHub account for Pro
33
- npx github:AI-Driftline/driftline deactivate <code> free that account's slot
34
- npx github:AI-Driftline/driftline mcp MCP server for coding agents (stdio)
31
+ npx @aidriftline/driftline [directory] scan and print the report
32
+ npx @aidriftline/driftline activate <license-key> register this GitHub account for Pro
33
+ npx @aidriftline/driftline deactivate <code> free that account's slot
34
+ npx @aidriftline/driftline mcp MCP server for coding agents (stdio)
35
35
 
36
36
  Options:
37
37
  --json print the scan as JSON (for scripts and coding agents)
package/src/mcp.js CHANGED
@@ -3,7 +3,7 @@
3
3
  // Speaks JSON-RPC 2.0, one message per line on stdin/stdout (the MCP stdio transport). No dependencies.
4
4
  // Data: the live registry at aidriftline.com/models.json (updated daily), or the copy bundled with this
5
5
  // version when the site can't be reached or is older. Set DRIFTLINE_OFFLINE=1 to never fetch.
6
- import { readFileSync } from "node:fs";
6
+ import { readFileSync, statSync } from "node:fs";
7
7
  import { resolve } from "node:path";
8
8
  import { createInterface } from "node:readline";
9
9
  import { REGISTRY_PATH, buildRegistry, classify, normalizeId } from "./registry.js";
@@ -55,46 +55,110 @@ function describe(rawId, reg, today) {
55
55
  };
56
56
  }
57
57
 
58
+ const STATUS_ENUM = ["retired", "at-risk", "deprecated", "active", "unknown"];
59
+ const MODEL_RESULT = {
60
+ type: "object",
61
+ properties: {
62
+ id: { type: "string", description: "The ID as given" },
63
+ normalized: { type: "string", description: "The ID after removing provider prefixes such as models/ or openai/" },
64
+ status: { type: "string", enum: STATUS_ENUM, description: "retired = calls fail (or are re-routed) now; at-risk = retires within 90 days; deprecated = retirement announced, further out; active = announced as active; unknown = no retirement published" },
65
+ provider: { type: "string" }, retires: { type: "string", description: "YYYY-MM-DD" },
66
+ qualifier: { type: "string", enum: ["exact", "earliest", "not-before"], description: "exact date, earliest possible shutdown (Google), or not before this date (Anthropic)" },
67
+ days_left: { type: "number", description: "Days until the date; 0 or negative when it has passed" },
68
+ replacement: { type: ["string", "null"] },
69
+ routes: { type: "boolean", description: "true: after the date the provider answers with the replacement model instead of failing" },
70
+ alias_of: { type: "string" }, page: { type: "string", description: "aidriftline.com page with the source for this date" },
71
+ advice: { type: "string", description: "One plain sentence to pass on to the user" },
72
+ },
73
+ required: ["id", "status", "advice"],
74
+ };
75
+ const META = {
76
+ registry_checked: { type: "string", description: "Date the registry was last checked against the provider pages" },
77
+ registry_source: { type: "string", enum: ["live", "bundled"], description: "live = aidriftline.com/models.json; bundled = the copy shipped in the package (used offline)" },
78
+ all_dates: { type: "string" },
79
+ next_step: { type: "string", description: "Present when a model has to move: how to test prompts on the replacement first" },
80
+ };
81
+ const READ_ONLY = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true };
82
+
58
83
  const TOOLS = [
59
84
  {
60
85
  name: "check_models",
61
- description: "Check AI model IDs (OpenAI, Anthropic, Google) for retirement: status, retirement date, replacement. Use before writing or keeping a model ID in code. Accepts provider spellings such as models/gemini-2.5-pro, openai/gpt-4o or Bedrock IDs.",
62
- inputSchema: { type: "object", properties: { ids: { type: "array", items: { type: "string" }, minItems: 1, description: "Model IDs, e.g. [\"gpt-4o\", \"claude-sonnet-4-5\"]" } }, required: ["ids"] },
86
+ title: "Check model IDs for retirement",
87
+ description: "Look up OpenAI, Anthropic and Google model IDs and return, for each one, its retirement status, retirement date, days left and replacement. " +
88
+ "Use it before writing, changing or keeping a model ID in code, or when the user asks whether a model still works. " +
89
+ "To find the IDs inside a codebase use scan_project instead; to list everything retiring soon use upcoming_retirements. " +
90
+ "Read-only. Data comes from the providers' official deprecation pages, re-checked daily at aidriftline.com (fetched with a 4 s timeout, falling back to the copy in the package when offline). " +
91
+ "IDs not in the registry return status \"unknown\": no retirement is published for them, which does not prove the ID exists. Up to 100 IDs per call; extra IDs are ignored.",
92
+ inputSchema: { type: "object", properties: {
93
+ ids: { type: "array", items: { type: "string" }, minItems: 1, maxItems: 100,
94
+ description: "Model IDs exactly as written in code, e.g. [\"gpt-4o\", \"claude-sonnet-4-5\", \"models/gemini-2.5-pro\"]. Provider prefixes (openai/, models/), Bedrock, Vertex and OpenRouter spellings and Claude short names are accepted and normalized." },
95
+ }, required: ["ids"] },
96
+ outputSchema: { type: "object", properties: { models: { type: "array", items: MODEL_RESULT }, ...META }, required: ["models", "registry_checked", "registry_source"] },
97
+ annotations: { title: "Check model IDs for retirement", ...READ_ONLY },
63
98
  },
64
99
  {
65
100
  name: "scan_project",
66
- description: "Scan a project folder for AI model IDs in code and config and report the ones that are retired, retiring or being re-routed, with file and line. Pass the absolute path of the project if unsure what the current directory is.",
101
+ title: "Scan a project for retiring model IDs",
102
+ description: "Scan a project folder for OpenAI, Anthropic and Google model IDs in code and config files and report the ones that are retired, retiring soon, deprecated or being re-routed, with file and line, most urgent first. " +
103
+ "Use it to audit a repository or before a release. For IDs you already know use check_models; for a time-based list use upcoming_retirements. " +
104
+ "Read-only: reads files on this machine and never changes them. Scans subfolders recursively, skips .git, node_modules, vendor, dist, build and similar folders, and files over 1 MB. " +
105
+ "Mentions in .md and .txt files are marked in_docs. Returns at most 200 findings (truncated: true when cut). Fails with an error if the path does not exist.",
67
106
  inputSchema: { type: "object", properties: {
68
- path: { type: "string", description: "Folder to scan (default: the server's current directory)" },
69
- include_ok: { type: "boolean", description: "Also list models with no retirement announced (default false)" },
107
+ path: { type: "string", description: "Absolute path of the folder to scan. Defaults to the server's working directory, which may not be the user's project, so pass it explicitly." },
108
+ include_ok: { type: "boolean", default: false, description: "Also list models with no retirement announced (status active or unknown). Default false." },
70
109
  } },
110
+ outputSchema: { type: "object", properties: {
111
+ scanned: { type: "string" }, model_references: { type: "number", description: "All model ID mentions found, before filtering" },
112
+ flagged: { type: "number" }, truncated: { type: "boolean" },
113
+ findings: { type: "array", items: { ...MODEL_RESULT, properties: { file: { type: "string" }, line: { type: "number" }, in_docs: { type: "boolean" }, ...MODEL_RESULT.properties } } },
114
+ ...META,
115
+ }, required: ["scanned", "flagged", "findings"] },
116
+ annotations: { title: "Scan a project for retiring model IDs", ...READ_ONLY },
71
117
  },
72
118
  {
73
119
  name: "upcoming_retirements",
74
- description: "List AI models retiring or being re-routed in the next N days, soonest first. Optionally filter by provider.",
120
+ title: "List upcoming model retirements",
121
+ description: "List OpenAI, Anthropic and Google models whose retirement or re-routing date falls in the next N days, soonest first, with replacement for each. " +
122
+ "Use it to answer \"what is being retired soon?\" or to plan migrations. It does not look at code: use scan_project for that, or check_models for specific IDs. " +
123
+ "Read-only. Models already past their date are not included (check_models reports those as retired). Anthropic \"not before\" dates are left out because those models are still active.",
75
124
  inputSchema: { type: "object", properties: {
76
- days: { type: "number", description: "Look-ahead window in days (default 90)" },
77
- provider: { type: "string", enum: ["openai", "anthropic", "google"] },
125
+ days: { type: "number", minimum: 1, maximum: 3650, default: 90, description: "Look-ahead window in days, 1 to 3650. Default 90." },
126
+ provider: { type: "string", enum: ["openai", "anthropic", "google"], description: "Only this provider. Omit for all three." },
78
127
  } },
128
+ outputSchema: { type: "object", properties: { days: { type: "number" }, count: { type: "number" }, models: { type: "array", items: MODEL_RESULT }, ...META }, required: ["days", "count", "models"] },
129
+ annotations: { title: "List upcoming model retirements", ...READ_ONLY },
79
130
  },
80
131
  ];
81
132
 
133
+ // When a model has to move, the open question is whether the replacement answers the same way.
134
+ const SWITCHING = new Set(["retired", "at-risk", "deprecated"]);
135
+ function nextStep(rows) {
136
+ const r = rows.find((x) => x.replacement && (SWITCHING.has(x.status) || x.routes));
137
+ if (!r) return {};
138
+ return { next_step: `Before moving ${r.id} to ${r.replacement}, test the prompts this app really sends on both models: answers often change between model versions. ` +
139
+ "Driftline Pro runs each prompt on the current model and the replacement in GitHub Actions and shows what changed (https://aidriftline.com/#drift)." };
140
+ }
141
+
82
142
  export async function callTool(name, args = {}, { reg, source, today = new Date() }) {
83
143
  const meta = { registry_checked: reg.checked, registry_source: source, all_dates: "https://aidriftline.com/models/" };
84
144
  if (name === "check_models") {
85
145
  const ids = Array.isArray(args.ids) ? args.ids.filter((x) => typeof x === "string").slice(0, 100) : [];
86
146
  if (!ids.length) throw new Error("ids must be a non-empty array of model ID strings");
87
- return { models: ids.map((id) => describe(id, reg, today)), ...meta };
147
+ const models = ids.map((id) => describe(id, reg, today));
148
+ return { models, ...nextStep(models), ...meta };
88
149
  }
89
150
  if (name === "scan_project") {
90
151
  const root = resolve(args.path || ".");
152
+ let isDir = false;
153
+ try { isDir = statSync(root).isDirectory(); } catch { /* missing */ }
154
+ if (!isDir) throw new Error(`Not a folder: ${root}. Pass the absolute path of the project to scan.`);
91
155
  const hits = scanDirectory(root, { known: reg.byId });
92
156
  const rows = hits.map((h) => ({ file: h.file.split("\\").join("/"), line: h.line, in_docs: h.doc, ...describe(h.id, reg, today) }))
93
157
  .filter((r) => args.include_ok || !["active", "unknown"].includes(r.status));
94
158
  const order = { retired: 0, "at-risk": 1, deprecated: 2, active: 3, unknown: 4 };
95
159
  rows.sort((a, b) => order[a.status] - order[b.status] || (a.days_left ?? 1e9) - (b.days_left ?? 1e9));
96
160
  return { scanned: root, model_references: hits.length, flagged: rows.length, findings: rows.slice(0, 200),
97
- ...(rows.length > 200 ? { truncated: true } : {}), ...meta };
161
+ ...(rows.length > 200 ? { truncated: true } : {}), ...nextStep(rows), ...meta };
98
162
  }
99
163
  if (name === "upcoming_retirements") {
100
164
  const days = Number.isFinite(args.days) && args.days > 0 ? Math.min(args.days, 3650) : 90;
package/src/run.js CHANGED
@@ -57,7 +57,7 @@ export async function run({
57
57
  notices.push(`Drift tests stopped: ${e.message}. The scan results above are complete.`);
58
58
  }
59
59
  } else if (findings.some((f) => FLAGGED.has(f.status))) {
60
- notices.push(`Add ${promptsFile} to test your real prompts on the replacement model before you switch.`);
60
+ notices.push(`Before you switch, test the prompts your app really sends on the replacement model: list them in ${promptsFile} and Driftline Pro runs each one on both models and shows what changed. https://aidriftline.com/#drift`);
61
61
  }
62
62
 
63
63
  const atRisk = findings.filter((f) => !f.doc && (f.status === "retired" || f.status === "at-risk")).length;