drift-ml 0.1.0
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 +78 -0
- package/bin/drift.js +175 -0
- package/package.json +13 -0
package/README.md
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# drift-ml
|
|
2
|
+
|
|
3
|
+
**Drift** by Lakshit Sachdeva — terminal-first, chat-based AutoML. Same engine as the web app. Local-first: the engine runs on your machine; no commands to memorize.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Exactly what to do
|
|
8
|
+
|
|
9
|
+
1. **Install drift**
|
|
10
|
+
```bash
|
|
11
|
+
npm install -g drift-ml
|
|
12
|
+
```
|
|
13
|
+
Requires Node.js ≥ 18 and Python 3 on your PATH.
|
|
14
|
+
|
|
15
|
+
2. **Run drift**
|
|
16
|
+
```bash
|
|
17
|
+
drift
|
|
18
|
+
```
|
|
19
|
+
On first run, the CLI detects your OS and architecture, downloads the correct engine binary into `~/.drift/bin/`, and starts the engine in the background. You’ll see a short welcome and instructions in the terminal.
|
|
20
|
+
|
|
21
|
+
3. **Use a local LLM**
|
|
22
|
+
Training and planning use an LLM. You need one of:
|
|
23
|
+
- **Gemini CLI** — install it and set `GEMINI_API_KEY` or have `gemini` on your PATH.
|
|
24
|
+
- **Ollama** — run `ollama run llama2` (or another model).
|
|
25
|
+
- Another local LLM the engine supports.
|
|
26
|
+
|
|
27
|
+
4. **Chat**
|
|
28
|
+
- `load path/to/data.csv`
|
|
29
|
+
- `predict price` (or any column)
|
|
30
|
+
- `try something stronger`
|
|
31
|
+
- `why is accuracy capped`
|
|
32
|
+
- `quit` to exit.
|
|
33
|
+
|
|
34
|
+
That’s it. The engine runs locally. The web app (if you use it) can be hosted on Vercel; the engine stays on your machine.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## What is drift?
|
|
39
|
+
|
|
40
|
+
- **Local-first** — The engine runs on your machine. Training and planning stay local; you never send data to our servers.
|
|
41
|
+
- **Terminal-first, chat-based** — Same engine as the web app. No commands to memorize; chat in natural language.
|
|
42
|
+
- **Engine** — On first run the CLI downloads and starts the engine from `~/.drift/bin/`. Or set `DRIFT_BACKEND_URL` to a running engine URL.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Install (details)
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
npm install -g drift-ml
|
|
50
|
+
drift
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The `drift` command installs or upgrades the chat CLI (Python) and runs it. You get the welcome and instructions every time.
|
|
54
|
+
|
|
55
|
+
### Alternative: pipx (Python only)
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
pipx install drift
|
|
59
|
+
drift
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## Example usage
|
|
65
|
+
|
|
66
|
+
```text
|
|
67
|
+
drift › load iris.csv
|
|
68
|
+
drift › predict sepal.length
|
|
69
|
+
drift › try something stronger
|
|
70
|
+
drift › why is accuracy capped
|
|
71
|
+
drift › quit
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## License
|
|
77
|
+
|
|
78
|
+
MIT
|
package/bin/drift.js
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { spawnSync, spawn } = require("child_process");
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
const https = require("https");
|
|
7
|
+
const http = require("http");
|
|
8
|
+
|
|
9
|
+
const ENGINE_PORT = process.env.DRIFT_ENGINE_PORT || "8000";
|
|
10
|
+
const ENGINE_BASE = process.env.DRIFT_ENGINE_BASE_URL || "https://github.com/lakshitsachdeva/drift/releases/latest/download";
|
|
11
|
+
const HEALTH_URL = `http://127.0.0.1:${ENGINE_PORT}/health`;
|
|
12
|
+
const HEALTH_TIMEOUT_MS = 2000;
|
|
13
|
+
const HEALTH_POLL_MS = 500;
|
|
14
|
+
const HEALTH_POLL_MAX = 60; // 30s
|
|
15
|
+
|
|
16
|
+
function which(cmd) {
|
|
17
|
+
const { status, stdout } = spawnSync("which", [cmd], { encoding: "utf8" });
|
|
18
|
+
return status === 0 ? (stdout || "").trim() : null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function getPlatformKey() {
|
|
22
|
+
const p = process.platform;
|
|
23
|
+
const a = process.arch;
|
|
24
|
+
const plat = p === "darwin" ? "macos" : p === "win32" ? "windows" : "linux";
|
|
25
|
+
const arch = a === "arm64" || a === "aarch64" ? "arm64" : "x64";
|
|
26
|
+
return { plat, arch };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function getEngineDir() {
|
|
30
|
+
const home = process.env.HOME || process.env.USERPROFILE || process.env.HOMEPATH;
|
|
31
|
+
if (!home) return null;
|
|
32
|
+
return path.join(home, ".drift", "bin");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function getEnginePath() {
|
|
36
|
+
const dir = getEngineDir();
|
|
37
|
+
if (!dir) return null;
|
|
38
|
+
const { plat, arch } = getPlatformKey();
|
|
39
|
+
const ext = process.platform === "win32" ? ".exe" : "";
|
|
40
|
+
return path.join(dir, `drift-engine-${plat}-${arch}${ext}`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function fetchOk(url) {
|
|
44
|
+
return new Promise((resolve, reject) => {
|
|
45
|
+
const client = url.startsWith("https") ? https : http;
|
|
46
|
+
const req = client.get(url, { timeout: HEALTH_TIMEOUT_MS }, (res) => {
|
|
47
|
+
const redirect = res.statusCode >= 301 && res.statusCode <= 302 && res.headers.location;
|
|
48
|
+
if (redirect) {
|
|
49
|
+
fetchOk(redirect).then(resolve).catch(reject);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
resolve(res.statusCode === 200);
|
|
53
|
+
});
|
|
54
|
+
req.on("error", () => resolve(false));
|
|
55
|
+
req.on("timeout", () => { req.destroy(); resolve(false); });
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function downloadFile(url, destPath) {
|
|
60
|
+
return new Promise((resolve, reject) => {
|
|
61
|
+
const client = url.startsWith("https") ? https : http;
|
|
62
|
+
const req = client.get(url, (res) => {
|
|
63
|
+
const redirect = res.statusCode >= 301 && res.statusCode <= 302 && res.headers.location;
|
|
64
|
+
if (redirect) {
|
|
65
|
+
downloadFile(redirect, destPath).then(resolve).catch(reject);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (res.statusCode !== 200) {
|
|
69
|
+
reject(new Error(`Download failed: ${res.statusCode}`));
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const dir = path.dirname(destPath);
|
|
73
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
74
|
+
const file = fs.createWriteStream(destPath);
|
|
75
|
+
res.pipe(file);
|
|
76
|
+
file.on("finish", () => { file.close(); resolve(); });
|
|
77
|
+
file.on("error", reject);
|
|
78
|
+
});
|
|
79
|
+
req.on("error", reject);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function engineRunning() {
|
|
84
|
+
return fetchOk(HEALTH_URL);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function waitForEngine() {
|
|
88
|
+
return new Promise((resolve) => {
|
|
89
|
+
let n = 0;
|
|
90
|
+
const t = setInterval(() => {
|
|
91
|
+
n++;
|
|
92
|
+
fetchOk(HEALTH_URL).then((ok) => {
|
|
93
|
+
if (ok) { clearInterval(t); resolve(true); }
|
|
94
|
+
else if (n >= HEALTH_POLL_MAX) { clearInterval(t); resolve(false); }
|
|
95
|
+
});
|
|
96
|
+
}, HEALTH_POLL_MS);
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function ensureEngine() {
|
|
101
|
+
const binPath = getEnginePath();
|
|
102
|
+
const binDir = getEngineDir();
|
|
103
|
+
if (!binPath || !binDir) {
|
|
104
|
+
console.error("drift: Could not resolve engine directory (~/.drift/bin). Set HOME or USERPROFILE.");
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
if (!fs.existsSync(binPath)) {
|
|
108
|
+
const { plat, arch } = getPlatformKey();
|
|
109
|
+
const ext = process.platform === "win32" ? ".exe" : "";
|
|
110
|
+
const asset = `drift-engine-${plat}-${arch}${ext}`;
|
|
111
|
+
const url = `${ENGINE_BASE}/${asset}`;
|
|
112
|
+
process.stderr.write(`drift: Downloading engine (${asset})...\n`);
|
|
113
|
+
try {
|
|
114
|
+
await downloadFile(url, binPath);
|
|
115
|
+
} catch (e) {
|
|
116
|
+
console.error("drift: Download failed.", e.message);
|
|
117
|
+
console.error("drift: Run the engine manually or set DRIFT_ENGINE_BASE_URL.");
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
if (process.platform !== "win32") {
|
|
121
|
+
try { fs.chmodSync(binPath, 0o755); } catch (_) {}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
const child = spawn(binPath, [], {
|
|
125
|
+
detached: true,
|
|
126
|
+
stdio: "ignore",
|
|
127
|
+
cwd: binDir,
|
|
128
|
+
env: { ...process.env, DRIFT_ENGINE_PORT: ENGINE_PORT },
|
|
129
|
+
});
|
|
130
|
+
child.unref();
|
|
131
|
+
return waitForEngine();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function main() {
|
|
135
|
+
const userBackend = process.env.DRIFT_BACKEND_URL;
|
|
136
|
+
if (userBackend) {
|
|
137
|
+
// User runs engine elsewhere; skip download/start and connect to their URL.
|
|
138
|
+
} else if (await engineRunning()) {
|
|
139
|
+
// Engine already running locally.
|
|
140
|
+
} else {
|
|
141
|
+
const engineStarted = await ensureEngine().catch(() => false);
|
|
142
|
+
if (!engineStarted) {
|
|
143
|
+
console.error("drift: Engine did not start. Run the engine manually or set DRIFT_BACKEND_URL to a running engine URL.");
|
|
144
|
+
process.exit(1);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const python = which("python3") || which("python");
|
|
149
|
+
if (!python) {
|
|
150
|
+
console.error("drift: Python is required for the chat CLI. Install Python 3 and ensure `python` or `python3` is on your PATH.");
|
|
151
|
+
process.exit(1);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const pip = spawnSync(python, ["-m", "pip", "install", "--quiet", "--upgrade", "drift"], {
|
|
155
|
+
encoding: "utf8",
|
|
156
|
+
stdio: ["inherit", "pipe", "inherit"],
|
|
157
|
+
});
|
|
158
|
+
if (pip.status !== 0) {
|
|
159
|
+
console.error("drift: Failed to install or upgrade the drift Python package.");
|
|
160
|
+
process.exit(pip.status === null ? 1 : pip.status);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const backendUrl = userBackend || `http://127.0.0.1:${ENGINE_PORT}`;
|
|
164
|
+
const run = spawnSync(python, ["-m", "drift"], {
|
|
165
|
+
encoding: "utf8",
|
|
166
|
+
stdio: "inherit",
|
|
167
|
+
env: { ...process.env, DRIFT_BACKEND_URL: backendUrl },
|
|
168
|
+
});
|
|
169
|
+
process.exit(run.status === null ? (run.signal ? 128 + 9 : 1) : run.status);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
main().catch((e) => {
|
|
173
|
+
console.error("drift:", e.message);
|
|
174
|
+
process.exit(1);
|
|
175
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "drift-ml",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Drift — terminal-first, chat-based AutoML. Same engine as the web app. On first run: downloads and starts the engine locally (never exposes engine source).",
|
|
5
|
+
"bin": {
|
|
6
|
+
"drift": "./bin/drift.js"
|
|
7
|
+
},
|
|
8
|
+
"engines": {
|
|
9
|
+
"node": ">=18"
|
|
10
|
+
},
|
|
11
|
+
"keywords": ["drift", "automl", "cli", "ml", "terminal", "local-first"],
|
|
12
|
+
"license": "MIT"
|
|
13
|
+
}
|