@macaw-ai/client 0.9.10 → 0.9.12
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/bin/install.js +215 -27
- package/package.json +2 -2
package/bin/install.js
CHANGED
|
@@ -65,24 +65,59 @@ const byVersionDesc = (a, b) => {
|
|
|
65
65
|
|
|
66
66
|
// ---------------------------------------------------------------- python ---
|
|
67
67
|
|
|
68
|
-
|
|
69
|
-
|
|
68
|
+
// An interpreter is a command plus leading arguments, because the reliable way
|
|
69
|
+
// to name a version on Windows is the launcher: `py -3.14`. Everything that
|
|
70
|
+
// runs Python here goes through this shape.
|
|
71
|
+
const interp = (cmd, ...pre) => ({ cmd, pre, display: [cmd, ...pre].join(" ") });
|
|
72
|
+
|
|
73
|
+
function pythonVersionOf(py) {
|
|
74
|
+
const r = spawnSync(py.cmd, [...py.pre, "-c", "import sys;print('%d.%d' % sys.version_info[:2])"],
|
|
70
75
|
{ encoding: "utf8" });
|
|
71
76
|
return r.status === 0 ? r.stdout.trim() : null;
|
|
72
77
|
}
|
|
73
78
|
|
|
79
|
+
// How to ask for a specific version, per platform. Unix names the binary;
|
|
80
|
+
// Windows has no python3.14 on PATH and uses the launcher instead, with bare
|
|
81
|
+
// `python` as the fallback - `python3` there is often a Store alias that is
|
|
82
|
+
// absent or stubbed.
|
|
83
|
+
function candidatesFor(v) {
|
|
84
|
+
const minor = v.split(".")[1];
|
|
85
|
+
return process.platform === "win32"
|
|
86
|
+
? [interp("py", `-${v}`), interp(`python${v}`), interp("python")]
|
|
87
|
+
: [interp(`python${v}`), interp(`python3.${minor}`)];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const defaultInterp = () => (process.platform === "win32" ? interp("python") : interp("python3"));
|
|
91
|
+
|
|
74
92
|
// Newest first, so a machine with several gets the one we would rather it use.
|
|
93
|
+
//
|
|
94
|
+
// MACAW_PYTHON names an interpreter outright, arguments included, so
|
|
95
|
+
// MACAW_PYTHON="py -3.12" works. That is the way out of the mismatch this
|
|
96
|
+
// preference can cause: on a machine where the default is 3.12 and 3.14 also
|
|
97
|
+
// exists, the library lands where `python3 foo.py` cannot import it.
|
|
75
98
|
function findPython(wheels) {
|
|
76
99
|
const wanted = [...new Set(wheels.map((w) => version(w.tag)))].sort(byVersionDesc);
|
|
77
100
|
|
|
101
|
+
const named = process.env.MACAW_PYTHON;
|
|
102
|
+
if (named) {
|
|
103
|
+
const py = interp(...named.trim().split(/\s+/));
|
|
104
|
+
const v = pythonVersionOf(py);
|
|
105
|
+
if (!v) return { error: `MACAW_PYTHON=${named} will not run.` };
|
|
106
|
+
if (!wanted.includes(v)) {
|
|
107
|
+
return { error: `MACAW_PYTHON=${named} is Python ${v}. We ship ${wanted.join(", ")}.` };
|
|
108
|
+
}
|
|
109
|
+
return { ...py, version: v };
|
|
110
|
+
}
|
|
111
|
+
|
|
78
112
|
for (const v of wanted) {
|
|
79
|
-
for (const
|
|
80
|
-
if (pythonVersionOf(
|
|
113
|
+
for (const py of candidatesFor(v)) {
|
|
114
|
+
if (pythonVersionOf(py) === v) return { ...py, version: v };
|
|
81
115
|
}
|
|
82
116
|
}
|
|
83
|
-
// Whatever
|
|
84
|
-
const
|
|
85
|
-
|
|
117
|
+
// Whatever the bare interpreter is may still be one we ship for.
|
|
118
|
+
const d = defaultInterp();
|
|
119
|
+
const v = pythonVersionOf(d);
|
|
120
|
+
if (v && wanted.includes(v)) return { ...d, version: v };
|
|
86
121
|
return null;
|
|
87
122
|
}
|
|
88
123
|
|
|
@@ -98,25 +133,81 @@ function findPython(wheels) {
|
|
|
98
133
|
// and "STRONGLY recommend that you additionally pass --user". The pair is the
|
|
99
134
|
// point: --user writes to ~/.local and never into the managed prefix, which is
|
|
100
135
|
// the thing the error exists to protect.
|
|
101
|
-
function pipInstall(
|
|
136
|
+
function pipInstall(py, files) {
|
|
102
137
|
const base = ["-m", "pip", "install", "--disable-pip-version-check", "-q"];
|
|
103
138
|
const run = (args) =>
|
|
104
|
-
spawnSync(
|
|
139
|
+
spawnSync(py.cmd, [...py.pre, ...base, ...args, ...files],
|
|
140
|
+
{ stdio: ["ignore", "pipe", "pipe"], encoding: "utf8" });
|
|
105
141
|
|
|
106
|
-
const inVenv = spawnSync(
|
|
142
|
+
const inVenv = spawnSync(py.cmd, [...py.pre, "-c", "import sys;print(sys.prefix!=sys.base_prefix)"],
|
|
107
143
|
{ encoding: "utf8" }).stdout?.trim() === "True";
|
|
108
144
|
|
|
145
|
+
// Windows has no externally-managed interpreters and no ~/.local; --user
|
|
146
|
+
// there writes to %APPDATA%\Python, which is what we want and is the default
|
|
147
|
+
// behaviour, so the wording follows the platform.
|
|
148
|
+
const userDir = process.platform === "win32" ? "your user site-packages" : "~/.local";
|
|
149
|
+
|
|
109
150
|
let r = run(inVenv ? [] : ["--user"]);
|
|
110
|
-
if (r.status === 0) return { ok: true, where: inVenv ? "the active virtualenv" :
|
|
151
|
+
if (r.status === 0) return { ok: true, where: inVenv ? "the active virtualenv" : userDir };
|
|
111
152
|
|
|
112
153
|
if (/externally-managed-environment/i.test(r.stderr || "")) {
|
|
113
|
-
info(
|
|
154
|
+
info(`this Python is externally managed - installing to ${userDir}, which leaves it untouched`);
|
|
114
155
|
r = run(["--user", "--break-system-packages"]);
|
|
115
|
-
if (r.status === 0) return { ok: true, where:
|
|
156
|
+
if (r.status === 0) return { ok: true, where: userDir };
|
|
116
157
|
}
|
|
117
158
|
return { ok: false, err: (r.stderr || r.stdout || "").trim() };
|
|
118
159
|
}
|
|
119
160
|
|
|
161
|
+
// ------------------------------------------------------------------ key ---
|
|
162
|
+
|
|
163
|
+
// Read from four places, write to one. The last two are read and never
|
|
164
|
+
// written: a key already on the machine joins that workspace instead of
|
|
165
|
+
// minting a second, and a config kept in ~/.macaw by hand stays as it is.
|
|
166
|
+
//
|
|
167
|
+
// The current directory is in the list because the coding-agents installer
|
|
168
|
+
// writes $PWD/.macaw/config.json, so `npx macaw-ai install` choosing both
|
|
169
|
+
// surfaces activates once rather than twice.
|
|
170
|
+
function existingKey(dir) {
|
|
171
|
+
// --activate asks for a new workspace outright. Without it the only way to
|
|
172
|
+
// re-key was to delete this install's config, which the next run refills
|
|
173
|
+
// from $HOME or the current directory - so the instruction looped and anyone
|
|
174
|
+
// who had ever used MACAW could not reach activation. Nothing is deleted
|
|
175
|
+
// here; the existing files are simply not read.
|
|
176
|
+
if (process.argv.includes("--activate")) return null;
|
|
177
|
+
|
|
178
|
+
// Handed to us rather than discovered, so there is nothing to confirm. This
|
|
179
|
+
// is how `macaw-ai install` choosing both surfaces passes the key the first
|
|
180
|
+
// installer just created, instead of asking for it a second time.
|
|
181
|
+
const fromEnv = process.env.MACAW_API_KEY;
|
|
182
|
+
if (fromEnv) {
|
|
183
|
+
return {
|
|
184
|
+
key: fromEnv.trim(),
|
|
185
|
+
from: "MACAW_API_KEY",
|
|
186
|
+
workspace: process.env.MACAW_WORKSPACE || "",
|
|
187
|
+
explicit: true,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const places = [
|
|
192
|
+
process.env.MACAW_HOME && path.join(process.env.MACAW_HOME, ".macaw", "config.json"),
|
|
193
|
+
path.join(dir, ".macaw", "config.json"),
|
|
194
|
+
path.join(process.cwd(), ".macaw", "config.json"),
|
|
195
|
+
path.join(os.homedir(), ".macaw", "config.json"),
|
|
196
|
+
].filter(Boolean);
|
|
197
|
+
|
|
198
|
+
for (const p of places) {
|
|
199
|
+
try {
|
|
200
|
+
const cfg = JSON.parse(fs.readFileSync(p, "utf8"));
|
|
201
|
+
if (typeof cfg?.api_key === "string" && cfg.api_key.startsWith("mw_")) {
|
|
202
|
+
return { key: cfg.api_key, from: p, workspace: cfg.workspace || "" };
|
|
203
|
+
}
|
|
204
|
+
} catch {
|
|
205
|
+
// Unreadable or not JSON. Try the next one.
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
|
|
120
211
|
// ------------------------------------------------------------ activation ---
|
|
121
212
|
|
|
122
213
|
async function activate() {
|
|
@@ -163,7 +254,24 @@ async function activate() {
|
|
|
163
254
|
|
|
164
255
|
// ------------------------------------------------------------------ main ---
|
|
165
256
|
|
|
257
|
+
function usage() {
|
|
258
|
+
console.log("");
|
|
259
|
+
console.log(`${C.b} macaw-client${C.x} - the MACAW SDK and the secureAI adapters`);
|
|
260
|
+
console.log("");
|
|
261
|
+
console.log(" macaw-client install pick a Python, install the wheels,");
|
|
262
|
+
console.log(" lay down the examples");
|
|
263
|
+
console.log(" macaw-client install --activate the same, with a new workspace");
|
|
264
|
+
console.log(" macaw-client --check report what it would do, change nothing");
|
|
265
|
+
console.log("");
|
|
266
|
+
process.exit(1);
|
|
267
|
+
}
|
|
268
|
+
|
|
166
269
|
async function main() {
|
|
270
|
+
// A verb, like `macaw-sidecar install`. Bare, this used to pip install into
|
|
271
|
+
// an interpreter and write a directory with nothing asked for.
|
|
272
|
+
const argv = process.argv.slice(2);
|
|
273
|
+
if (!argv.includes("--check") && !argv.includes("install")) usage();
|
|
274
|
+
|
|
167
275
|
console.log("");
|
|
168
276
|
console.log(`${C.b} MACAW${C.x}`);
|
|
169
277
|
console.log("");
|
|
@@ -183,12 +291,23 @@ async function main() {
|
|
|
183
291
|
// Useful before installing, and the first thing to ask when it did not work.
|
|
184
292
|
if (process.argv.includes("--check")) {
|
|
185
293
|
const py = findPython(usable);
|
|
294
|
+
const d = defaultInterp();
|
|
295
|
+
const dflt = pythonVersionOf(d);
|
|
186
296
|
console.log(` platform ${process.platform}/${process.arch} → ${plat}`);
|
|
187
297
|
const shipped = [...new Set(usable.map((w) => version(w.tag)))].sort(byVersionDesc);
|
|
188
298
|
console.log(` we ship ${shipped.map((v) => "Python " + v).join(", ")}`);
|
|
189
|
-
console.log(
|
|
299
|
+
console.log(` ${d.display.padEnd(9)} ${dflt || "not found"}`);
|
|
300
|
+
console.log(py?.error ? ` would use ${py.error}`
|
|
301
|
+
: py ? ` would use ${py.display} (${py.version})`
|
|
302
|
+
: ` would use nothing we have a build for`);
|
|
303
|
+
if (py && !py.error && dflt && dflt !== py.version) {
|
|
304
|
+
console.log(` ${C.y}⚠${C.x} your ${d.display} cannot import what that installs.`);
|
|
305
|
+
console.log(` MACAW_PYTHON=${d.display} to install into it instead`);
|
|
306
|
+
}
|
|
307
|
+
const haveKey = existingKey(path.resolve("./macaw"));
|
|
308
|
+
console.log(` key ${haveKey ? "reusing " + haveKey.from : "none found; would activate"}`);
|
|
190
309
|
console.log(` endpoint ${API}`);
|
|
191
|
-
process.exit(py ? 0 : 1);
|
|
310
|
+
process.exit(py && !py.error ? 0 : 1);
|
|
192
311
|
}
|
|
193
312
|
|
|
194
313
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
@@ -206,22 +325,76 @@ async function main() {
|
|
|
206
325
|
|
|
207
326
|
// 2 — python
|
|
208
327
|
const py = findPython(usable);
|
|
209
|
-
if (!py) {
|
|
210
|
-
no("No Python here that we have a build for.");
|
|
328
|
+
if (!py || py.error) {
|
|
329
|
+
no(py?.error || "No Python here that we have a build for.");
|
|
211
330
|
info(`On ${plat} we ship: ${[...new Set(usable.map((w) => version(w.tag)))].sort(byVersionDesc).join(", ")}`);
|
|
212
331
|
rl.close();
|
|
213
332
|
process.exit(1);
|
|
214
333
|
}
|
|
215
|
-
ok(`Python ${py.version}`);
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
334
|
+
ok(`Python ${py.version} (${py.display})`);
|
|
335
|
+
|
|
336
|
+
// We prefer the newest interpreter we have a wheel for, which is not always
|
|
337
|
+
// the one a bare `python3` resolves to. When they differ the install succeeds
|
|
338
|
+
// and the user's default interpreter still cannot import the library, so say
|
|
339
|
+
// so here rather than letting them find out from a traceback.
|
|
340
|
+
const dflt = defaultInterp();
|
|
341
|
+
const dfltVersion = pythonVersionOf(dflt);
|
|
342
|
+
if (dfltVersion && dfltVersion !== py.version) {
|
|
343
|
+
console.log(` ${C.y}⚠${C.x} Your ${dflt.display} is ${dfltVersion}; this installs into Python ${py.version}.`);
|
|
344
|
+
console.log(` Run MACAW code with ${C.b}${py.display}${C.x}, or install into ${dflt.display} instead:`);
|
|
345
|
+
console.log(` ${C.c}MACAW_PYTHON=${dflt.display} npx @macaw-ai/client install${C.x}`);
|
|
219
346
|
}
|
|
220
347
|
|
|
221
|
-
// 3 —
|
|
348
|
+
// 3 — the workspace. The same three choices, in the same words, as the
|
|
349
|
+
// coding-agents installer: the step that confused people was this one
|
|
350
|
+
// deciding for them and saying so afterwards.
|
|
222
351
|
console.log("");
|
|
223
|
-
|
|
224
|
-
|
|
352
|
+
console.log(`${C.b} Workspace${C.x}`);
|
|
353
|
+
const found = existingKey(dir);
|
|
354
|
+
let workspace = found?.workspace || "";
|
|
355
|
+
let key;
|
|
356
|
+
|
|
357
|
+
if (found?.explicit) {
|
|
358
|
+
// Handed to us rather than discovered - nothing to confirm.
|
|
359
|
+
key = found.key;
|
|
360
|
+
ok(found.workspace ? `Same workspace: ${found.workspace}` : "Using the key passed in");
|
|
361
|
+
} else if (!process.stdin.isTTY) {
|
|
362
|
+
if (!found) {
|
|
363
|
+
no("No key found, and nothing is attached to answer.");
|
|
364
|
+
info("Pass one: MACAW_API_KEY=mw_... npx @macaw-ai/client install");
|
|
365
|
+
rl.close();
|
|
366
|
+
process.exit(1);
|
|
367
|
+
}
|
|
368
|
+
key = found.key;
|
|
369
|
+
ok(found.from);
|
|
370
|
+
} else {
|
|
371
|
+
console.log("");
|
|
372
|
+
console.log(" 1) Use a key already on this machine");
|
|
373
|
+
console.log(" 2) Paste an API key");
|
|
374
|
+
console.log(" 3) Create a new workspace");
|
|
375
|
+
console.log("");
|
|
376
|
+
const choice = (await ask(" Choose [1]: ", "1")) || "1";
|
|
377
|
+
|
|
378
|
+
if (choice === "1" && found) {
|
|
379
|
+
key = found.key;
|
|
380
|
+
ok(found.from);
|
|
381
|
+
} else if (choice === "1") {
|
|
382
|
+
console.log(` ${C.y}⚠${C.x} No key found.`);
|
|
383
|
+
console.log(" Looked in: $MACAW_API_KEY, $MACAW_HOME, this directory, ~/.macaw");
|
|
384
|
+
console.log(" Creating a new workspace instead.");
|
|
385
|
+
key = "";
|
|
386
|
+
} else if (choice === "2") {
|
|
387
|
+
key = await ask(" API Key: ", "");
|
|
388
|
+
if (!key) { no("Nothing entered."); rl.close(); process.exit(1); }
|
|
389
|
+
} else if (choice === "3") {
|
|
390
|
+
key = "";
|
|
391
|
+
} else {
|
|
392
|
+
no("Not a choice.");
|
|
393
|
+
rl.close();
|
|
394
|
+
process.exit(1);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
225
398
|
if (key === "") {
|
|
226
399
|
const got = await activate();
|
|
227
400
|
if (!got) { rl.close(); process.exit(1); }
|
|
@@ -242,9 +415,21 @@ async function main() {
|
|
|
242
415
|
? fs.readdirSync(path.join(PKG, "wheels")).filter((f) => f.startsWith("macaw_adapters-"))
|
|
243
416
|
.map((f) => path.join(PKG, "wheels", f))
|
|
244
417
|
: [];
|
|
245
|
-
const r = pipInstall(py
|
|
418
|
+
const r = pipInstall(py, [wheel.file, ...adapters]);
|
|
246
419
|
if (!r.ok) { no("pip install failed"); console.log(r.err); process.exit(1); }
|
|
247
|
-
|
|
420
|
+
|
|
421
|
+
// pip exiting 0 does not mean the library is importable: a --user install
|
|
422
|
+
// whose site directory is not on this interpreter's sys.path passes, and so
|
|
423
|
+
// does one shadowed by an older copy earlier on the path. Ask the question
|
|
424
|
+
// the user will ask.
|
|
425
|
+
const imported = spawnSync(py.cmd, [...py.pre, "-c", "import macaw_client, macaw_adapters"],
|
|
426
|
+
{ encoding: "utf8" });
|
|
427
|
+
if (imported.status !== 0) {
|
|
428
|
+
no(`pip reported success, but ${py.display} cannot import what it installed.`);
|
|
429
|
+
console.log((imported.stderr || imported.stdout || "").trim());
|
|
430
|
+
process.exit(1);
|
|
431
|
+
}
|
|
432
|
+
ok(`macaw-client and macaw-adapters installed into ${r.where}, importable by ${py.display}`);
|
|
248
433
|
|
|
249
434
|
// 5 — the tree, and the config beside it
|
|
250
435
|
// Whatever build.sh assembled, verbatim. A list here would be a second
|
|
@@ -253,6 +438,9 @@ async function main() {
|
|
|
253
438
|
if (fs.existsSync(payload)) fs.cpSync(payload, dir, { recursive: true });
|
|
254
439
|
const cfgDir = path.join(dir, ".macaw");
|
|
255
440
|
fs.mkdirSync(cfgDir, { recursive: true, mode: 0o700 });
|
|
441
|
+
// The config holds an API key and the install directory may be inside a
|
|
442
|
+
// repository.
|
|
443
|
+
fs.writeFileSync(path.join(cfgDir, ".gitignore"), "*\n!.gitignore\n");
|
|
256
444
|
fs.writeFileSync(path.join(cfgDir, "config.json"), JSON.stringify({
|
|
257
445
|
transport: "http",
|
|
258
446
|
http_enabled: true,
|
|
@@ -267,7 +455,7 @@ async function main() {
|
|
|
267
455
|
console.log("");
|
|
268
456
|
console.log(`${C.b} Check it${C.x}`);
|
|
269
457
|
console.log(` cd ${dir}`);
|
|
270
|
-
console.log(` ${py.
|
|
458
|
+
console.log(` ${py.display} secureAI/test_harness_quick.py`);
|
|
271
459
|
console.log("");
|
|
272
460
|
console.log(` ${C.dim}To use MACAW from other directories, point MACAW_HOME at${C.x}`);
|
|
273
461
|
console.log(` ${C.dim}this directory in your shell:${C.x}`);
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@macaw-ai/client",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.12",
|
|
4
4
|
"description": "Set up the MACAW client for a Python project: installs the SDK and adapters, creates a workspace, leaves the examples ready to run.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"macaw-client
|
|
7
|
+
"macaw-client": "bin/install.js"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"bin/install.js",
|