@alva-ai/toolkit 0.1.3 → 0.2.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 +1 -1
- package/dist/browser.global.js +1 -1
- package/dist/browser.global.js.map +1 -1
- package/dist/cli.js +567 -53
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +106 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +170 -1
- package/dist/index.d.ts +170 -1
- package/dist/index.js +106 -4
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -11,6 +11,14 @@ var AlvaError = class extends Error {
|
|
|
11
11
|
this.status = status;
|
|
12
12
|
}
|
|
13
13
|
};
|
|
14
|
+
var CliUsageError = class extends Error {
|
|
15
|
+
command;
|
|
16
|
+
constructor(message, command) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.name = "CliUsageError";
|
|
19
|
+
this.command = command;
|
|
20
|
+
}
|
|
21
|
+
};
|
|
14
22
|
|
|
15
23
|
// src/resources/fs.ts
|
|
16
24
|
var FsResource = class {
|
|
@@ -200,6 +208,23 @@ var DeployResource = class {
|
|
|
200
208
|
`/api/v1/deploy/cronjob/${params.id}/resume`
|
|
201
209
|
);
|
|
202
210
|
}
|
|
211
|
+
async listRuns(params) {
|
|
212
|
+
this.client._requireAuth();
|
|
213
|
+
return this.client._request(
|
|
214
|
+
"GET",
|
|
215
|
+
`/api/v1/deploy/cronjob/${params.cronjob_id}/runs`,
|
|
216
|
+
{
|
|
217
|
+
query: { first: params.first, cursor: params.cursor }
|
|
218
|
+
}
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
async getRunLogs(params) {
|
|
222
|
+
this.client._requireAuth();
|
|
223
|
+
return this.client._request(
|
|
224
|
+
"GET",
|
|
225
|
+
`/api/v1/deploy/cronjob/${params.cronjob_id}/runs/${params.run_id}/logs`
|
|
226
|
+
);
|
|
227
|
+
}
|
|
203
228
|
};
|
|
204
229
|
|
|
205
230
|
// src/resources/release.ts
|
|
@@ -391,10 +416,88 @@ var UserResource = class {
|
|
|
391
416
|
}
|
|
392
417
|
};
|
|
393
418
|
|
|
419
|
+
// src/resources/trading.ts
|
|
420
|
+
var TradingResource = class {
|
|
421
|
+
constructor(client) {
|
|
422
|
+
this.client = client;
|
|
423
|
+
}
|
|
424
|
+
client;
|
|
425
|
+
async accounts() {
|
|
426
|
+
this.client._requireAuth();
|
|
427
|
+
return this.client._request("GET", "/api/v1/trading/accounts");
|
|
428
|
+
}
|
|
429
|
+
async portfolio(accountId) {
|
|
430
|
+
this.client._requireAuth();
|
|
431
|
+
return this.client._request("GET", "/api/v1/trading/portfolio", {
|
|
432
|
+
query: { accountId }
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
async orders(params) {
|
|
436
|
+
this.client._requireAuth();
|
|
437
|
+
return this.client._request("GET", "/api/v1/trading/orders", {
|
|
438
|
+
query: {
|
|
439
|
+
accountId: params.accountId,
|
|
440
|
+
source: params.source,
|
|
441
|
+
since: params.since,
|
|
442
|
+
limit: params.limit
|
|
443
|
+
}
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
async subscriptions(accountId) {
|
|
447
|
+
this.client._requireAuth();
|
|
448
|
+
return this.client._request("GET", "/api/v1/trading/subscriptions", {
|
|
449
|
+
query: { accountId }
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
async equityHistory(params) {
|
|
453
|
+
this.client._requireAuth();
|
|
454
|
+
return this.client._request("GET", "/api/v1/trading/equity-history", {
|
|
455
|
+
query: {
|
|
456
|
+
accountId: params.accountId,
|
|
457
|
+
timeframe: params.timeframe,
|
|
458
|
+
sinceMs: params.sinceMs,
|
|
459
|
+
untilMs: params.untilMs
|
|
460
|
+
}
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
async riskRules() {
|
|
464
|
+
this.client._requireAuth();
|
|
465
|
+
return this.client._request(
|
|
466
|
+
"GET",
|
|
467
|
+
"/api/v1/trading/risk-rules"
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
async subscribe(params) {
|
|
471
|
+
this.client._requireAuth();
|
|
472
|
+
return this.client._request("POST", "/api/v1/trading/subscribe", {
|
|
473
|
+
body: params
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
async unsubscribe(subscriptionId) {
|
|
477
|
+
this.client._requireAuth();
|
|
478
|
+
return this.client._request("POST", "/api/v1/trading/unsubscribe", {
|
|
479
|
+
body: { subscriptionId }
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
async execute(params) {
|
|
483
|
+
this.client._requireAuth();
|
|
484
|
+
return this.client._request("POST", "/api/v1/trading/execute", {
|
|
485
|
+
body: params
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
async updateRiskRules(rules) {
|
|
489
|
+
this.client._requireAuth();
|
|
490
|
+
return this.client._request("PUT", "/api/v1/trading/risk-rules", {
|
|
491
|
+
body: rules
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
};
|
|
495
|
+
|
|
394
496
|
// src/client.ts
|
|
395
497
|
var DEFAULT_BASE_URL = "https://api-llm.prd.alva.ai";
|
|
396
498
|
var AlvaClient = class {
|
|
397
499
|
baseUrl;
|
|
500
|
+
token;
|
|
398
501
|
apiKey;
|
|
399
502
|
_fs;
|
|
400
503
|
_run;
|
|
@@ -406,8 +509,10 @@ var AlvaClient = class {
|
|
|
406
509
|
_remix;
|
|
407
510
|
_screenshot;
|
|
408
511
|
_user;
|
|
512
|
+
_trading;
|
|
409
513
|
constructor(config) {
|
|
410
514
|
this.baseUrl = config.baseUrl ?? DEFAULT_BASE_URL;
|
|
515
|
+
this.token = config.token;
|
|
411
516
|
this.apiKey = config.apiKey;
|
|
412
517
|
}
|
|
413
518
|
get fs() {
|
|
@@ -440,11 +545,14 @@ var AlvaClient = class {
|
|
|
440
545
|
get user() {
|
|
441
546
|
return this._user ??= new UserResource(this);
|
|
442
547
|
}
|
|
548
|
+
get trading() {
|
|
549
|
+
return this._trading ??= new TradingResource(this);
|
|
550
|
+
}
|
|
443
551
|
_requireAuth() {
|
|
444
|
-
if (!this.apiKey) {
|
|
552
|
+
if (!this.token && !this.apiKey) {
|
|
445
553
|
throw new AlvaError(
|
|
446
554
|
"UNAUTHENTICATED",
|
|
447
|
-
"
|
|
555
|
+
"Authentication is required. Pass token or apiKey in the constructor.",
|
|
448
556
|
401
|
|
449
557
|
);
|
|
450
558
|
}
|
|
@@ -464,7 +572,9 @@ var AlvaClient = class {
|
|
|
464
572
|
}
|
|
465
573
|
}
|
|
466
574
|
const headers = {};
|
|
467
|
-
if (this.
|
|
575
|
+
if (this.token) {
|
|
576
|
+
headers.Authorization = `${this.token}`;
|
|
577
|
+
} else if (this.apiKey) {
|
|
468
578
|
headers["X-Alva-Api-Key"] = this.apiKey;
|
|
469
579
|
}
|
|
470
580
|
let fetchBody;
|
|
@@ -596,16 +706,16 @@ function parseFlag(argv, flag) {
|
|
|
596
706
|
return void 0;
|
|
597
707
|
}
|
|
598
708
|
function loadConfig(deps) {
|
|
599
|
-
const { argv, env, readFile:
|
|
709
|
+
const { argv, env, readFile: readFile3, homedir: homedir3 } = deps;
|
|
600
710
|
const profileName = parseFlag(argv, "--profile") || env.ALVA_PROFILE || "default";
|
|
601
711
|
const baseUrlFlag = parseFlag(argv, "--base-url");
|
|
602
712
|
const baseUrlEnv = env.ALVA_ENDPOINT;
|
|
603
713
|
const apiKeyFlag = parseFlag(argv, "--api-key");
|
|
604
714
|
const apiKeyEnv = env.ALVA_API_KEY;
|
|
605
715
|
let fileProfile = {};
|
|
606
|
-
const path = configPath({ env, homedir:
|
|
716
|
+
const path = configPath({ env, homedir: homedir3 });
|
|
607
717
|
try {
|
|
608
|
-
const raw =
|
|
718
|
+
const raw = readFile3(path);
|
|
609
719
|
let config;
|
|
610
720
|
try {
|
|
611
721
|
config = readConfigFile(raw);
|
|
@@ -625,11 +735,135 @@ function loadConfig(deps) {
|
|
|
625
735
|
};
|
|
626
736
|
}
|
|
627
737
|
|
|
628
|
-
// src/cli/
|
|
629
|
-
import * as
|
|
738
|
+
// src/cli/auth.ts
|
|
739
|
+
import * as crypto from "crypto";
|
|
740
|
+
import * as http from "http";
|
|
741
|
+
import { exec } from "child_process";
|
|
630
742
|
import * as os from "os";
|
|
631
743
|
import * as fsPromises from "fs/promises";
|
|
632
|
-
|
|
744
|
+
function generateState() {
|
|
745
|
+
return crypto.randomBytes(32).toString("hex");
|
|
746
|
+
}
|
|
747
|
+
function parseFlags(argv) {
|
|
748
|
+
const flags = {};
|
|
749
|
+
for (let i = 0; i < argv.length; i++) {
|
|
750
|
+
const arg = argv[i];
|
|
751
|
+
if (arg.startsWith("--")) {
|
|
752
|
+
const eqIdx = arg.indexOf("=");
|
|
753
|
+
if (eqIdx !== -1) {
|
|
754
|
+
flags[arg.slice(2, eqIdx)] = arg.slice(eqIdx + 1);
|
|
755
|
+
} else if (i + 1 < argv.length && !argv[i + 1].startsWith("--")) {
|
|
756
|
+
flags[arg.slice(2)] = argv[i + 1];
|
|
757
|
+
i++;
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
return flags;
|
|
762
|
+
}
|
|
763
|
+
function defaultOpenBrowser(url) {
|
|
764
|
+
return new Promise((resolve) => {
|
|
765
|
+
const platform = process.platform;
|
|
766
|
+
let cmd;
|
|
767
|
+
if (platform === "darwin") {
|
|
768
|
+
cmd = `open "${url}"`;
|
|
769
|
+
} else if (platform === "win32") {
|
|
770
|
+
cmd = `start "${url}"`;
|
|
771
|
+
} else {
|
|
772
|
+
cmd = `xdg-open "${url}"`;
|
|
773
|
+
}
|
|
774
|
+
exec(cmd, () => {
|
|
775
|
+
resolve();
|
|
776
|
+
});
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
function defaultDeps() {
|
|
780
|
+
return {
|
|
781
|
+
generateState,
|
|
782
|
+
openBrowser: defaultOpenBrowser,
|
|
783
|
+
writeConfigDeps: {
|
|
784
|
+
env: process.env,
|
|
785
|
+
homedir: () => os.homedir(),
|
|
786
|
+
mkdir: (path, options) => fsPromises.mkdir(path, options).then(() => void 0),
|
|
787
|
+
writeFile: (path, data, options) => fsPromises.writeFile(path, data, options).then(() => void 0),
|
|
788
|
+
readFile: (path) => fsPromises.readFile(path, "utf-8")
|
|
789
|
+
},
|
|
790
|
+
createServer: (handler) => http.createServer(handler),
|
|
791
|
+
timeout: 12e4,
|
|
792
|
+
log: (msg) => process.stderr.write(msg)
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
async function handleAuthLogin(args, deps) {
|
|
796
|
+
const d = { ...defaultDeps(), ...deps };
|
|
797
|
+
const flags = parseFlags(args.slice(1));
|
|
798
|
+
const profileName = flags.profile || "default";
|
|
799
|
+
const authUrl = flags["auth-url"] || "https://alva.ai";
|
|
800
|
+
const timeout = d.timeout ?? 12e4;
|
|
801
|
+
const state = d.generateState();
|
|
802
|
+
return new Promise((resolve, reject) => {
|
|
803
|
+
const server = d.createServer((req, res) => {
|
|
804
|
+
const reqUrl = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
805
|
+
if (reqUrl.pathname !== "/callback") {
|
|
806
|
+
res.writeHead(404);
|
|
807
|
+
res.end("Not found");
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
const callbackState = reqUrl.searchParams.get("state");
|
|
811
|
+
const apiKey = reqUrl.searchParams.get("api_key");
|
|
812
|
+
if (callbackState !== state) {
|
|
813
|
+
res.writeHead(400);
|
|
814
|
+
res.end(
|
|
815
|
+
"<html><body><h1>Error</h1><p>State mismatch. Please try again.</p></body></html>"
|
|
816
|
+
);
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
if (!apiKey) {
|
|
820
|
+
res.writeHead(400);
|
|
821
|
+
res.end(
|
|
822
|
+
"<html><body><h1>Error</h1><p>Missing API key. Please try again.</p></body></html>"
|
|
823
|
+
);
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
827
|
+
res.end(
|
|
828
|
+
`<html><body style="display:flex;align-items:center;justify-content:center;height:100vh;margin:0;background:rgba(246,246,246,1);font-family:system-ui,sans-serif"><div style="text-align:center;display:flex;flex-direction:column;align-items:center;gap:40px"><h1 style="font-size:45px;font-weight:400;line-height:120%;margin:0">Turn Ideas into Live<br>Investing Playbooks in Minutes</h1><p style="font-size:24px;font-weight:400;margin:0">You're all set for Alva.</p></div></body></html>`
|
|
829
|
+
);
|
|
830
|
+
server.close();
|
|
831
|
+
clearTimeout(timer);
|
|
832
|
+
writeConfig({ apiKey }, d.writeConfigDeps, profileName).then(() => {
|
|
833
|
+
resolve({ status: "logged_in", apiKey, profile: profileName });
|
|
834
|
+
}, reject);
|
|
835
|
+
});
|
|
836
|
+
server.on("error", (err) => {
|
|
837
|
+
clearTimeout(timer);
|
|
838
|
+
reject(err);
|
|
839
|
+
});
|
|
840
|
+
server.listen(0, "127.0.0.1", () => {
|
|
841
|
+
const addr = server.address();
|
|
842
|
+
const callbackUrl = `http://127.0.0.1:${addr.port}/callback`;
|
|
843
|
+
const loginUrl = `${authUrl}/authorize?callback_url=${encodeURIComponent(callbackUrl)}&state=${state}`;
|
|
844
|
+
d.log(
|
|
845
|
+
`Opening browser...
|
|
846
|
+
If it doesn't open, visit:
|
|
847
|
+
${loginUrl}
|
|
848
|
+
|
|
849
|
+
Waiting for login callback...
|
|
850
|
+
`
|
|
851
|
+
);
|
|
852
|
+
d.openBrowser(loginUrl).catch(() => {
|
|
853
|
+
});
|
|
854
|
+
});
|
|
855
|
+
const timer = setTimeout(() => {
|
|
856
|
+
server.close();
|
|
857
|
+
reject(new Error("Login timed out waiting for callback"));
|
|
858
|
+
}, timeout);
|
|
859
|
+
});
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
// src/cli/index.ts
|
|
863
|
+
import * as fs from "fs";
|
|
864
|
+
import * as os2 from "os";
|
|
865
|
+
import * as fsPromises2 from "fs/promises";
|
|
866
|
+
var CLI_VERSION = true ? "0.2.0" : "dev";
|
|
633
867
|
function isVersionOlderThan(a, b) {
|
|
634
868
|
const parse = (v) => {
|
|
635
869
|
if (!v) return null;
|
|
@@ -661,6 +895,8 @@ Commands:
|
|
|
661
895
|
sdk SDK documentation (doc, partitions, partition-summary)
|
|
662
896
|
comments Playbook comments (create, pin, unpin)
|
|
663
897
|
remix Save playbook remix lineage
|
|
898
|
+
trading Trading operations (accounts, portfolio, orders, subscriptions, equity-history, risk-rules, subscribe, unsubscribe, execute, update-risk-rules)
|
|
899
|
+
auth Authentication (login via browser)
|
|
664
900
|
screenshot Capture a web screenshot as PNG
|
|
665
901
|
|
|
666
902
|
Global options:
|
|
@@ -704,6 +940,14 @@ Examples:
|
|
|
704
940
|
alva configure --api-key alva_abc123 --base-url http://localhost:8080
|
|
705
941
|
alva configure --profile staging --api-key alva_stg_key --base-url https://api-llm.stg.alva.ai
|
|
706
942
|
alva --profile staging whoami`,
|
|
943
|
+
auth: `Usage: alva auth <subcommand>
|
|
944
|
+
|
|
945
|
+
Subcommands:
|
|
946
|
+
login Open browser to authenticate and save credentials
|
|
947
|
+
|
|
948
|
+
Examples:
|
|
949
|
+
alva auth login
|
|
950
|
+
alva auth login --profile staging`,
|
|
707
951
|
whoami: `Usage: alva whoami [--profile <name>]
|
|
708
952
|
|
|
709
953
|
Verify that your credentials are valid by calling the Alva API. Shows your
|
|
@@ -791,11 +1035,13 @@ data SDKs, ALFS, HTTP networking, and the Feed SDK.
|
|
|
791
1035
|
|
|
792
1036
|
Options:
|
|
793
1037
|
--code <code> Inline JavaScript code to execute
|
|
1038
|
+
--local-file <path> Path to a local file whose contents are sent as code
|
|
794
1039
|
--entry-path <path> Path to a script file on ALFS (home-relative)
|
|
795
1040
|
--working-dir <dir> Working directory for require() (inline code only)
|
|
796
1041
|
--args <json> JSON object passed to require("env").args
|
|
797
1042
|
|
|
798
|
-
At least one of --code or --entry-path is required.
|
|
1043
|
+
At least one of --code, --local-file, or --entry-path is required.
|
|
1044
|
+
These three options are mutually exclusive.
|
|
799
1045
|
|
|
800
1046
|
Response fields:
|
|
801
1047
|
result JSON-encoded return value of the script
|
|
@@ -822,7 +1068,8 @@ Examples:
|
|
|
822
1068
|
alva run --code "1 + 2 + 3;"
|
|
823
1069
|
alva run --code "JSON.stringify(require('env').args);" --args '{"symbol":"BTC"}'
|
|
824
1070
|
alva run --entry-path ~/feeds/my-feed/v1/src/index.js
|
|
825
|
-
alva run --entry-path ~/tasks/analyze/src/index.js --args '{"symbol":"NVDA","limit":50}'
|
|
1071
|
+
alva run --entry-path ~/tasks/analyze/src/index.js --args '{"symbol":"NVDA","limit":50}'
|
|
1072
|
+
alva run --local-file ./my-script.js --args '{"symbol":"BTC"}'`,
|
|
826
1073
|
deploy: `Usage: alva deploy <subcommand> [options]
|
|
827
1074
|
|
|
828
1075
|
Manage scheduled cronjobs that run your scripts on a cron schedule.
|
|
@@ -871,7 +1118,10 @@ Examples:
|
|
|
871
1118
|
alva deploy update --id 42 --cron "0 */2 * * *" --no-push-notify
|
|
872
1119
|
alva deploy pause --id 42
|
|
873
1120
|
alva deploy resume --id 42
|
|
874
|
-
alva deploy delete --id 42
|
|
1121
|
+
alva deploy delete --id 42
|
|
1122
|
+
alva deploy runs --id 42
|
|
1123
|
+
alva deploy runs --id 42 --first 10
|
|
1124
|
+
alva deploy run-logs --id 42 --run-id 123`,
|
|
875
1125
|
release: `Usage: alva release <subcommand> [options]
|
|
876
1126
|
|
|
877
1127
|
Publish feeds and playbooks to the Alva platform. The typical workflow:
|
|
@@ -1032,15 +1282,79 @@ Optional:
|
|
|
1032
1282
|
|
|
1033
1283
|
Examples:
|
|
1034
1284
|
alva screenshot --url /playbook/alice/btc-dashboard --out dashboard.png
|
|
1035
|
-
alva screenshot --url /playbook/alice/btc-dashboard --out chart.png --selector ".chart-container"
|
|
1285
|
+
alva screenshot --url /playbook/alice/btc-dashboard --out chart.png --selector ".chart-container"`,
|
|
1286
|
+
trading: `Usage: alva trading <subcommand> [options]
|
|
1287
|
+
|
|
1288
|
+
Manage trading accounts, portfolios, orders, subscriptions, and risk rules.
|
|
1289
|
+
|
|
1290
|
+
Subcommands:
|
|
1291
|
+
accounts List all trading accounts
|
|
1292
|
+
portfolio Get portfolio for an account
|
|
1293
|
+
orders List orders for an account
|
|
1294
|
+
subscriptions List subscriptions for an account
|
|
1295
|
+
equity-history Get equity history for an account
|
|
1296
|
+
risk-rules Show risk rules
|
|
1297
|
+
subscribe Subscribe an account to a source feed
|
|
1298
|
+
unsubscribe Unsubscribe by subscription ID
|
|
1299
|
+
execute Execute a signal on an account
|
|
1300
|
+
update-risk-rules Update risk rules
|
|
1301
|
+
|
|
1302
|
+
Portfolio/Orders/Subscriptions/Equity-history flags:
|
|
1303
|
+
--account-id <id> Trading account ID (required)
|
|
1304
|
+
|
|
1305
|
+
Orders optional flags:
|
|
1306
|
+
--limit <n> Max results
|
|
1307
|
+
--source <source> Filter by source
|
|
1308
|
+
--since <timestamp> Filter orders since timestamp
|
|
1309
|
+
|
|
1310
|
+
Equity-history optional flags:
|
|
1311
|
+
--timeframe <tf> Timeframe (e.g. "1d", "1h")
|
|
1312
|
+
--since-ms <ms> Start timestamp in ms
|
|
1313
|
+
--until-ms <ms> End timestamp in ms
|
|
1314
|
+
|
|
1315
|
+
Subscribe flags:
|
|
1316
|
+
--account-id <id> Account ID (required)
|
|
1317
|
+
--source-username <user> Source username (required)
|
|
1318
|
+
--source-feed <feed> Source feed (required)
|
|
1319
|
+
--playbook-id <id> Playbook ID (required)
|
|
1320
|
+
--playbook-version <ver> Playbook version (required)
|
|
1321
|
+
--execute-latest Execute latest signal on subscribe
|
|
1322
|
+
|
|
1323
|
+
Unsubscribe flags:
|
|
1324
|
+
--subscription-id <id> Subscription ID (required)
|
|
1325
|
+
|
|
1326
|
+
Execute flags:
|
|
1327
|
+
--account-id <id> Account ID (required)
|
|
1328
|
+
--signal <json> Signal JSON (required)
|
|
1329
|
+
--dry-run Dry run mode
|
|
1330
|
+
--source-username <user> Source username (optional)
|
|
1331
|
+
--source-feed <feed> Source feed (optional)
|
|
1332
|
+
|
|
1333
|
+
Update-risk-rules flags:
|
|
1334
|
+
--max-single-order-value <n> Max single order value (required)
|
|
1335
|
+
--max-single-order-enabled <bool> Max single order enabled (required)
|
|
1336
|
+
--max-daily-turnover-value <n> Max daily turnover value (required)
|
|
1337
|
+
--max-daily-turnover-enabled <bool> Max daily turnover enabled (required)
|
|
1338
|
+
--max-daily-orders-value <n> Max daily orders value (required)
|
|
1339
|
+
--max-daily-orders-enabled <bool> Max daily orders enabled (required)
|
|
1340
|
+
|
|
1341
|
+
Examples:
|
|
1342
|
+
alva trading accounts
|
|
1343
|
+
alva trading portfolio --account-id acc_123
|
|
1344
|
+
alva trading orders --account-id acc_123 --limit 10
|
|
1345
|
+
alva trading subscriptions --account-id acc_123
|
|
1346
|
+
alva trading equity-history --account-id acc_123 --timeframe 1d
|
|
1347
|
+
alva trading risk-rules
|
|
1348
|
+
alva trading subscribe --account-id acc_123 --source-username alice --source-feed btc-signals --playbook-id pb_1 --playbook-version v1.0.0
|
|
1349
|
+
alva trading unsubscribe --subscription-id sub_456
|
|
1350
|
+
alva trading execute --account-id acc_123 --signal '{"symbol":"BTC","side":"buy","qty":0.1}' --dry-run
|
|
1351
|
+
alva trading update-risk-rules --max-single-order-value 10000 --max-single-order-enabled true --max-daily-turnover-value 50000 --max-daily-turnover-enabled true --max-daily-orders-value 100 --max-daily-orders-enabled true`
|
|
1036
1352
|
};
|
|
1037
1353
|
async function handleConfigure(args, deps) {
|
|
1038
|
-
const flags =
|
|
1354
|
+
const flags = parseFlags2(args.slice(1));
|
|
1039
1355
|
const apiKey = flags["api-key"];
|
|
1040
1356
|
if (!apiKey) {
|
|
1041
|
-
throw new
|
|
1042
|
-
"--api-key is required. Usage: alva configure --api-key <key> [--base-url <url>] [--profile <name>]"
|
|
1043
|
-
);
|
|
1357
|
+
throw new CliUsageError("--api-key is required", "configure");
|
|
1044
1358
|
}
|
|
1045
1359
|
if (!apiKey.startsWith("alva_")) {
|
|
1046
1360
|
process.stderr?.write?.(
|
|
@@ -1053,10 +1367,10 @@ async function handleConfigure(args, deps) {
|
|
|
1053
1367
|
if (baseUrl) configInput.baseUrl = baseUrl;
|
|
1054
1368
|
const writeDeps = deps ?? {
|
|
1055
1369
|
env: process.env,
|
|
1056
|
-
homedir: () =>
|
|
1057
|
-
mkdir: (path, options) =>
|
|
1058
|
-
writeFile: (path, data, options) =>
|
|
1059
|
-
readFile: (path) =>
|
|
1370
|
+
homedir: () => os2.homedir(),
|
|
1371
|
+
mkdir: (path, options) => fsPromises2.mkdir(path, options).then(() => void 0),
|
|
1372
|
+
writeFile: (path, data, options) => fsPromises2.writeFile(path, data, options).then(() => void 0),
|
|
1373
|
+
readFile: (path) => fsPromises2.readFile(path, "utf-8")
|
|
1060
1374
|
};
|
|
1061
1375
|
const result = await writeConfig(configInput, writeDeps, profileName);
|
|
1062
1376
|
return {
|
|
@@ -1070,9 +1384,11 @@ var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
|
|
|
1070
1384
|
"recursive",
|
|
1071
1385
|
"mkdir-parents",
|
|
1072
1386
|
"push-notify",
|
|
1073
|
-
"help"
|
|
1387
|
+
"help",
|
|
1388
|
+
"execute-latest",
|
|
1389
|
+
"dry-run"
|
|
1074
1390
|
]);
|
|
1075
|
-
function
|
|
1391
|
+
function parseFlags2(argv) {
|
|
1076
1392
|
const flags = {};
|
|
1077
1393
|
for (let i = 0; i < argv.length; i++) {
|
|
1078
1394
|
const arg = argv[i];
|
|
@@ -1100,7 +1416,8 @@ function boolFlag(val) {
|
|
|
1100
1416
|
function requireFlag(flags, name, command) {
|
|
1101
1417
|
const val = flags[name];
|
|
1102
1418
|
if (val === void 0) {
|
|
1103
|
-
|
|
1419
|
+
const group = command.split(" ")[0];
|
|
1420
|
+
throw new CliUsageError(`--${name} is required for '${command}'`, group);
|
|
1104
1421
|
}
|
|
1105
1422
|
return val;
|
|
1106
1423
|
}
|
|
@@ -1108,8 +1425,10 @@ function requireNumericFlag(flags, name, command) {
|
|
|
1108
1425
|
const val = requireFlag(flags, name, command);
|
|
1109
1426
|
const n = Number(val);
|
|
1110
1427
|
if (Number.isNaN(n)) {
|
|
1111
|
-
|
|
1112
|
-
|
|
1428
|
+
const group = command.split(" ")[0];
|
|
1429
|
+
throw new CliUsageError(
|
|
1430
|
+
`--${name} must be a number for '${command}', got '${val}'`,
|
|
1431
|
+
group
|
|
1113
1432
|
);
|
|
1114
1433
|
}
|
|
1115
1434
|
return n;
|
|
@@ -1153,7 +1472,7 @@ async function dispatch(client, args, meta) {
|
|
|
1153
1472
|
return result;
|
|
1154
1473
|
}
|
|
1155
1474
|
const subcommand = args[1];
|
|
1156
|
-
const flags =
|
|
1475
|
+
const flags = parseFlags2(
|
|
1157
1476
|
args.slice(
|
|
1158
1477
|
group === "run" || group === "remix" || group === "screenshot" ? 1 : 2
|
|
1159
1478
|
)
|
|
@@ -1164,11 +1483,13 @@ async function dispatch(client, args, meta) {
|
|
|
1164
1483
|
}
|
|
1165
1484
|
switch (group) {
|
|
1166
1485
|
case "user":
|
|
1167
|
-
if (!subcommand)
|
|
1486
|
+
if (!subcommand)
|
|
1487
|
+
throw new CliUsageError("Missing subcommand for user", "user");
|
|
1168
1488
|
if (subcommand === "me") return client.user.me();
|
|
1169
|
-
throw new
|
|
1489
|
+
throw new CliUsageError(`Unknown subcommand: user ${subcommand}`, "user");
|
|
1170
1490
|
case "fs": {
|
|
1171
|
-
if (!subcommand)
|
|
1491
|
+
if (!subcommand)
|
|
1492
|
+
throw new CliUsageError("Missing subcommand for fs", "fs");
|
|
1172
1493
|
switch (subcommand) {
|
|
1173
1494
|
case "read":
|
|
1174
1495
|
return client.fs.read({
|
|
@@ -1245,18 +1566,33 @@ async function dispatch(client, args, meta) {
|
|
|
1245
1566
|
permission: requireFlag(flags, "permission", "fs revoke")
|
|
1246
1567
|
});
|
|
1247
1568
|
default:
|
|
1248
|
-
throw new
|
|
1569
|
+
throw new CliUsageError(`Unknown subcommand: fs ${subcommand}`, "fs");
|
|
1249
1570
|
}
|
|
1250
1571
|
}
|
|
1251
|
-
case "run":
|
|
1572
|
+
case "run": {
|
|
1573
|
+
const sourceFlags = ["code", "local-file", "entry-path"].filter(
|
|
1574
|
+
(f) => flags[f] !== void 0
|
|
1575
|
+
);
|
|
1576
|
+
if (sourceFlags.length > 1) {
|
|
1577
|
+
throw new CliUsageError(
|
|
1578
|
+
`--${sourceFlags.join(" and --")} are mutually exclusive`,
|
|
1579
|
+
"run"
|
|
1580
|
+
);
|
|
1581
|
+
}
|
|
1582
|
+
let code = flags["code"];
|
|
1583
|
+
if (flags["local-file"]) {
|
|
1584
|
+
code = fs.readFileSync(flags["local-file"], "utf-8");
|
|
1585
|
+
}
|
|
1252
1586
|
return client.run.execute({
|
|
1253
|
-
code
|
|
1587
|
+
code,
|
|
1254
1588
|
entry_path: flags["entry-path"],
|
|
1255
1589
|
working_dir: flags["working-dir"],
|
|
1256
1590
|
args: jsonParse(flags["args"])
|
|
1257
1591
|
});
|
|
1592
|
+
}
|
|
1258
1593
|
case "deploy": {
|
|
1259
|
-
if (!subcommand)
|
|
1594
|
+
if (!subcommand)
|
|
1595
|
+
throw new CliUsageError("Missing subcommand for deploy", "deploy");
|
|
1260
1596
|
switch (subcommand) {
|
|
1261
1597
|
case "create":
|
|
1262
1598
|
return client.deploy.create({
|
|
@@ -1295,12 +1631,27 @@ async function dispatch(client, args, meta) {
|
|
|
1295
1631
|
return client.deploy.resume({
|
|
1296
1632
|
id: requireNumericFlag(flags, "id", "deploy resume")
|
|
1297
1633
|
});
|
|
1634
|
+
case "runs":
|
|
1635
|
+
return client.deploy.listRuns({
|
|
1636
|
+
cronjob_id: requireNumericFlag(flags, "id", "deploy runs"),
|
|
1637
|
+
first: num(flags["first"]),
|
|
1638
|
+
cursor: num(flags["cursor"])
|
|
1639
|
+
});
|
|
1640
|
+
case "run-logs":
|
|
1641
|
+
return client.deploy.getRunLogs({
|
|
1642
|
+
cronjob_id: requireNumericFlag(flags, "id", "deploy run-logs"),
|
|
1643
|
+
run_id: requireNumericFlag(flags, "run-id", "deploy run-logs")
|
|
1644
|
+
});
|
|
1298
1645
|
default:
|
|
1299
|
-
throw new
|
|
1646
|
+
throw new CliUsageError(
|
|
1647
|
+
`Unknown subcommand: deploy ${subcommand}`,
|
|
1648
|
+
"deploy"
|
|
1649
|
+
);
|
|
1300
1650
|
}
|
|
1301
1651
|
}
|
|
1302
1652
|
case "release": {
|
|
1303
|
-
if (!subcommand)
|
|
1653
|
+
if (!subcommand)
|
|
1654
|
+
throw new CliUsageError("Missing subcommand for release", "release");
|
|
1304
1655
|
switch (subcommand) {
|
|
1305
1656
|
case "feed":
|
|
1306
1657
|
return client.release.feed({
|
|
@@ -1339,11 +1690,15 @@ async function dispatch(client, args, meta) {
|
|
|
1339
1690
|
changelog: requireFlag(flags, "changelog", "release playbook")
|
|
1340
1691
|
});
|
|
1341
1692
|
default:
|
|
1342
|
-
throw new
|
|
1693
|
+
throw new CliUsageError(
|
|
1694
|
+
`Unknown subcommand: release ${subcommand}`,
|
|
1695
|
+
"release"
|
|
1696
|
+
);
|
|
1343
1697
|
}
|
|
1344
1698
|
}
|
|
1345
1699
|
case "secrets": {
|
|
1346
|
-
if (!subcommand)
|
|
1700
|
+
if (!subcommand)
|
|
1701
|
+
throw new CliUsageError("Missing subcommand for secrets", "secrets");
|
|
1347
1702
|
switch (subcommand) {
|
|
1348
1703
|
case "create":
|
|
1349
1704
|
return client.secrets.create({
|
|
@@ -1366,11 +1721,15 @@ async function dispatch(client, args, meta) {
|
|
|
1366
1721
|
name: requireFlag(flags, "name", "secrets delete")
|
|
1367
1722
|
});
|
|
1368
1723
|
default:
|
|
1369
|
-
throw new
|
|
1724
|
+
throw new CliUsageError(
|
|
1725
|
+
`Unknown subcommand: secrets ${subcommand}`,
|
|
1726
|
+
"secrets"
|
|
1727
|
+
);
|
|
1370
1728
|
}
|
|
1371
1729
|
}
|
|
1372
1730
|
case "sdk": {
|
|
1373
|
-
if (!subcommand)
|
|
1731
|
+
if (!subcommand)
|
|
1732
|
+
throw new CliUsageError("Missing subcommand for sdk", "sdk");
|
|
1374
1733
|
switch (subcommand) {
|
|
1375
1734
|
case "doc":
|
|
1376
1735
|
return client.sdk.doc({
|
|
@@ -1383,11 +1742,15 @@ async function dispatch(client, args, meta) {
|
|
|
1383
1742
|
partition: requireFlag(flags, "partition", "sdk partition-summary")
|
|
1384
1743
|
});
|
|
1385
1744
|
default:
|
|
1386
|
-
throw new
|
|
1745
|
+
throw new CliUsageError(
|
|
1746
|
+
`Unknown subcommand: sdk ${subcommand}`,
|
|
1747
|
+
"sdk"
|
|
1748
|
+
);
|
|
1387
1749
|
}
|
|
1388
1750
|
}
|
|
1389
1751
|
case "comments": {
|
|
1390
|
-
if (!subcommand)
|
|
1752
|
+
if (!subcommand)
|
|
1753
|
+
throw new CliUsageError("Missing subcommand for comments", "comments");
|
|
1391
1754
|
switch (subcommand) {
|
|
1392
1755
|
case "create":
|
|
1393
1756
|
return client.comments.create({
|
|
@@ -1409,7 +1772,10 @@ async function dispatch(client, args, meta) {
|
|
|
1409
1772
|
)
|
|
1410
1773
|
});
|
|
1411
1774
|
default:
|
|
1412
|
-
throw new
|
|
1775
|
+
throw new CliUsageError(
|
|
1776
|
+
`Unknown subcommand: comments ${subcommand}`,
|
|
1777
|
+
"comments"
|
|
1778
|
+
);
|
|
1413
1779
|
}
|
|
1414
1780
|
}
|
|
1415
1781
|
case "remix":
|
|
@@ -1431,10 +1797,124 @@ async function dispatch(client, args, meta) {
|
|
|
1431
1797
|
fs.writeFileSync(outFile, buf);
|
|
1432
1798
|
return { written: outFile, bytes: buf.length };
|
|
1433
1799
|
}
|
|
1800
|
+
case "trading": {
|
|
1801
|
+
if (!subcommand)
|
|
1802
|
+
throw new CliUsageError("Missing subcommand for trading", "trading");
|
|
1803
|
+
switch (subcommand) {
|
|
1804
|
+
case "accounts":
|
|
1805
|
+
return client.trading.accounts();
|
|
1806
|
+
case "portfolio":
|
|
1807
|
+
return client.trading.portfolio(
|
|
1808
|
+
requireFlag(flags, "account-id", "trading portfolio")
|
|
1809
|
+
);
|
|
1810
|
+
case "orders":
|
|
1811
|
+
return client.trading.orders({
|
|
1812
|
+
accountId: requireFlag(flags, "account-id", "trading orders"),
|
|
1813
|
+
source: flags["source"],
|
|
1814
|
+
since: num(flags["since"]),
|
|
1815
|
+
limit: num(flags["limit"])
|
|
1816
|
+
});
|
|
1817
|
+
case "subscriptions":
|
|
1818
|
+
return client.trading.subscriptions(
|
|
1819
|
+
requireFlag(flags, "account-id", "trading subscriptions")
|
|
1820
|
+
);
|
|
1821
|
+
case "equity-history":
|
|
1822
|
+
return client.trading.equityHistory({
|
|
1823
|
+
accountId: requireFlag(
|
|
1824
|
+
flags,
|
|
1825
|
+
"account-id",
|
|
1826
|
+
"trading equity-history"
|
|
1827
|
+
),
|
|
1828
|
+
timeframe: flags["timeframe"],
|
|
1829
|
+
sinceMs: num(flags["since-ms"]),
|
|
1830
|
+
untilMs: num(flags["until-ms"])
|
|
1831
|
+
});
|
|
1832
|
+
case "risk-rules":
|
|
1833
|
+
return client.trading.riskRules();
|
|
1834
|
+
case "subscribe":
|
|
1835
|
+
return client.trading.subscribe({
|
|
1836
|
+
accountId: requireFlag(flags, "account-id", "trading subscribe"),
|
|
1837
|
+
sourceUsername: requireFlag(
|
|
1838
|
+
flags,
|
|
1839
|
+
"source-username",
|
|
1840
|
+
"trading subscribe"
|
|
1841
|
+
),
|
|
1842
|
+
sourceFeed: requireFlag(flags, "source-feed", "trading subscribe"),
|
|
1843
|
+
playbookId: requireFlag(flags, "playbook-id", "trading subscribe"),
|
|
1844
|
+
playbookVersion: requireFlag(
|
|
1845
|
+
flags,
|
|
1846
|
+
"playbook-version",
|
|
1847
|
+
"trading subscribe"
|
|
1848
|
+
),
|
|
1849
|
+
executeLatest: boolFlag(flags["execute-latest"])
|
|
1850
|
+
});
|
|
1851
|
+
case "unsubscribe":
|
|
1852
|
+
return client.trading.unsubscribe(
|
|
1853
|
+
requireFlag(flags, "subscription-id", "trading unsubscribe")
|
|
1854
|
+
);
|
|
1855
|
+
case "execute":
|
|
1856
|
+
return client.trading.execute({
|
|
1857
|
+
accountId: requireFlag(flags, "account-id", "trading execute"),
|
|
1858
|
+
signalJson: requireFlag(flags, "signal", "trading execute"),
|
|
1859
|
+
dryRun: boolFlag(flags["dry-run"]) ?? false,
|
|
1860
|
+
sourceUsername: flags["source-username"],
|
|
1861
|
+
sourceFeed: flags["source-feed"]
|
|
1862
|
+
});
|
|
1863
|
+
case "update-risk-rules":
|
|
1864
|
+
return client.trading.updateRiskRules({
|
|
1865
|
+
maxSingleOrder: {
|
|
1866
|
+
value: requireNumericFlag(
|
|
1867
|
+
flags,
|
|
1868
|
+
"max-single-order-value",
|
|
1869
|
+
"trading update-risk-rules"
|
|
1870
|
+
),
|
|
1871
|
+
enabled: requireFlag(
|
|
1872
|
+
flags,
|
|
1873
|
+
"max-single-order-enabled",
|
|
1874
|
+
"trading update-risk-rules"
|
|
1875
|
+
) === "true"
|
|
1876
|
+
},
|
|
1877
|
+
maxDailyTurnover: {
|
|
1878
|
+
value: requireNumericFlag(
|
|
1879
|
+
flags,
|
|
1880
|
+
"max-daily-turnover-value",
|
|
1881
|
+
"trading update-risk-rules"
|
|
1882
|
+
),
|
|
1883
|
+
enabled: requireFlag(
|
|
1884
|
+
flags,
|
|
1885
|
+
"max-daily-turnover-enabled",
|
|
1886
|
+
"trading update-risk-rules"
|
|
1887
|
+
) === "true"
|
|
1888
|
+
},
|
|
1889
|
+
maxDailyOrders: {
|
|
1890
|
+
value: requireNumericFlag(
|
|
1891
|
+
flags,
|
|
1892
|
+
"max-daily-orders-value",
|
|
1893
|
+
"trading update-risk-rules"
|
|
1894
|
+
),
|
|
1895
|
+
enabled: requireFlag(
|
|
1896
|
+
flags,
|
|
1897
|
+
"max-daily-orders-enabled",
|
|
1898
|
+
"trading update-risk-rules"
|
|
1899
|
+
) === "true"
|
|
1900
|
+
}
|
|
1901
|
+
});
|
|
1902
|
+
default:
|
|
1903
|
+
throw new CliUsageError(
|
|
1904
|
+
`Unknown subcommand: trading ${subcommand}`,
|
|
1905
|
+
"trading"
|
|
1906
|
+
);
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1909
|
+
case "auth": {
|
|
1910
|
+
const authSub = args[1];
|
|
1911
|
+
if (!authSub || authSub === "--help" || authSub === "-h" || args[2] === "--help" || args[2] === "-h") {
|
|
1912
|
+
return { _help: true, text: COMMAND_HELP.auth };
|
|
1913
|
+
}
|
|
1914
|
+
throw new CliUsageError(`Unknown auth subcommand: '${authSub}'`, "auth");
|
|
1915
|
+
}
|
|
1434
1916
|
default:
|
|
1435
|
-
throw new
|
|
1436
|
-
`Unknown command: '${group}'. Run 'alva --help' to see available commands.`
|
|
1437
|
-
);
|
|
1917
|
+
throw new CliUsageError(`Unknown command: '${group}'`);
|
|
1438
1918
|
}
|
|
1439
1919
|
}
|
|
1440
1920
|
async function main() {
|
|
@@ -1454,11 +1934,28 @@ async function main() {
|
|
|
1454
1934
|
process.stdout.write(JSON.stringify(result2, null, 2) + "\n");
|
|
1455
1935
|
return;
|
|
1456
1936
|
}
|
|
1937
|
+
if (rawArgs[0] === "auth") {
|
|
1938
|
+
const authSub = rawArgs[1];
|
|
1939
|
+
if (!authSub || authSub === "--help" || authSub === "-h" || rawArgs[2] === "--help" || rawArgs[2] === "-h") {
|
|
1940
|
+
process.stdout.write(`${COMMAND_HELP.auth}
|
|
1941
|
+
`);
|
|
1942
|
+
return;
|
|
1943
|
+
}
|
|
1944
|
+
if (authSub === "login") {
|
|
1945
|
+
const result2 = await handleAuthLogin(rawArgs);
|
|
1946
|
+
process.stdout.write(`${JSON.stringify(result2, null, 2)}
|
|
1947
|
+
`);
|
|
1948
|
+
return;
|
|
1949
|
+
}
|
|
1950
|
+
process.stdout.write(`${COMMAND_HELP.auth}
|
|
1951
|
+
`);
|
|
1952
|
+
return;
|
|
1953
|
+
}
|
|
1457
1954
|
const config = loadConfig({
|
|
1458
1955
|
argv: rawArgs,
|
|
1459
1956
|
env: process.env,
|
|
1460
1957
|
readFile: (path) => fs.readFileSync(path, "utf-8"),
|
|
1461
|
-
homedir: () =>
|
|
1958
|
+
homedir: () => os2.homedir()
|
|
1462
1959
|
});
|
|
1463
1960
|
const client = new AlvaClient({
|
|
1464
1961
|
apiKey: config.apiKey,
|
|
@@ -1500,12 +1997,29 @@ async function main() {
|
|
|
1500
1997
|
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
1501
1998
|
}
|
|
1502
1999
|
} catch (err) {
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
2000
|
+
if (err instanceof CliUsageError) {
|
|
2001
|
+
const help = err.command ? COMMAND_HELP[err.command] : HELP_TEXT;
|
|
2002
|
+
process.stderr.write(`Error: ${err.message}
|
|
2003
|
+
`);
|
|
2004
|
+
if (help) process.stderr.write(`
|
|
2005
|
+
${help}
|
|
2006
|
+
`);
|
|
2007
|
+
process.exit(1);
|
|
2008
|
+
} else if (err instanceof AlvaError) {
|
|
2009
|
+
const error = {
|
|
2010
|
+
code: err.code,
|
|
2011
|
+
message: err.message,
|
|
2012
|
+
status: err.status
|
|
2013
|
+
};
|
|
2014
|
+
process.stderr.write(`${JSON.stringify({ error }, null, 2)}
|
|
2015
|
+
`);
|
|
2016
|
+
process.exit(1);
|
|
2017
|
+
} else {
|
|
2018
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2019
|
+
process.stderr.write(`Error: ${message}
|
|
2020
|
+
`);
|
|
2021
|
+
process.exit(1);
|
|
2022
|
+
}
|
|
1509
2023
|
}
|
|
1510
2024
|
}
|
|
1511
2025
|
var isDirectRun = typeof process !== "undefined" && process.argv[1] && (process.argv[1].endsWith("cli.mjs") || process.argv[1].endsWith("cli.js") || process.argv[1].endsWith("/alva") || process.argv[1].endsWith("\\alva"));
|