@use-aistack/cli 0.4.0 → 0.5.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,6 +20,18 @@ npx @use-aistack/cli sync
20
20
 
21
21
  Requires a one-time `login` first.
22
22
 
23
+ ### `npx @use-aistack/cli sync --auto on` / `off`
24
+
25
+ Optional: keep your stack fresh without manual syncs. `on` writes a `SessionStart` hook (`async: true`) into `~/.claude/settings.json`. The hook runs a silent sync at most once per day when a Claude Code session starts. `off` removes the hook and revokes the standing opt-in.
26
+
27
+ ```sh
28
+ npx @use-aistack/cli sync --auto on # enable, default every 24h
29
+ npx @use-aistack/cli sync --auto on --every 12 # custom frequency in hours
30
+ npx @use-aistack/cli sync --auto off # revoke
31
+ ```
32
+
33
+ The silent run (`sync --auto`) never prompts and publishes only under this opt-in. 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.
34
+
23
35
  ### `npx @use-aistack/cli login`
24
36
 
25
37
  Link this machine to your AI Stack account via browser.
package/dist/index.js CHANGED
@@ -182,39 +182,67 @@ function classify(files) {
182
182
  // src/config.ts
183
183
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
184
184
  import { homedir } from "os";
185
- import { join } from "path";
185
+ import { dirname as dirname2, join } from "path";
186
186
  var CONFIG_DIR = join(homedir(), ".config", "aistack");
187
187
  var CREDENTIALS_FILE = join(CONFIG_DIR, "credentials.json");
188
- function getToken() {
189
- if (!existsSync(CREDENTIALS_FILE)) return null;
188
+ var DEFAULT_SERVER_URL = "https://aistack.to";
189
+ function readCredentials(file) {
190
+ const empty = { data: { servers: {} }, legacy: false };
191
+ if (!existsSync(file)) return empty;
190
192
  try {
191
- const data = JSON.parse(
192
- readFileSync(CREDENTIALS_FILE, "utf-8")
193
- );
194
- return data.token ?? null;
193
+ const raw = JSON.parse(readFileSync(file, "utf-8"));
194
+ if (!raw || typeof raw !== "object") return empty;
195
+ if (raw.servers && typeof raw.servers === "object") {
196
+ return {
197
+ data: { servers: raw.servers },
198
+ legacy: false
199
+ };
200
+ }
201
+ if (typeof raw.token === "string" && raw.token) {
202
+ return {
203
+ data: {
204
+ servers: {
205
+ [DEFAULT_SERVER_URL]: { token: raw.token, userId: raw.userId }
206
+ }
207
+ },
208
+ legacy: true
209
+ };
210
+ }
211
+ return { data: { servers: {} }, legacy: true };
195
212
  } catch {
196
- return null;
213
+ return empty;
197
214
  }
198
215
  }
199
- function saveToken(token, userId) {
200
- mkdirSync(CONFIG_DIR, { recursive: true });
201
- writeFileSync(CREDENTIALS_FILE, JSON.stringify({ token, userId }, null, 2));
216
+ function writeCredentials(file, data) {
217
+ mkdirSync(dirname2(file), { recursive: true });
218
+ writeFileSync(file, JSON.stringify(data, null, 2));
219
+ }
220
+ function getToken(serverUrl = BASE_URL, file = CREDENTIALS_FILE) {
221
+ const { data, legacy } = readCredentials(file);
222
+ if (legacy) writeCredentials(file, data);
223
+ return data.servers[serverUrl]?.token ?? null;
224
+ }
225
+ function saveToken(token, userId, serverUrl = BASE_URL, file = CREDENTIALS_FILE) {
226
+ const { data } = readCredentials(file);
227
+ data.servers[serverUrl] = { token, userId };
228
+ writeCredentials(file, data);
202
229
  }
203
230
  var SETTINGS_FILE = join(CONFIG_DIR, "settings.json");
204
- function getSettings() {
205
- if (!existsSync(SETTINGS_FILE)) return {};
231
+ var DEFAULT_FREQUENCY_HOURS = 24;
232
+ function getSettings(file = SETTINGS_FILE) {
233
+ if (!existsSync(file)) return {};
206
234
  try {
207
- const raw = JSON.parse(readFileSync(SETTINGS_FILE, "utf-8"));
235
+ const raw = JSON.parse(readFileSync(file, "utf-8"));
208
236
  return raw && typeof raw === "object" ? raw : {};
209
237
  } catch {
210
238
  return {};
211
239
  }
212
240
  }
213
- function saveSettings(patch) {
214
- mkdirSync(CONFIG_DIR, { recursive: true });
241
+ function saveSettings(patch, file = SETTINGS_FILE) {
242
+ mkdirSync(dirname2(file), { recursive: true });
215
243
  writeFileSync(
216
- SETTINGS_FILE,
217
- JSON.stringify({ ...getSettings(), ...patch }, null, 2)
244
+ file,
245
+ JSON.stringify({ ...getSettings(file), ...patch }, null, 2)
218
246
  );
219
247
  }
220
248
  var PROJECTS_FILE = join(CONFIG_DIR, "projects.json");
@@ -650,7 +678,7 @@ function resolvePluginLinks(installed, marketplaces, manifests) {
650
678
  const marketplace = key.slice(at + 1);
651
679
  const mpRepoUrl = marketplaceRepoUrl(marketplaces[marketplace]);
652
680
  const entry = manifests[marketplace]?.plugins?.find(
653
- (p7) => p7.name === pluginName
681
+ (p8) => p8.name === pluginName
654
682
  );
655
683
  if (!entry) continue;
656
684
  const resolved = resolveSource(entry, mpRepoUrl);
@@ -1289,7 +1317,7 @@ function diffResources(current, existing) {
1289
1317
  import { spawnSync } from "child_process";
1290
1318
  import { cpSync, existsSync as existsSync6 } from "fs";
1291
1319
  import { homedir as homedir6 } from "os";
1292
- import { dirname as dirname2, join as join6 } from "path";
1320
+ import { dirname as dirname3, join as join6 } from "path";
1293
1321
  import { fileURLToPath } from "url";
1294
1322
  import * as p3 from "@clack/prompts";
1295
1323
  var MANUAL_MCP_ADD = "claude mcp add --scope user aistack -- npx -y @use-aistack/cli mcp";
@@ -1319,12 +1347,12 @@ function runClaude(args) {
1319
1347
  function claudeOnPath(run = runClaude) {
1320
1348
  return !run(["--version"]).notFound;
1321
1349
  }
1322
- function findSkillSource(fromDir = dirname2(fileURLToPath(import.meta.url))) {
1350
+ function findSkillSource(fromDir = dirname3(fileURLToPath(import.meta.url))) {
1323
1351
  let dir = fromDir;
1324
1352
  for (let i = 0; i < 4; i++) {
1325
1353
  const candidate = join6(dir, "skills", "aistack-sync");
1326
1354
  if (existsSync6(join6(candidate, "SKILL.md"))) return candidate;
1327
- const parent = dirname2(dir);
1355
+ const parent = dirname3(dir);
1328
1356
  if (parent === dir) break;
1329
1357
  dir = parent;
1330
1358
  }
@@ -1431,7 +1459,7 @@ async function offerConnectUpsell() {
1431
1459
 
1432
1460
  // src/commands/create.ts
1433
1461
  import { existsSync as existsSync7, mkdirSync as mkdirSync2, readFileSync as readFileSync6, writeFileSync as writeFileSync2 } from "fs";
1434
- import { dirname as dirname3, join as join7 } from "path";
1462
+ import { dirname as dirname4, join as join7 } from "path";
1435
1463
  import * as p4 from "@clack/prompts";
1436
1464
  async function createCommand() {
1437
1465
  intro2("create");
@@ -1521,7 +1549,7 @@ async function createCommand() {
1521
1549
  }
1522
1550
  for (const f of toWrite) {
1523
1551
  const fullPath = join7(cwd, f.path);
1524
- const dir = dirname3(fullPath);
1552
+ const dir = dirname4(fullPath);
1525
1553
  mkdirSync2(dir, { recursive: true });
1526
1554
  writeFileSync2(fullPath, f.content);
1527
1555
  }
@@ -1602,8 +1630,166 @@ async function loginCommand() {
1602
1630
  }
1603
1631
 
1604
1632
  // src/commands/sync.ts
1633
+ import * as p7 from "@clack/prompts";
1634
+
1635
+ // src/autosync/optin.ts
1605
1636
  import * as p6 from "@clack/prompts";
1606
1637
 
1638
+ // src/autosync/hook.ts
1639
+ import { existsSync as existsSync8, mkdirSync as mkdirSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync3 } from "fs";
1640
+ import { homedir as homedir7 } from "os";
1641
+ import { dirname as dirname5, join as join8 } from "path";
1642
+ var CLAUDE_SETTINGS_FILE = join8(homedir7(), ".claude", "settings.json");
1643
+ var AUTO_SYNC_HOOK_COMMAND = "npx -y @use-aistack/cli@latest sync --auto || npx -y --prefer-offline @use-aistack/cli sync --auto";
1644
+ function isOurs(entry) {
1645
+ return typeof entry.command === "string" && entry.command.includes("@use-aistack/cli") && entry.command.includes("sync --auto");
1646
+ }
1647
+ function readClaudeSettings(file) {
1648
+ if (!existsSync8(file)) return { settings: {} };
1649
+ try {
1650
+ const raw = JSON.parse(readFileSync7(file, "utf-8"));
1651
+ if (raw && typeof raw === "object" && !Array.isArray(raw)) {
1652
+ return { settings: raw };
1653
+ }
1654
+ return { error: `${file} does not hold a JSON object` };
1655
+ } catch {
1656
+ return { error: `${file} is not valid JSON \u2014 fix it, then retry` };
1657
+ }
1658
+ }
1659
+ function installAutoSyncHook(file = CLAUDE_SETTINGS_FILE) {
1660
+ const read = readClaudeSettings(file);
1661
+ if ("error" in read) return { ok: false, message: read.error };
1662
+ const settings = read.settings;
1663
+ const hooks = settings.hooks ?? {};
1664
+ const sessionStart = Array.isArray(hooks.SessionStart) ? hooks.SessionStart : [];
1665
+ const kept = sessionStart.map((m) => ({
1666
+ ...m,
1667
+ hooks: (m.hooks ?? []).filter((h) => !isOurs(h))
1668
+ })).filter((m) => (m.hooks?.length ?? 0) > 0);
1669
+ kept.push({
1670
+ hooks: [{ type: "command", command: AUTO_SYNC_HOOK_COMMAND, async: true }]
1671
+ });
1672
+ settings.hooks = { ...hooks, SessionStart: kept };
1673
+ mkdirSync3(dirname5(file), { recursive: true });
1674
+ writeFileSync3(file, `${JSON.stringify(settings, null, 2)}
1675
+ `);
1676
+ return { ok: true, message: `SessionStart hook written to ${file}` };
1677
+ }
1678
+ function removeAutoSyncHook(file = CLAUDE_SETTINGS_FILE) {
1679
+ if (!existsSync8(file)) return { ok: true, message: "no hook to remove" };
1680
+ const read = readClaudeSettings(file);
1681
+ if ("error" in read) return { ok: false, message: read.error };
1682
+ const settings = read.settings;
1683
+ const sessionStart = settings.hooks?.SessionStart;
1684
+ if (!Array.isArray(sessionStart)) {
1685
+ return { ok: true, message: "no hook to remove" };
1686
+ }
1687
+ const kept = sessionStart.map((m) => ({
1688
+ ...m,
1689
+ hooks: (m.hooks ?? []).filter((h) => !isOurs(h))
1690
+ })).filter((m) => (m.hooks?.length ?? 0) > 0);
1691
+ const hooks = { ...settings.hooks };
1692
+ if (kept.length > 0) {
1693
+ hooks.SessionStart = kept;
1694
+ } else {
1695
+ delete hooks.SessionStart;
1696
+ }
1697
+ if (Object.keys(hooks).length > 0) {
1698
+ settings.hooks = hooks;
1699
+ } else {
1700
+ delete settings.hooks;
1701
+ }
1702
+ writeFileSync3(file, `${JSON.stringify(settings, null, 2)}
1703
+ `);
1704
+ return { ok: true, message: `hook removed from ${file}` };
1705
+ }
1706
+
1707
+ // src/autosync/optin.ts
1708
+ function enableAutoSync(frequencyHours = DEFAULT_FREQUENCY_HOURS, deps = {}) {
1709
+ const install = deps.installHook ?? installAutoSyncHook;
1710
+ const result = install();
1711
+ if (!result.ok) return result;
1712
+ saveSettings(
1713
+ {
1714
+ autoSyncAnswered: true,
1715
+ autoSync: { enabled: true, frequencyHours }
1716
+ },
1717
+ deps.settingsFile
1718
+ );
1719
+ return {
1720
+ ok: true,
1721
+ message: `Auto-sync is on \u2014 about every ${frequencyHours}h when a Claude Code session starts. Turn it off any time: npx @use-aistack/cli sync --auto off`
1722
+ };
1723
+ }
1724
+ function disableAutoSync(deps = {}) {
1725
+ const remove = deps.removeHook ?? removeAutoSyncHook;
1726
+ const settings = getSettings(deps.settingsFile);
1727
+ saveSettings(
1728
+ {
1729
+ autoSyncAnswered: true,
1730
+ autoSync: {
1731
+ enabled: false,
1732
+ frequencyHours: settings.autoSync?.frequencyHours ?? DEFAULT_FREQUENCY_HOURS
1733
+ }
1734
+ },
1735
+ deps.settingsFile
1736
+ );
1737
+ const result = remove();
1738
+ if (!result.ok) {
1739
+ return {
1740
+ ok: false,
1741
+ message: `Auto-sync is off (nothing will publish), but the hook could not be removed: ${result.message}`
1742
+ };
1743
+ }
1744
+ return { ok: true, message: "Auto-sync is off. The hook was removed." };
1745
+ }
1746
+ async function offerAutoSyncOptIn() {
1747
+ if (getSettings().autoSyncAnswered === true) return false;
1748
+ const answer = await p6.select({
1749
+ message: "Keep this stack fresh automatically?",
1750
+ options: [
1751
+ {
1752
+ value: "later",
1753
+ label: "Not now",
1754
+ hint: "this question will not come back"
1755
+ },
1756
+ {
1757
+ value: "enable",
1758
+ label: "Enable",
1759
+ hint: "a silent daily sync when a Claude Code session starts"
1760
+ }
1761
+ ],
1762
+ initialValue: "later"
1763
+ });
1764
+ if (p6.isCancel(answer)) return true;
1765
+ if (answer === "enable") {
1766
+ const result = enableAutoSync();
1767
+ if (result.ok) {
1768
+ p6.log.success(result.message);
1769
+ } else {
1770
+ p6.log.error(result.message);
1771
+ }
1772
+ return true;
1773
+ }
1774
+ saveSettings({ autoSyncAnswered: true });
1775
+ p6.log.message(
1776
+ `If you change your mind: ${limeBold("npx @use-aistack/cli sync --auto on")} ${dim(
1777
+ "(and --auto off to revoke)"
1778
+ )}`
1779
+ );
1780
+ return true;
1781
+ }
1782
+
1783
+ // src/autosync/run.ts
1784
+ import {
1785
+ appendFileSync,
1786
+ mkdirSync as mkdirSync4,
1787
+ readFileSync as readFileSync8,
1788
+ writeFileSync as writeFileSync4
1789
+ } from "fs";
1790
+ import { homedir as homedir9 } from "os";
1791
+ import { dirname as dirname6, join as join9 } from "path";
1792
+
1607
1793
  // src/sync/stage.ts
1608
1794
  import { createHash } from "crypto";
1609
1795
 
@@ -1643,9 +1829,9 @@ function priceAt(modelKey, atMs) {
1643
1829
  if (atMs === null) return null;
1644
1830
  const periods = PRICES[modelKey];
1645
1831
  if (!periods) return null;
1646
- for (const p7 of periods) {
1647
- if ((p7.from === null || atMs >= p7.from) && (p7.to === null || atMs < p7.to)) {
1648
- return p7;
1832
+ for (const p8 of periods) {
1833
+ if ((p8.from === null || atMs >= p8.from) && (p8.to === null || atMs < p8.to)) {
1834
+ return p8;
1649
1835
  }
1650
1836
  }
1651
1837
  return null;
@@ -1654,10 +1840,10 @@ function isPricedModel(modelKey) {
1654
1840
  return PRICES[modelKey] !== void 0;
1655
1841
  }
1656
1842
  function apiEquivalentCost(modelKey, t, atMs) {
1657
- const p7 = priceAt(modelKey, atMs);
1658
- if (!p7) return null;
1843
+ const p8 = priceAt(modelKey, atMs);
1844
+ if (!p8) return null;
1659
1845
  const M = 1e6;
1660
- return (t.input * p7.input + t.output * p7.output + (t.cacheWrite5m + t.cacheWriteUnsplit) * p7.input * CACHE_WRITE_5M_MULTIPLIER + t.cacheWrite1h * p7.input * CACHE_WRITE_1H_MULTIPLIER + t.cacheRead * p7.input * CACHE_READ_MULTIPLIER) / M;
1846
+ return (t.input * p8.input + t.output * p8.output + (t.cacheWrite5m + t.cacheWriteUnsplit) * p8.input * CACHE_WRITE_5M_MULTIPLIER + t.cacheWrite1h * p8.input * CACHE_WRITE_1H_MULTIPLIER + t.cacheRead * p8.input * CACHE_READ_MULTIPLIER) / M;
1661
1847
  }
1662
1848
 
1663
1849
  // src/transcripts/analyzer.ts
@@ -2024,7 +2210,7 @@ function newestVersion(versions) {
2024
2210
  let best = null;
2025
2211
  let bestParts = [];
2026
2212
  for (const v of versions) {
2027
- const parts = v.split(".").map((p7) => Number.parseInt(p7, 10));
2213
+ const parts = v.split(".").map((p8) => Number.parseInt(p8, 10));
2028
2214
  if (parts.some((n) => !Number.isFinite(n))) continue;
2029
2215
  if (best === null || compareParts(parts, bestParts) > 0) {
2030
2216
  best = v;
@@ -2385,7 +2571,7 @@ function filterAtoms(atoms, sets) {
2385
2571
  // src/transcripts/scan.ts
2386
2572
  import { createReadStream } from "fs";
2387
2573
  import { readdir, realpath, stat } from "fs/promises";
2388
- import { homedir as homedir7 } from "os";
2574
+ import { homedir as homedir8 } from "os";
2389
2575
  import path from "path";
2390
2576
  import readline from "readline";
2391
2577
  function transcriptRoots() {
@@ -2393,8 +2579,8 @@ function transcriptRoots() {
2393
2579
  if (env) {
2394
2580
  return env.split(",").map((s) => s.trim()).filter(Boolean).map((s) => path.join(s, "projects"));
2395
2581
  }
2396
- const roots = [path.join(homedir7(), ".claude", "projects")];
2397
- const xdg = process.env.XDG_CONFIG_HOME ?? path.join(homedir7(), ".config");
2582
+ const roots = [path.join(homedir8(), ".claude", "projects")];
2583
+ const xdg = process.env.XDG_CONFIG_HOME ?? path.join(homedir8(), ".config");
2398
2584
  roots.push(path.join(xdg, "claude", "projects"));
2399
2585
  return roots;
2400
2586
  }
@@ -2468,9 +2654,9 @@ async function scan(agg, opts = {}) {
2468
2654
  }
2469
2655
  return stats;
2470
2656
  }
2471
- async function exists(p7) {
2657
+ async function exists(p8) {
2472
2658
  try {
2473
- await stat(p7);
2659
+ await stat(p8);
2474
2660
  return true;
2475
2661
  } catch {
2476
2662
  return false;
@@ -2889,15 +3075,132 @@ async function stageSync(deps) {
2889
3075
  };
2890
3076
  }
2891
3077
 
3078
+ // src/autosync/run.ts
3079
+ var SYNC_LOG_FILE = join9(homedir9(), ".config", "aistack", "sync.log");
3080
+ var SYNC_LOG_MAX_LINES = 200;
3081
+ var FIX_COMMAND = "npx @use-aistack/cli sync";
3082
+ function appendLogLine(file, line) {
3083
+ mkdirSync4(dirname6(file), { recursive: true });
3084
+ appendFileSync(file, `${line}
3085
+ `);
3086
+ const lines2 = readFileSync8(file, "utf-8").split("\n").filter(Boolean);
3087
+ if (lines2.length > SYNC_LOG_MAX_LINES) {
3088
+ writeFileSync4(file, `${lines2.slice(-SYNC_LOG_MAX_LINES).join("\n")}
3089
+ `);
3090
+ }
3091
+ }
3092
+ async function runAutoSync(deps) {
3093
+ const now = (deps.now ?? Date.now)();
3094
+ const settingsFile = deps.settingsFile;
3095
+ const logFile = deps.logFile ?? SYNC_LOG_FILE;
3096
+ const emit = deps.emit ?? ((line) => process.stdout.write(`${line}
3097
+ `));
3098
+ const stamp = new Date(now).toISOString();
3099
+ const settings = getSettings(settingsFile);
3100
+ const config = settings.autoSync;
3101
+ if (config?.enabled !== true) {
3102
+ appendLogLine(logFile, `${stamp} skipped \u2014 auto-sync is not enabled`);
3103
+ return;
3104
+ }
3105
+ const frequencyHours = config.frequencyHours || DEFAULT_FREQUENCY_HOURS;
3106
+ const state = settings.autoSyncState ?? {};
3107
+ const lastRunAt = state.lastRunAt ?? 0;
3108
+ if (now - lastRunAt < frequencyHours * 36e5) return;
3109
+ const stage = deps.stageImpl ?? stageSync;
3110
+ const publish = deps.publishImpl ?? syncPublish;
3111
+ let failure2 = null;
3112
+ let url;
3113
+ try {
3114
+ const staged = await stage({ baseUrl: deps.baseUrl, now: () => now });
3115
+ if (staged.blockedReason !== null) {
3116
+ failure2 = staged.blockedReason;
3117
+ } else {
3118
+ const res = await publish(staged.token, staged.bodyJson);
3119
+ url = res.url;
3120
+ }
3121
+ } catch (e) {
3122
+ failure2 = e instanceof Error ? e.message : String(e);
3123
+ }
3124
+ if (failure2 === null) {
3125
+ saveSettings(
3126
+ {
3127
+ autoSyncState: {
3128
+ lastRunAt: now,
3129
+ lastSuccessAt: now,
3130
+ lastResult: `ok \u2014 published at ${stamp}`,
3131
+ consecutiveFailures: 0,
3132
+ failureWarned: false
3133
+ }
3134
+ },
3135
+ settingsFile
3136
+ );
3137
+ appendLogLine(logFile, `${stamp} ok \u2014 published${url ? ` ${url}` : ""}`);
3138
+ return;
3139
+ }
3140
+ const consecutiveFailures = (state.consecutiveFailures ?? 0) + 1;
3141
+ const shouldWarn = consecutiveFailures >= 3 && state.failureWarned !== true;
3142
+ saveSettings(
3143
+ {
3144
+ autoSyncState: {
3145
+ ...state,
3146
+ lastRunAt: now,
3147
+ lastResult: `failed at ${stamp} \u2014 ${failure2}`,
3148
+ consecutiveFailures,
3149
+ failureWarned: state.failureWarned === true || shouldWarn
3150
+ }
3151
+ },
3152
+ settingsFile
3153
+ );
3154
+ appendLogLine(
3155
+ logFile,
3156
+ `${stamp} fail (${consecutiveFailures} in a row) \u2014 ${failure2}`
3157
+ );
3158
+ if (shouldWarn) {
3159
+ emit(
3160
+ JSON.stringify({
3161
+ systemMessage: `aistack auto-sync failed ${consecutiveFailures} times in a row (${failure2}). Run \`${FIX_COMMAND}\` in a terminal to fix it, or \`${FIX_COMMAND} --auto off\` to stop these runs.`
3162
+ })
3163
+ );
3164
+ }
3165
+ }
3166
+
2892
3167
  // src/commands/sync.ts
2893
- async function syncCommand() {
3168
+ async function syncCommand(options = {}) {
3169
+ if (options.auto === true) {
3170
+ await runAutoSync({ baseUrl: BASE_URL });
3171
+ return;
3172
+ }
3173
+ if (options.auto === "on" || options.auto === "off") {
3174
+ intro2("sync");
3175
+ const result = options.auto === "on" ? enableAutoSync(
3176
+ options.every ? Number.parseInt(options.every, 10) || DEFAULT_FREQUENCY_HOURS : DEFAULT_FREQUENCY_HOURS
3177
+ ) : disableAutoSync();
3178
+ if (result.ok) {
3179
+ p7.log.success(result.message);
3180
+ outro2("done");
3181
+ } else {
3182
+ outroError(result.message);
3183
+ process.exitCode = 1;
3184
+ }
3185
+ return;
3186
+ }
3187
+ if (options.auto !== void 0) {
3188
+ intro2("sync");
3189
+ outroError(`unknown --auto value "${options.auto}" \u2014 use on or off`);
3190
+ process.exitCode = 1;
3191
+ return;
3192
+ }
2894
3193
  intro2("sync");
3194
+ const lastAuto = getSettings().autoSyncState?.lastResult;
3195
+ if (lastAuto !== void 0) {
3196
+ p7.log.message(dim(`auto-sync: ${lastAuto}`));
3197
+ }
2895
3198
  if (!process.stdin.isTTY || !process.stdout.isTTY) {
2896
3199
  outroError("sync needs an interactive terminal \u2014 nothing was sent");
2897
3200
  process.exitCode = 1;
2898
3201
  return;
2899
3202
  }
2900
- const s = p6.spinner();
3203
+ const s = p7.spinner();
2901
3204
  s.start("Scanning local Claude Code transcripts");
2902
3205
  let staged;
2903
3206
  try {
@@ -2909,13 +3212,13 @@ async function syncCommand() {
2909
3212
  return;
2910
3213
  }
2911
3214
  s.stop("Scan complete");
2912
- p6.log.message(staged.summary.split("\n").join("\n"));
3215
+ p7.log.message(staged.summary.split("\n").join("\n"));
2913
3216
  if (staged.blockedReason !== null) {
2914
3217
  outroError(staged.blockedReason);
2915
3218
  process.exitCode = 1;
2916
3219
  return;
2917
3220
  }
2918
- const decision = await p6.select({
3221
+ const decision = await p7.select({
2919
3222
  message: staged.dialog.split("\n").join(dim(" \xB7 ")),
2920
3223
  options: [
2921
3224
  { value: "cancel", label: "Cancel", hint: "nothing leaves this machine" },
@@ -2923,7 +3226,7 @@ async function syncCommand() {
2923
3226
  ],
2924
3227
  initialValue: "cancel"
2925
3228
  });
2926
- if (p6.isCancel(decision) || decision !== "publish") {
3229
+ if (p7.isCancel(decision) || decision !== "publish") {
2927
3230
  outroCancel("nothing was sent");
2928
3231
  return;
2929
3232
  }
@@ -2944,8 +3247,9 @@ async function syncCommand() {
2944
3247
  `${res.keptPrivate.stored} kept-private names went up for your review at ${res.url}/changes`
2945
3248
  );
2946
3249
  }
2947
- p6.log.message(lines2.join("\n"));
2948
- await offerConnectUpsell();
3250
+ p7.log.message(lines2.join("\n"));
3251
+ const asked = await offerAutoSyncOptIn();
3252
+ if (!asked) await offerConnectUpsell();
2949
3253
  outro2("done");
2950
3254
  } catch (e) {
2951
3255
  s.stop("Publish failed");
@@ -2996,7 +3300,7 @@ function createSyncServer(deps, send) {
2996
3300
  const now = deps.now ?? Date.now;
2997
3301
  const stage = deps.stageImpl ?? stageSync;
2998
3302
  const publish = deps.publishImpl ?? syncPublish;
2999
- const log6 = deps.log ?? (() => {
3303
+ const log7 = deps.log ?? (() => {
3000
3304
  });
3001
3305
  const elicitTimeoutMs = deps.elicitTimeoutMs ?? ELICIT_TIMEOUT_MS;
3002
3306
  let clientSupportsElicitation = false;
@@ -3076,7 +3380,7 @@ function createSyncServer(deps, send) {
3076
3380
  );
3077
3381
  }
3078
3382
  const approvedStage = staged;
3079
- log6(`elicitation raised for stage ${approvedStage.id}`);
3383
+ log7(`elicitation raised for stage ${approvedStage.id}`);
3080
3384
  request2(
3081
3385
  "elicitation/create",
3082
3386
  {
@@ -3099,7 +3403,7 @@ function createSyncServer(deps, send) {
3099
3403
  const approved = result?.action === "accept" && result?.content?.decision === "publish";
3100
3404
  if (!approved) {
3101
3405
  const outcome = reply === null ? "timed out" : result?.action ?? "error";
3102
- log6(`elicitation resolved without consent: ${outcome}`);
3406
+ log7(`elicitation resolved without consent: ${outcome}`);
3103
3407
  return ok(
3104
3408
  id,
3105
3409
  textResult(
@@ -3107,7 +3411,7 @@ function createSyncServer(deps, send) {
3107
3411
  )
3108
3412
  );
3109
3413
  }
3110
- log6(`consent received, sending stage ${approvedStage.id}`);
3414
+ log7(`consent received, sending stage ${approvedStage.id}`);
3111
3415
  publish(approvedStage.token, approvedStage.bodyJson).then(
3112
3416
  (res) => {
3113
3417
  if (staged?.id === approvedStage.id) staged = null;
@@ -3150,7 +3454,7 @@ function createSyncServer(deps, send) {
3150
3454
  case "initialize": {
3151
3455
  const capabilities = params?.capabilities ?? {};
3152
3456
  clientSupportsElicitation = "elicitation" in capabilities;
3153
- log6(
3457
+ log7(
3154
3458
  `initialize: elicitation ${clientSupportsElicitation ? "declared" : "ABSENT"}`
3155
3459
  );
3156
3460
  return ok(id, {
@@ -3205,7 +3509,7 @@ function runStdioSyncServer(deps) {
3205
3509
 
3206
3510
  // src/index.ts
3207
3511
  var program = new Command();
3208
- program.name("aistack").description("Measure and share your AI stack from your terminal").version("0.4.0");
3512
+ program.name("aistack").description("Measure and share your AI stack from your terminal").version("0.5.0");
3209
3513
  program.command("login").description("Authenticate with AI Stack").action(loginCommand);
3210
3514
  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 }));
3211
3515
  program.command("create").description("Download and write your stack's AI config files").action(createCommand);
@@ -3218,7 +3522,13 @@ program.command("mcp").description(
3218
3522
  `)
3219
3523
  });
3220
3524
  });
3221
- program.command("sync").description("Scan, preview, and publish measured usage (rolling 30 days)").action(syncCommand);
3525
+ program.command("sync").description("Scan, preview, and publish measured usage (rolling 30 days)").option(
3526
+ "--auto [state]",
3527
+ "silent background sync; 'on' enables the SessionStart hook, 'off' revokes it"
3528
+ ).option(
3529
+ "--every <hours>",
3530
+ "with --auto on: hours between auto-syncs (default 24)"
3531
+ ).action((options) => syncCommand(options));
3222
3532
  program.command("connect").description("Install the in-session sync surface (MCP server + Skill)").argument("<harness>", 'the harness to connect ("claude")').action(connectCommand);
3223
3533
  program.parse();
3224
3534
  //# sourceMappingURL=index.js.map