@use-aistack/cli 0.10.2 → 0.11.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 CHANGED
@@ -20,17 +20,19 @@ npx @use-aistack/cli sync
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
+ After a manual sync, the CLI offers auto-sync with three choices: Enable, Maybe later, and Never ask again. Maybe later asks again after your next manual sync. Existing declines from older CLI versions are treated as Maybe later.
24
+
23
25
  ### `npx @use-aistack/cli sync --auto on` / `off`
24
26
 
25
- Optional: keep your stack fresh without manual syncs. `on` asks your stack for the permission, then writes a `SessionStart` hook into the harnesses you actually use - `~/.claude/settings.json` for Claude Code, `~/.codex/hooks.json` for Codex. The hook runs a silent sync at most once per day when a session starts. `off` removes the hooks and takes the permission back.
27
+ Optional: keep your stack fresh without manual syncs. `on` asks your stack for the permission, then writes a `SessionStart` hook into the harnesses you actually use - `~/.claude/settings.json` for Claude Code, `~/.codex/hooks.json` for Codex. The hook runs a silent sync at most once every 6 hours when a session starts. `off` removes the hooks and takes the permission back.
26
28
 
27
29
  ```sh
28
- npx @use-aistack/cli sync --auto on # enable, default every 24h
30
+ npx @use-aistack/cli sync --auto on # enable, default every 6h
29
31
  npx @use-aistack/cli sync --auto on --every 12 # custom frequency in hours
30
32
  npx @use-aistack/cli sync --auto off # revoke
31
33
  ```
32
34
 
33
- **Your stack owns the permission, not this machine.** The silent run asks aistack.to before it publishes anything, so the switch on your stack page is a complete revoke: it stops every machine, even one whose hooks are still installed. It works the other way too - turn the switch on, run one `sync`, and that machine installs its own triggers. A harness you adopt months later gets its trigger the same way.
35
+ **Your stack owns the permission, not this machine.** The silent run asks aistack.to before it publishes anything, so the switch above the first section on your stack page is a complete revoke: it stops every machine, even one whose hooks are still installed. The switch defaults to every 6 hours. Run the enable command shown beside it on each machine that should install triggers. A harness you adopt months later gets its trigger the same way.
34
36
 
35
37
  The silent run (`sync --auto`) never prompts and never installs a hook. Each run appends one line to `~/.config/aistack/sync.log` (capped at 200 lines). The next interactive `sync` reports the last result. After 3 failures in a row, one visible message appears in Claude Code and names the fix. No email, no dialogs.
36
38
 
package/dist/index.js CHANGED
@@ -348,7 +348,7 @@ function apiEquivalentCost(modelKey, t, atMs) {
348
348
  }
349
349
 
350
350
  // src/version.ts
351
- var CLI_VERSION = true ? "0.10.2" : "0.0.0-dev";
351
+ var CLI_VERSION = true ? "0.11.0" : "0.0.0-dev";
352
352
 
353
353
  // src/api.ts
354
354
  var BASE_URL = process.env.AISTACK_URL || "https://aistack.to";
