@use-aistack/cli 0.6.1 → 0.6.3
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 +2 -2
- package/dist/index.js +169 -43
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@ The default command. Scan your local transcripts (rolling 30 days), review the f
|
|
|
18
18
|
npx @use-aistack/cli sync
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
On an unlinked machine, `sync` starts the login flow inline — your browser opens to approve the machine, then the sync continues. One command is the whole onboarding.
|
|
22
22
|
|
|
23
23
|
### `npx @use-aistack/cli sync --auto on` / `off`
|
|
24
24
|
|
|
@@ -34,7 +34,7 @@ The silent run (`sync --auto`) never prompts and publishes only under this opt-i
|
|
|
34
34
|
|
|
35
35
|
### `npx @use-aistack/cli login`
|
|
36
36
|
|
|
37
|
-
Link this machine to your AI Stack account via browser.
|
|
37
|
+
Link this machine to your AI Stack account via browser. Optional — `sync` runs this inline when the machine is not linked yet.
|
|
38
38
|
|
|
39
39
|
```sh
|
|
40
40
|
npx @use-aistack/cli login
|
package/dist/index.js
CHANGED
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { Command } from "commander";
|
|
5
5
|
|
|
6
|
+
// src/version.ts
|
|
7
|
+
var CLI_VERSION = true ? "0.6.3" : "0.0.0-dev";
|
|
8
|
+
|
|
6
9
|
// src/api.ts
|
|
7
10
|
var BASE_URL = process.env.AISTACK_URL || "https://aistack.to";
|
|
8
11
|
async function request(path3, options = {}) {
|
|
@@ -34,7 +37,12 @@ function failure(what, res) {
|
|
|
34
37
|
async function authStart(machineName) {
|
|
35
38
|
const res = await request("/api/cli/auth/start", {
|
|
36
39
|
method: "POST",
|
|
37
|
-
|
|
40
|
+
// `cliVersion` rides along so `cli_login_completed` can report which
|
|
41
|
+
// version linked the machine (#78). The server carries it on the pending
|
|
42
|
+
// session and reads it at the token exchange.
|
|
43
|
+
body: JSON.stringify(
|
|
44
|
+
machineName ? { machineName, cliVersion: CLI_VERSION } : { cliVersion: CLI_VERSION }
|
|
45
|
+
)
|
|
38
46
|
});
|
|
39
47
|
if (!res.ok) throw failure("Auth start failed", res);
|
|
40
48
|
return res.json();
|
|
@@ -1572,8 +1580,7 @@ function proposedMachineName(read = hostname) {
|
|
|
1572
1580
|
return void 0;
|
|
1573
1581
|
}
|
|
1574
1582
|
}
|
|
1575
|
-
async function
|
|
1576
|
-
intro2("login");
|
|
1583
|
+
async function performLogin() {
|
|
1577
1584
|
const s = p5.spinner();
|
|
1578
1585
|
s.start("Starting authentication...");
|
|
1579
1586
|
let session;
|
|
@@ -1583,8 +1590,7 @@ async function loginCommand() {
|
|
|
1583
1590
|
} catch (err) {
|
|
1584
1591
|
s.stop("Failed to start authentication");
|
|
1585
1592
|
p5.log.error(err instanceof Error ? err.message : String(err));
|
|
1586
|
-
|
|
1587
|
-
process.exit(1);
|
|
1593
|
+
return false;
|
|
1588
1594
|
}
|
|
1589
1595
|
p5.log.info(`${dim("CODE")} ${limeBold(session.userCode)}`);
|
|
1590
1596
|
p5.log.info(`${dim("OPEN")} ${dim(session.authUrl)}`);
|
|
@@ -1604,29 +1610,33 @@ async function loginCommand() {
|
|
|
1604
1610
|
if (result.status === "approved" && result.token) {
|
|
1605
1611
|
s.stop(lime("Authenticated"));
|
|
1606
1612
|
saveToken(result.token, result.userId);
|
|
1607
|
-
|
|
1608
|
-
`Token saved. Run ${limeBold("npx @use-aistack/cli collect")} to get started.`
|
|
1609
|
-
);
|
|
1610
|
-
outro2(lime("done"));
|
|
1611
|
-
return;
|
|
1613
|
+
return true;
|
|
1612
1614
|
}
|
|
1613
1615
|
if (result.status === "expired") {
|
|
1614
1616
|
s.stop("Session expired");
|
|
1615
1617
|
p5.log.error("Authentication session expired. Please try again.");
|
|
1616
|
-
|
|
1617
|
-
process.exit(1);
|
|
1618
|
+
return false;
|
|
1618
1619
|
}
|
|
1619
1620
|
} catch (err) {
|
|
1620
1621
|
s.stop("Error polling");
|
|
1621
1622
|
p5.log.error(err instanceof Error ? err.message : String(err));
|
|
1622
|
-
|
|
1623
|
-
process.exit(1);
|
|
1623
|
+
return false;
|
|
1624
1624
|
}
|
|
1625
1625
|
}
|
|
1626
1626
|
s.stop("Timed out");
|
|
1627
1627
|
p5.log.error("Authentication timed out after 3 minutes. Please try again.");
|
|
1628
|
-
|
|
1629
|
-
|
|
1628
|
+
return false;
|
|
1629
|
+
}
|
|
1630
|
+
async function loginCommand() {
|
|
1631
|
+
intro2("login");
|
|
1632
|
+
if (!await performLogin()) {
|
|
1633
|
+
outroError("error");
|
|
1634
|
+
process.exit(1);
|
|
1635
|
+
}
|
|
1636
|
+
p5.log.success(
|
|
1637
|
+
`Token saved. Run ${limeBold("npx @use-aistack/cli sync")} to publish your usage.`
|
|
1638
|
+
);
|
|
1639
|
+
outro2(lime("done"));
|
|
1630
1640
|
}
|
|
1631
1641
|
|
|
1632
1642
|
// src/commands/sync.ts
|
|
@@ -2748,7 +2758,11 @@ function emptyScanStats() {
|
|
|
2748
2758
|
filesRead: 0,
|
|
2749
2759
|
filesSkippedByMtime: 0,
|
|
2750
2760
|
filesSkippedAsDuplicate: 0,
|
|
2751
|
-
filesUnreadable: 0
|
|
2761
|
+
filesUnreadable: 0,
|
|
2762
|
+
filesForeign: 0,
|
|
2763
|
+
foreignOriginators: /* @__PURE__ */ new Map(),
|
|
2764
|
+
unreadableFiles: [],
|
|
2765
|
+
filesZstdUnsupported: 0
|
|
2752
2766
|
};
|
|
2753
2767
|
}
|
|
2754
2768
|
|
|
@@ -3040,11 +3054,12 @@ async function scan2(agg, opts = {}) {
|
|
|
3040
3054
|
} catch {
|
|
3041
3055
|
resolved = file;
|
|
3042
3056
|
}
|
|
3043
|
-
|
|
3057
|
+
const dedupKey = resolved.endsWith(".zst") ? resolved.slice(0, -".zst".length) : resolved;
|
|
3058
|
+
if (visited.has(dedupKey)) {
|
|
3044
3059
|
stats.filesSkippedAsDuplicate++;
|
|
3045
3060
|
continue;
|
|
3046
3061
|
}
|
|
3047
|
-
visited.add(
|
|
3062
|
+
visited.add(dedupKey);
|
|
3048
3063
|
if (opts.sinceMs !== void 0) {
|
|
3049
3064
|
try {
|
|
3050
3065
|
const st = await stat3(file);
|
|
@@ -3058,11 +3073,20 @@ async function scan2(agg, opts = {}) {
|
|
|
3058
3073
|
agg.files++;
|
|
3059
3074
|
stats.filesRead++;
|
|
3060
3075
|
if (opts.onProgress && agg.files % 200 === 0) opts.onProgress(agg.files);
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
} catch {
|
|
3076
|
+
const outcome = ingestWithRetry(agg, file, opts);
|
|
3077
|
+
if (!outcome.ok) {
|
|
3064
3078
|
stats.filesUnreadable++;
|
|
3065
3079
|
stats.filesRead--;
|
|
3080
|
+
if (outcome.reason === "zstd-unsupported") stats.filesZstdUnsupported++;
|
|
3081
|
+
stats.unreadableFiles.push({
|
|
3082
|
+
path: path2.relative(root, file),
|
|
3083
|
+
reason: outcome.reason
|
|
3084
|
+
});
|
|
3085
|
+
} else if (!outcome.genuine) {
|
|
3086
|
+
stats.filesForeign++;
|
|
3087
|
+
stats.filesRead--;
|
|
3088
|
+
const seen = stats.foreignOriginators.get(outcome.originator) ?? 0;
|
|
3089
|
+
stats.foreignOriginators.set(outcome.originator, seen + 1);
|
|
3066
3090
|
}
|
|
3067
3091
|
}
|
|
3068
3092
|
}
|
|
@@ -3077,29 +3101,81 @@ async function exists3(p8) {
|
|
|
3077
3101
|
return false;
|
|
3078
3102
|
}
|
|
3079
3103
|
}
|
|
3080
|
-
function
|
|
3104
|
+
function errorClass(e) {
|
|
3105
|
+
const code = e?.code;
|
|
3106
|
+
if (typeof code === "string" && code.length > 0) return code;
|
|
3107
|
+
return e instanceof Error ? e.constructor.name : "unknown";
|
|
3108
|
+
}
|
|
3109
|
+
var readError = (reason) => Object.assign(new Error(reason), { code: reason });
|
|
3110
|
+
function ingestWithRetry(agg, file, opts) {
|
|
3111
|
+
try {
|
|
3112
|
+
return ingestFile2(agg, file, opts);
|
|
3113
|
+
} catch (e) {
|
|
3114
|
+
if (errorClass(e) === "ENOENT" && !file.endsWith(".zst")) {
|
|
3115
|
+
try {
|
|
3116
|
+
return ingestFile2(agg, `${file}.zst`, opts);
|
|
3117
|
+
} catch (e2) {
|
|
3118
|
+
return { ok: false, reason: errorClass(e2) };
|
|
3119
|
+
}
|
|
3120
|
+
}
|
|
3121
|
+
return { ok: false, reason: errorClass(e) };
|
|
3122
|
+
}
|
|
3123
|
+
}
|
|
3124
|
+
function ingestFile2(agg, file, opts) {
|
|
3125
|
+
const readFile = opts.readFileImpl ?? readFileSync9;
|
|
3081
3126
|
let text;
|
|
3082
3127
|
if (file.endsWith(".zst")) {
|
|
3083
|
-
if (zstdDecompress === null)
|
|
3084
|
-
|
|
3128
|
+
if (zstdDecompress === null) throw readError("zstd-unsupported");
|
|
3129
|
+
const raw = readFile(file);
|
|
3130
|
+
try {
|
|
3131
|
+
text = zstdDecompress(
|
|
3132
|
+
Buffer.isBuffer(raw) ? raw : Buffer.from(raw)
|
|
3133
|
+
).toString("utf8");
|
|
3134
|
+
} catch {
|
|
3135
|
+
throw readError("zstd-corrupt");
|
|
3085
3136
|
}
|
|
3086
|
-
text = zstdDecompress(readFileSync9(file)).toString("utf8");
|
|
3087
3137
|
} else {
|
|
3088
|
-
text =
|
|
3138
|
+
text = readFile(file).toString("utf8");
|
|
3089
3139
|
}
|
|
3090
|
-
const
|
|
3140
|
+
const records = [];
|
|
3141
|
+
let nonEmptyLines = 0;
|
|
3142
|
+
let parseErrors = 0;
|
|
3091
3143
|
for (const line of text.split("\n")) {
|
|
3092
3144
|
if (!line) continue;
|
|
3093
|
-
|
|
3094
|
-
let rec;
|
|
3145
|
+
nonEmptyLines++;
|
|
3095
3146
|
try {
|
|
3096
|
-
|
|
3147
|
+
records.push(JSON.parse(line));
|
|
3097
3148
|
} catch {
|
|
3098
|
-
|
|
3099
|
-
continue;
|
|
3149
|
+
parseErrors++;
|
|
3100
3150
|
}
|
|
3101
|
-
ingestLine(agg, rec, state, sinceMs);
|
|
3102
3151
|
}
|
|
3152
|
+
const verdict = classifyRollout(records);
|
|
3153
|
+
if (!verdict.genuine) return { ok: true, ...verdict };
|
|
3154
|
+
agg.lines += nonEmptyLines;
|
|
3155
|
+
agg.parseErrors += parseErrors;
|
|
3156
|
+
const state = createFileState();
|
|
3157
|
+
for (const rec of records) ingestLine(agg, rec, state, opts.sinceMs);
|
|
3158
|
+
return { ok: true, genuine: true };
|
|
3159
|
+
}
|
|
3160
|
+
function classifyRollout(records) {
|
|
3161
|
+
let originator = null;
|
|
3162
|
+
let sawTurnContext = false;
|
|
3163
|
+
let genuine = records.length > 0;
|
|
3164
|
+
for (const [i, raw] of records.entries()) {
|
|
3165
|
+
const rec = asObj(raw);
|
|
3166
|
+
const type = rec ? asStr(rec.type) : null;
|
|
3167
|
+
const payload = rec ? asObj(rec.payload) : null;
|
|
3168
|
+
if (i === 0 && type !== "session_meta") genuine = false;
|
|
3169
|
+
if (type === "session_meta" && payload && originator === null) {
|
|
3170
|
+
originator = asStr(payload.originator);
|
|
3171
|
+
} else if (type === "turn_context") {
|
|
3172
|
+
sawTurnContext = true;
|
|
3173
|
+
} else if (type === "event_msg" && payload && asStr(payload.type) === "token_count" && !sawTurnContext) {
|
|
3174
|
+
genuine = false;
|
|
3175
|
+
}
|
|
3176
|
+
}
|
|
3177
|
+
if (genuine) return { genuine: true };
|
|
3178
|
+
return { genuine: false, originator: originator ?? "(none)" };
|
|
3103
3179
|
}
|
|
3104
3180
|
function readConfiguredMcpServers(agg, configFile) {
|
|
3105
3181
|
const file = configFile ?? path2.join(codexHome2(), "config.toml");
|
|
@@ -3368,11 +3444,12 @@ function mergeKeptPrivate(halves) {
|
|
|
3368
3444
|
}
|
|
3369
3445
|
return out;
|
|
3370
3446
|
}
|
|
3371
|
-
function buildSyncBody(built, syncConfig) {
|
|
3447
|
+
function buildSyncBody(built, syncConfig, autoSync) {
|
|
3372
3448
|
const payloads = built.map((b) => b.payload);
|
|
3373
|
-
|
|
3449
|
+
const base = autoSync ? { payloads, autoSync } : { payloads };
|
|
3450
|
+
if (!syncConfig.reviewKeptPrivate) return base;
|
|
3374
3451
|
return {
|
|
3375
|
-
|
|
3452
|
+
...base,
|
|
3376
3453
|
keptPrivate: mergeKeptPrivate(built.map((b) => b.keptPrivate))
|
|
3377
3454
|
};
|
|
3378
3455
|
}
|
|
@@ -3468,7 +3545,32 @@ function harnessLabel(name) {
|
|
|
3468
3545
|
if (name === "codex") return "Codex";
|
|
3469
3546
|
return name;
|
|
3470
3547
|
}
|
|
3471
|
-
|
|
3548
|
+
var UNREADABLE_FILES_SHOWN = 5;
|
|
3549
|
+
function scanNoteLines(stats, label) {
|
|
3550
|
+
const out = [];
|
|
3551
|
+
const shown = stats.unreadableFiles.slice(0, UNREADABLE_FILES_SHOWN);
|
|
3552
|
+
for (const f of shown) {
|
|
3553
|
+
out.push(` ${f.path} (${f.reason})`);
|
|
3554
|
+
}
|
|
3555
|
+
if (stats.unreadableFiles.length > shown.length) {
|
|
3556
|
+
out.push(
|
|
3557
|
+
` ...${stats.unreadableFiles.length - shown.length} more`
|
|
3558
|
+
);
|
|
3559
|
+
}
|
|
3560
|
+
if (stats.filesZstdUnsupported > 0) {
|
|
3561
|
+
out.push(
|
|
3562
|
+
` ${stats.filesZstdUnsupported} compressed rollout${stats.filesZstdUnsupported === 1 ? "" : "s"} need Node 22.15 or newer`
|
|
3563
|
+
);
|
|
3564
|
+
}
|
|
3565
|
+
if (stats.filesForeign > 0) {
|
|
3566
|
+
const origins = [...stats.foreignOriginators].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).map(([name, n]) => n > 1 ? `${name} \xD7${n}` : name).join(", ");
|
|
3567
|
+
out.push(
|
|
3568
|
+
`skipped ${stats.filesForeign} file${stats.filesForeign === 1 ? "" : "s"} not written by ${label} \u2014 left out (originators: ${origins})`
|
|
3569
|
+
);
|
|
3570
|
+
}
|
|
3571
|
+
return out;
|
|
3572
|
+
}
|
|
3573
|
+
function payloadBlock(payload, showHeader, stats) {
|
|
3472
3574
|
const out = [];
|
|
3473
3575
|
if (showHeader) {
|
|
3474
3576
|
out.push(
|
|
@@ -3491,6 +3593,9 @@ function payloadBlock(payload, showHeader) {
|
|
|
3491
3593
|
`coverage ${cov.filesUnreadable} files unreadable \xB7 ${cov.linesFailed} lines failed \u2014 this reading is a floor`
|
|
3492
3594
|
);
|
|
3493
3595
|
}
|
|
3596
|
+
if (stats) {
|
|
3597
|
+
out.push(...scanNoteLines(stats, harnessLabel(payload.harness.name)));
|
|
3598
|
+
}
|
|
3494
3599
|
out.push("");
|
|
3495
3600
|
out.push("models");
|
|
3496
3601
|
for (const m of payload.models) {
|
|
@@ -3522,7 +3627,8 @@ function buildGateSummary(ctx) {
|
|
|
3522
3627
|
);
|
|
3523
3628
|
}
|
|
3524
3629
|
for (const payload of payloads) {
|
|
3525
|
-
|
|
3630
|
+
const stats = ctx.scanStats?.[payload.harness.name];
|
|
3631
|
+
out.push(...payloadBlock(payload, payloads.length > 1, stats));
|
|
3526
3632
|
out.push("");
|
|
3527
3633
|
}
|
|
3528
3634
|
if (out[out.length - 1] === "") out.pop();
|
|
@@ -3548,6 +3654,12 @@ function buildGateSummary(ctx) {
|
|
|
3548
3654
|
out.push(" they stay on this machine");
|
|
3549
3655
|
}
|
|
3550
3656
|
}
|
|
3657
|
+
if (body.autoSync !== void 0) {
|
|
3658
|
+
out.push("");
|
|
3659
|
+
out.push(
|
|
3660
|
+
`auto-sync ${body.autoSync.enabled ? `on, about every ${body.autoSync.frequencyHours}h` : "off"}`
|
|
3661
|
+
);
|
|
3662
|
+
}
|
|
3551
3663
|
if (source === "bundled") {
|
|
3552
3664
|
out.push("");
|
|
3553
3665
|
out.push(
|
|
@@ -3575,9 +3687,11 @@ async function stageSync(deps) {
|
|
|
3575
3687
|
...token ? { token } : {}
|
|
3576
3688
|
});
|
|
3577
3689
|
const built = [];
|
|
3690
|
+
const scanStats = {};
|
|
3578
3691
|
const sinceMs = windowStartMs(now, windowDays);
|
|
3579
3692
|
for (const adapter of await adapters()) {
|
|
3580
3693
|
const { aggregate, stats } = await adapter.scan({ sinceMs });
|
|
3694
|
+
scanStats[adapter.name] = stats;
|
|
3581
3695
|
built.push(
|
|
3582
3696
|
buildPayload({
|
|
3583
3697
|
aggregate,
|
|
@@ -3591,7 +3705,8 @@ async function stageSync(deps) {
|
|
|
3591
3705
|
})
|
|
3592
3706
|
);
|
|
3593
3707
|
}
|
|
3594
|
-
const
|
|
3708
|
+
const settings = (deps.getSettingsImpl ?? getSettings)();
|
|
3709
|
+
const body = buildSyncBody(built, config, settings.autoSync);
|
|
3595
3710
|
const bodyJson = JSON.stringify(body);
|
|
3596
3711
|
const keptPrivate = mergeKeptPrivate(built.map((b) => b.keptPrivate));
|
|
3597
3712
|
const ctx = {
|
|
@@ -3599,7 +3714,8 @@ async function stageSync(deps) {
|
|
|
3599
3714
|
keptPrivate,
|
|
3600
3715
|
config,
|
|
3601
3716
|
source,
|
|
3602
|
-
baseUrl: deps.baseUrl
|
|
3717
|
+
baseUrl: deps.baseUrl,
|
|
3718
|
+
scanStats
|
|
3603
3719
|
};
|
|
3604
3720
|
let blockedReason = null;
|
|
3605
3721
|
if (built.length === 0) {
|
|
@@ -3607,7 +3723,7 @@ async function stageSync(deps) {
|
|
|
3607
3723
|
} else if (token === null) {
|
|
3608
3724
|
blockedReason = "This machine is not linked. Run `npx @use-aistack/cli login` first.";
|
|
3609
3725
|
} else if (config.stack === null) {
|
|
3610
|
-
blockedReason = source === "bundled" ? "Could not fetch your settings from aistack, so the destination stack is unknown. Publish needs it. Check the network and preview again." :
|
|
3726
|
+
blockedReason = source === "bundled" ? "Could not fetch your settings from aistack, so the destination stack is unknown. Publish needs it. Check the network and preview again." : `The token resolves no destination stack. Create one at ${deps.baseUrl}/stacks/new, then sync again.`;
|
|
3611
3727
|
}
|
|
3612
3728
|
return {
|
|
3613
3729
|
id: stageId(bodyJson),
|
|
@@ -3751,6 +3867,16 @@ async function syncCommand(options = {}) {
|
|
|
3751
3867
|
process.exitCode = 1;
|
|
3752
3868
|
return;
|
|
3753
3869
|
}
|
|
3870
|
+
if (getToken() === null) {
|
|
3871
|
+
p7.log.message(
|
|
3872
|
+
"This machine is not linked to an aistack account yet \u2014 linking it first."
|
|
3873
|
+
);
|
|
3874
|
+
if (!await performLogin()) {
|
|
3875
|
+
outroError("login failed \u2014 nothing was sent");
|
|
3876
|
+
process.exitCode = 1;
|
|
3877
|
+
return;
|
|
3878
|
+
}
|
|
3879
|
+
}
|
|
3754
3880
|
const s = p7.spinner();
|
|
3755
3881
|
s.start("Scanning local agent transcripts");
|
|
3756
3882
|
let staged;
|
|
@@ -4060,7 +4186,7 @@ function runStdioSyncServer(deps) {
|
|
|
4060
4186
|
|
|
4061
4187
|
// src/index.ts
|
|
4062
4188
|
var program = new Command();
|
|
4063
|
-
program.name("aistack").description("Measure and share your AI stack from your terminal").version(
|
|
4189
|
+
program.name("aistack").description("Measure and share your AI stack from your terminal").version(CLI_VERSION);
|
|
4064
4190
|
program.command("login").description("Authenticate with AI Stack").action(loginCommand);
|
|
4065
4191
|
program.command("collect").description("Scan and upload AI config files from your project").option("--no-global", "Exclude global config files (~/.claude, etc.)").action((options) => collectCommand({ global: options.global ?? true }));
|
|
4066
4192
|
program.command("create").description("Download and write your stack's AI config files").action(createCommand);
|