@@ -6202,7 +6202,7 @@ ${e instanceof Error ? e.message : String(e)}`
6202
6202
  }
6203
6203
  return {
6204
6204
  ok: true,
6205
- message: `Installed. Say ${limeBold('"sync my stack"')} in any Claude Code session.`
6205
+ message: `Installed the user-scoped aistack MCP server and Skill. Say ${limeBold('"sync my stack"')} in any Claude Code session. Every send still requires your confirmation. Remove it with: claude mcp remove --scope user aistack`
6206
6206
  };
6207
6207
  }
6208
6208
  async function connectCommand(harness) {
@@ -6238,17 +6238,17 @@ async function offerConnectUpsell(deps = {}) {
6238
6238
  if (!await (deps.claudeActiveImpl ?? claudeRecentlyActive)()) return;
6239
6239
  if (!(deps.claudeOnPathImpl ?? claudeOnPath)()) return;
6240
6240
  const answer = await p3.select({
6241
- message: "Sync from inside Claude Code too?",
6241
+ message: "Add AI Stack commands to Claude Code? Every send still asks first.",
6242
6242
  options: [
6243
6243
  {
6244
6244
  value: "later",
6245
- label: "Not now",
6246
- hint: "this question will not come back"
6245
+ label: "No, don't ask again",
6246
+ hint: "you can install later with aistack connect claude"
6247
6247
  },
6248
6248
  {
6249
6249
  value: "install",
6250
- label: "Install",
6251
- hint: "adds the aistack MCP server + Skill to Claude Code"
6250
+ label: "Install MCP + Skill",
6251
+ hint: "user scope; enables preview and confirmed send commands"
6252
6252
  }
6253
6253
  ],
6254
6254
  initialValue: "later"
@@ -6781,7 +6781,8 @@ async function settleAutoSync(permission, deps = {}) {
6781
6781
  return offerAutoSyncOptIn(deps);
6782
6782
  }
6783
6783
  async function offerAutoSyncOptIn(deps = {}) {
6784
- if (getSettings(deps.settingsFile).autoSyncAnswered === true) return false;
6784
+ if (getSettings(deps.settingsFile).autoSyncNeverAskAgain === true)
6785
+ return false;
6785
6786
  const detected = await (deps.detectedImpl ?? detectedAdapters)();
6786
6787
  if (detected.length === 0) return false;
6787
6788
  const answer = await p6.select({
@@ -6794,8 +6795,13 @@ async function offerAutoSyncOptIn(deps = {}) {
6794
6795
  },
6795
6796
  {
6796
6797
  value: "later",
6797
- label: "No thanks",
6798
- hint: "you can enable it later"
6798
+ label: "Maybe later",
6799
+ hint: "ask again after your next manual sync"
6800
+ },
6801
+ {
6802
+ value: "never",
6803
+ label: "Never ask again",
6804
+ hint: "you can still enable it with sync --auto on"
6799
6805
  }
6800
6806
  ],
6801
6807
  initialValue: "enable"
@@ -6813,7 +6819,9 @@ async function offerAutoSyncOptIn(deps = {}) {
6813
6819
  }
6814
6820
  return true;
6815
6821
  }
6816
- saveSettings({ autoSyncAnswered: true }, deps.settingsFile);
6822
+ if (answer === "never") {
6823
+ saveSettings({ autoSyncNeverAskAgain: true }, deps.settingsFile);
6824
+ }
6817
6825
  p6.log.message(
6818
6826
  `If you change your mind: ${limeBold("npx @use-aistack/cli sync --auto on")} ${dim(
6819
6827
  "(and --auto off to revoke)"
@@ -6982,7 +6990,7 @@ function selectDaysToPublish(input) {
6982
6990
  }
6983
6991
 
6984
6992
  // src/workflow/git.ts
6985
- import { execFileSync as execFileSync2 } from "child_process";
6993
+ import { execFile, execFileSync as execFileSync2 } from "child_process";
6986
6994
  import path6 from "path";
6987
6995
  var TEST_FILE_RULE_VERSION = "test-files/v2";
6988
6996
  var FILE_TYPE_RULE_VERSION = "file-types/v2";
@@ -6999,6 +7007,18 @@ var defaultRunner = (cwd, args) => {
6999
7007
  return null;
7000
7008
  }
7001
7009
  };
7010
+ var defaultAsyncRunner = (cwd, args) => new Promise((resolve) => {
7011
+ execFile(
7012
+ "git",
7013
+ [...args],
7014
+ {
7015
+ cwd,
7016
+ encoding: "utf8",
7017
+ maxBuffer: 64 * 1024 * 1024
7018
+ },
7019
+ (error, stdout) => resolve(error ? null : stdout)
7020
+ );
7021
+ });
7002
7022
  var emptyGitDay = () => ({
7003
7023
  testFileRuleVersion: TEST_FILE_RULE_VERSION,
7004
7024
  fileTypeRuleVersion: FILE_TYPE_RULE_VERSION,
@@ -7153,6 +7173,39 @@ function extractGitWorkflow(options) {
7153
7173
  const root = run(directory, ["rev-parse", "--show-toplevel"])?.trim();
7154
7174
  if (root) roots.add(root);
7155
7175
  }
7176
+ const histories = [];
7177
+ for (const root of roots) {
7178
+ const history = run(root, gitLogArgs());
7179
+ if (history) histories.push(history);
7180
+ }
7181
+ return reduceGitHistories(histories, options);
7182
+ }
7183
+ async function extractGitWorkflowAsync(options) {
7184
+ const run = options.run ?? defaultAsyncRunner;
7185
+ const roots = /* @__PURE__ */ new Set();
7186
+ for (const directory of options.workingDirectories) {
7187
+ const root = (await run(directory, ["rev-parse", "--show-toplevel"]))?.trim();
7188
+ if (root) roots.add(root);
7189
+ }
7190
+ const histories = await Promise.all(
7191
+ [...roots].map((root) => run(root, gitLogArgs()))
7192
+ );
7193
+ return reduceGitHistories(
7194
+ histories.filter((history) => history !== null),
7195
+ options
7196
+ );
7197
+ }
7198
+ function gitLogArgs() {
7199
+ return [
7200
+ "log",
7201
+ "--all",
7202
+ "--no-merges",
7203
+ `--format=%x00${COMMIT_MARKER}%x00%H%x00%aI%x00`,
7204
+ "--numstat",
7205
+ "-z"
7206
+ ];
7207
+ }
7208
+ function reduceGitHistories(histories, options) {
7156
7209
  const days = /* @__PURE__ */ new Map();
7157
7210
  const dayOf = (date) => {
7158
7211
  let day = days.get(date);
@@ -7173,16 +7226,7 @@ function extractGitWorkflow(options) {
7173
7226
  return day;
7174
7227
  };
7175
7228
  const seenCommits = /* @__PURE__ */ new Set();
7176
- for (const root of roots) {
7177
- const history = run(root, [
7178
- "log",
7179
- "--all",
7180
- "--no-merges",
7181
- `--format=%x00${COMMIT_MARKER}%x00%H%x00%aI%x00`,
7182
- "--numstat",
7183
- "-z"
7184
- ]);
7185
- if (!history) continue;
7229
+ for (const history of histories) {
7186
7230
  let current;
7187
7231
  const finishCommit = () => {
7188
7232
  if (!current?.included || !current.authored) return;
@@ -7292,6 +7336,18 @@ function extractLocalWorkflow(options) {
7292
7336
  });
7293
7337
  return buildWorkflowExtraction(options.harnesses, git, utcOffsetMinutes);
7294
7338
  }
7339
+ async function extractLocalWorkflowAsync(options) {
7340
+ const utcOffsetMinutes = options.utcOffsetMinutes ?? machineUtcOffsetMinutes();
7341
+ const git = await extractGitWorkflowAsync({
7342
+ workingDirectories: options.harnesses.flatMap(({ local }) => [
7343
+ ...local.projectWorkspaces
7344
+ ]),
7345
+ fromMs: options.fromMs,
7346
+ toMs: options.toMs,
7347
+ utcOffsetMinutes
7348
+ });
7349
+ return buildWorkflowExtraction(options.harnesses, git, utcOffsetMinutes);
7350
+ }
7295
7351
  function machineUtcOffsetMinutes(now = /* @__PURE__ */ new Date()) {
7296
7352
  return -now.getTimezoneOffset();
7297
7353
  }
@@ -7378,20 +7434,21 @@ function buildGateDialog(ctx) {
7378
7434
  const days = payloads[0]?.window.days ?? 0;
7379
7435
  const facts = [
7380
7436
  `${fmtTokens(tokens)} tokens`,
7381
- `${days} days`,
7437
+ `${days}-day profile`,
7438
+ ...ctx.body.measuredDays && ctx.body.measuredDays.days.length !== days ? [`${ctx.body.measuredDays.days.length} historical days`] : [],
7382
7439
  ...usd === null ? [] : [fmtUSD(usd)]
7383
7440
  ].join(" \xB7 ");
7384
7441
  const n = payloads.reduce((a, p8) => a + withheldCount(p8), 0);
7385
7442
  const lines2 = [`Publish to aistack? ${facts}`];
7386
7443
  if (n > 0) {
7387
7444
  lines2.push(
7388
- keptPrivate === void 0 ? `${n} name${n === 1 ? "" : "s"} stay${n === 1 ? "s" : ""} on this machine` : `${n} name${n === 1 ? "" : "s"} go${n === 1 ? "es" : ""} up for you to review`
7445
+ keptPrivate === void 0 ? `${n} name${n === 1 ? "" : "s"} stay${n === 1 ? "s" : ""} on this machine` : `${n} private review name${n === 1 ? "" : "s"} will be stored`
7389
7446
  );
7390
7447
  }
7391
7448
  return lines2.join("\n");
7392
7449
  }
7393
7450
  var CATEGORY_LABEL = {
7394
- builtinTools: "tools",
7451
+ builtinTools: "actions",
7395
7452
  mcpServers: "mcp",
7396
7453
  skills: "skills",
7397
7454
  subagents: "agents",
@@ -7421,6 +7478,24 @@ function wrapRow(head, continuation, text, width) {
7421
7478
  if (line !== "") lines2.push(line);
7422
7479
  return lines2.map((l, i) => (i === 0 ? head : continuation) + l);
7423
7480
  }
7481
+ function wrapEntries(head, continuation, entries, width) {
7482
+ const limit = Math.max(24, width - continuation.length);
7483
+ const lines2 = [];
7484
+ let line = "";
7485
+ for (const entry of entries) {
7486
+ const next = line ? `${line} \xB7 ${entry}` : entry;
7487
+ if (line && next.length > limit) {
7488
+ lines2.push(line);
7489
+ line = entry;
7490
+ } else {
7491
+ line = next;
7492
+ }
7493
+ }
7494
+ if (line) lines2.push(line);
7495
+ return lines2.map(
7496
+ (line2, index) => `${index === 0 ? head : continuation}${line2}`
7497
+ );
7498
+ }
7424
7499
  function keptPrivateRows(keptPrivate) {
7425
7500
  const groups = /* @__PURE__ */ new Map();
7426
7501
  const singles = [];
@@ -7516,23 +7591,20 @@ function payloadBlock(payload, width, ownWindow, stats) {
7516
7591
  }
7517
7592
  if (entries.length > 0) {
7518
7593
  out.push(
7519
- ...wrapRow(
7520
- "models ",
7521
- " ".repeat(LABEL_WIDTH),
7522
- entries.join(" \xB7 "),
7523
- width
7524
- )
7594
+ ...wrapEntries("models ", " ".repeat(LABEL_WIDTH), entries, width)
7525
7595
  );
7526
7596
  }
7527
7597
  const filled = NAME_CATEGORIES.filter(
7528
7598
  (category) => payload.inventory[category].length > 0
7529
7599
  );
7530
7600
  if (filled.length === 0) {
7531
- out.push(`${"publishes".padEnd(LABEL_WIDTH)}no names from this harness`);
7601
+ out.push(
7602
+ `${"sends".padEnd(LABEL_WIDTH)}no inventory names from this harness`
7603
+ );
7532
7604
  return out;
7533
7605
  }
7534
7606
  out.push(
7535
- `${"publishes".padEnd(LABEL_WIDTH)}${filled.map(
7607
+ `${"sends".padEnd(LABEL_WIDTH)}${filled.map(
7536
7608
  (category) => `${payload.inventory[category].length} ${CATEGORY_LABEL[category]}`
7537
7609
  ).join(" \xB7 ")}`
7538
7610
  );
@@ -7648,6 +7720,14 @@ function buildGateSummary(ctx) {
7648
7720
  out.push(`prices ${origin} \xB7 cites ${cited.join(", ")}`);
7649
7721
  }
7650
7722
  const width = wrapWidth(ctx.width);
7723
+ out.push(
7724
+ ...wrapRow(
7725
+ "privacy ",
7726
+ " ".repeat(LABEL_WIDTH),
7727
+ "raw conversation text, paths, repo names, and command arguments stay local",
7728
+ width
7729
+ )
7730
+ );
7651
7731
  for (const payload of payloads) {
7652
7732
  const stats = ctx.scanStats?.[payload.harness.name];
7653
7733
  out.push("", DIVIDER, "");
@@ -7675,10 +7755,12 @@ function buildGateSummary(ctx) {
7675
7755
  const shown = rows.slice(0, KEPT_PRIVATE_ROWS_SHOWN);
7676
7756
  const examples = shown.map((r) => r.names > 1 ? `${r.label} \xD7${r.names}` : r.label).join(", ");
7677
7757
  const more = rows.length > shown.length ? `, ...${rows.length - shown.length} more` : "";
7678
- out.push(`private ${n} name${n === 1 ? "" : "s"} \xB7 ${examples}${more}`);
7758
+ out.push(
7759
+ `private ${n} review name${n === 1 ? "" : "s"} \xB7 ${examples}${more}`
7760
+ );
7679
7761
  if (body.keptPrivate !== void 0 && config.stack !== null) {
7680
7762
  out.push(
7681
- ` they go up for you to review at ${host}/stacks/${config.stack.slug}/changes`
7763
+ ` stored privately for your review at ${host}/stacks/${config.stack.slug}/changes`
7682
7764
  );
7683
7765
  out.push(
7684
7766
  " (turn off: Review kept-private names, on your stack)"
@@ -7718,10 +7800,13 @@ async function stageSync(deps) {
7718
7800
  const projectWorkspaceId = deps.getProjectWorkspaceIdImpl ?? getProjectWorkspaceId;
7719
7801
  const fetchManifest = deps.fetchManifestImpl ?? fetchDayManifest;
7720
7802
  const fetchPrices = deps.fetchPricesImpl ?? fetchPriceTable;
7803
+ const progress = deps.onProgress ?? (() => {
7804
+ });
7721
7805
  let prices = {
7722
7806
  id: BUNDLED_PRICE_TABLE_ID,
7723
7807
  origin: "bundled"
7724
7808
  };
7809
+ progress("Checking prices and stack settings");
7725
7810
  try {
7726
7811
  const table = await fetchPrices(deps.baseUrl);
7727
7812
  if (table) {
@@ -7757,7 +7842,11 @@ async function stageSync(deps) {
7757
7842
  const daysSinceMs = windowStartMs(now, retentionDays);
7758
7843
  const active2 = await adapters(sinceMs);
7759
7844
  for (const adapter of active2) {
7760
- const { aggregate, stats } = await adapter.scan({ sinceMs });
7845
+ progress(`Scanning recent ${adapter.name} usage`);
7846
+ const { aggregate, stats } = await adapter.scan({
7847
+ sinceMs,
7848
+ onProgress: (files) => progress(`Scanning recent ${adapter.name} usage \xB7 ${files} files`)
7849
+ });
7761
7850
  scanStats[adapter.name] = stats;
7762
7851
  built.push(
7763
7852
  buildPayload({
@@ -7773,8 +7862,10 @@ async function stageSync(deps) {
7773
7862
  );
7774
7863
  }
7775
7864
  for (const adapter of active2) {
7865
+ progress(`Reading historical ${adapter.name} days`);
7776
7866
  const { aggregate, workflow: workflow2, workflowLocal } = await adapter.scan({
7777
- sinceMs: daysSinceMs
7867
+ sinceMs: daysSinceMs,
7868
+ onProgress: (files) => progress(`Reading historical ${adapter.name} days \xB7 ${files} files`)
7778
7869
  });
7779
7870
  workflowScans.push({ aggregate: workflow2, local: workflowLocal });
7780
7871
  usageScans.push(
@@ -7787,12 +7878,20 @@ async function stageSync(deps) {
7787
7878
  );
7788
7879
  }
7789
7880
  const settings = (deps.getSettingsImpl ?? getSettings)();
7790
- const workflow = workflowScans.length > 0 && config.publishWorkflow ? extractLocalWorkflow({
7791
- harnesses: workflowScans,
7792
- fromMs: daysSinceMs,
7793
- toMs: now,
7794
- ...deps.gitRunnerImpl ? { run: deps.gitRunnerImpl } : {}
7795
- }) : void 0;
7881
+ let workflow;
7882
+ if (workflowScans.length > 0 && config.publishWorkflow) {
7883
+ progress("Reading Git history");
7884
+ workflow = deps.gitRunnerImpl ? extractLocalWorkflow({
7885
+ harnesses: workflowScans,
7886
+ fromMs: daysSinceMs,
7887
+ toMs: now,
7888
+ run: deps.gitRunnerImpl
7889
+ }) : await extractLocalWorkflowAsync({
7890
+ harnesses: workflowScans,
7891
+ fromMs: daysSinceMs,
7892
+ toMs: now
7893
+ });
7894
+ }
7796
7895
  const localDays = applyDayConsent(
7797
7896
  buildMeasuredDays({
7798
7897
  usage: mergeUsageDays(usageScans),
@@ -7819,6 +7918,7 @@ async function stageSync(deps) {
7819
7918
  } : void 0,
7820
7919
  CLI_VERSION
7821
7920
  );
7921
+ progress("Preparing review");
7822
7922
  const bodyJson = JSON.stringify(body);
7823
7923
  const keptPrivate = mergeKeptPrivate(built.map((b) => b.keptPrivate));
7824
7924
  const ctx = {
@@ -8090,7 +8190,8 @@ async function syncCommand(options = {}) {
8090
8190
  staged = await stageSync({
8091
8191
  baseUrl: BASE_URL,
8092
8192
  getTokenImpl: () => destinationToken,
8093
- loadConfigImpl: async () => destinationConfig
8193
+ loadConfigImpl: async () => destinationConfig,
8194
+ onProgress: (message) => s.message(message)
8094
8195
  });
8095
8196
  } catch (e) {
8096
8197
  s.stop("Scan failed");
@@ -8133,7 +8234,12 @@ async function syncCommand(options = {}) {
8133
8234
  );
8134
8235
  } else if (res.keptPrivate.stored > 0) {
8135
8236
  lines2.push(
8136
- `${res.keptPrivate.stored} kept-private names went up for your review at ${res.url}/changes`
8237
+ `${res.keptPrivate.stored} private review name${res.keptPrivate.stored === 1 ? "" : "s"} stored at ${res.url}/changes`
8238
+ );
8239
+ }
8240
+ if (res.keptPrivate.machineStored > 0) {
8241
+ lines2.push(
8242
+ "This machine's private label was stored for the same review."
8137
8243
  );
8138
8244
  }
8139
8245
  p7.log.message(lines2.join("\n"));
@@ -8332,7 +8438,12 @@ function createSyncServer(deps, send) {
8332
8438
  );
8333
8439
  } else if (res.keptPrivate.stored > 0) {
8334
8440
  lines2.push(
8335
- `${res.keptPrivate.stored} kept-private names went up for your review at ${res.url}/changes`
8441
+ `${res.keptPrivate.stored} private review name${res.keptPrivate.stored === 1 ? "" : "s"} stored at ${res.url}/changes`
8442
+ );
8443
+ }
8444
+ if (res.keptPrivate.machineStored > 0) {
8445
+ lines2.push(
8446
+ "This machine's private label was stored for the same review."
8336
8447
  );
8337
8448
  }
8338
8449
  ok(id, textResult(lines2.join("\n")));