@astrale-os/cli 1.0.0-beta.27 → 1.0.0-beta.28

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.
Files changed (64) hide show
  1. package/README.md +26 -3
  2. package/dist/astrale.js +243 -76
  3. package/package.json +1 -1
  4. package/src/commands/__tests__/domain-install-operation.test.ts +23 -0
  5. package/src/commands/__tests__/install-identity-override.test.ts +3 -3
  6. package/src/commands/domain/install.ts +19 -11
  7. package/src/commands/get.ts +1 -1
  8. package/src/commands/identity/register.ts +3 -4
  9. package/src/commands/logs.ts +2 -5
  10. package/src/commands/session/analyze.ts +26 -4
  11. package/src/lib/__tests__/skills.test.ts +8 -2
  12. package/src/program/__tests__/program.test.ts +1 -1
  13. package/src/telemetry/__tests__/retention.test.ts +180 -0
  14. package/src/telemetry/__tests__/settings.test.ts +102 -0
  15. package/src/telemetry/__tests__/store-scan.test.ts +128 -0
  16. package/src/telemetry/__tests__/trigger.test.ts +33 -4
  17. package/src/telemetry/recorder.ts +6 -3
  18. package/src/telemetry/retention.ts +129 -0
  19. package/src/telemetry/session.ts +18 -12
  20. package/src/telemetry/settings.ts +64 -11
  21. package/src/telemetry/store.ts +87 -9
  22. package/src/telemetry/trigger.ts +16 -28
  23. package/studio/client/dist/assets/index-BFVFs_8x.js +81 -0
  24. package/studio/client/dist/assets/index-DuB9iUdu.css +2 -0
  25. package/studio/client/dist/assets/schema-studio--a0kX3QU.css +1 -0
  26. package/studio/client/dist/assets/schema-studio-DH6oEWME.js +8 -0
  27. package/studio/client/dist/index.html +3 -3
  28. package/studio/server/agent/prompts/anchors.test.ts +17 -4
  29. package/studio/server/agent/prompts/anchors.ts +3 -2
  30. package/studio/server/agent/prompts/system.test.ts +2 -3
  31. package/studio/server/agent/prompts/system.ts +3 -3
  32. package/studio/server/agent/run/preparation.ts +1 -1
  33. package/studio/server/api/context.ts +1 -1
  34. package/studio/server/api/deployment.ts +1 -1
  35. package/studio/server/api/views.ts +2 -2
  36. package/studio/server/api/workspace.ts +1 -1
  37. package/studio/server/cache.test.ts +3 -8
  38. package/studio/server/cache.ts +4 -45
  39. package/studio/server/handoff/copy.ts +1 -1
  40. package/studio/server/introspect/canonical-schema.test.ts +154 -128
  41. package/studio/server/introspect/canonical-schema.ts +183 -302
  42. package/studio/server/introspect/core.ts +14 -21
  43. package/studio/server/introspect/extractor.ts +11 -15
  44. package/studio/server/introspect/overlay-tsmorph.test.ts +1 -5
  45. package/studio/server/introspect/overlay-tsmorph.ts +0 -1
  46. package/studio/server/introspect/overlay.ts +3 -24
  47. package/studio/server/introspect/revision.test.ts +24 -28
  48. package/studio/server/introspect/revision.ts +10 -21
  49. package/studio/server/introspect/runtime.test.ts +14 -14
  50. package/studio/server/introspect/runtime.ts +1 -46
  51. package/studio/server/lifecycle.ts +4 -1
  52. package/studio/server/state/documents.test.ts +69 -0
  53. package/studio/server/state/documents.ts +57 -10
  54. package/studio/shared/contracts/schema.ts +10 -26
  55. package/studio/shared/contracts/surface.test.ts +2 -2
  56. package/studio/shared/contracts/workspace.ts +3 -0
  57. package/studio/shared/schema/identity.ts +0 -1
  58. package/studio/client/dist/assets/index-C0FAnwNw.css +0 -2
  59. package/studio/client/dist/assets/index-CpBed0V1.js +0 -81
  60. package/studio/client/dist/assets/schema-studio-BilUNb6Z.css +0 -1
  61. package/studio/client/dist/assets/schema-studio-DJm9guI8.js +0 -8
  62. package/studio/server/introspect/core-extractor.ts +0 -30
  63. package/studio/server/introspect/overlay.test.ts +0 -44
  64. package/studio/server/introspect/source-overlay/annotations.ts +0 -14
package/README.md CHANGED
@@ -87,12 +87,12 @@ Install the browser driver once:
87
87
 
88
88
  ```bash
89
89
  npm install -g agent-browser && agent-browser install
90
- npx skills add astrale-os/cli # installs both the astrale-cli (ops) and astrale-domain (authoring) skills
90
+ npx skills add astrale-os/cli # installs every Astrale skill published by this repository
91
91
  npx skills add vercel-labs/agent-browser
92
92
  ```
93
93
 
94
- To install just one: `npx skills add astrale-os/cli@astrale-cli` or
95
- `npx skills add astrale-os/cli@astrale-domain`.
94
+ To install just one, select `astrale-cli`, `astrale-domain`, `astrale-frontend-design`, or
95
+ `astrale-services`, for example: `npx skills add astrale-os/cli@astrale-frontend-design`.
96
96
 
97
97
  ## Updating
98
98
 
@@ -148,6 +148,29 @@ CLI state lives under `~/.astrale/`:
148
148
  - `identities.json`
149
149
  - keypairs
150
150
  - cached IdP sessions
151
+ - `sessions/` — locally recorded work sessions (see below)
152
+
153
+ ### Session retention
154
+
155
+ Each invocation appends one line to a local session record under
156
+ `~/.astrale/sessions/`; `astrale session list` shows them. The store is swept
157
+ automatically against two bounds — sessions idle past `maxAgeDays` are dropped,
158
+ then the store is trimmed to `maxBytes`, oldest first. Both are optional:
159
+
160
+ ```jsonc
161
+ // ~/.astrale/config.json
162
+ {
163
+ "telemetry": {
164
+ "enabled": true, // false disables recording (the sweep still runs)
165
+ "maxAgeDays": 30, // default
166
+ "maxBytes": 52428800 // default: 50 MB
167
+ }
168
+ }
169
+ ```
170
+
171
+ `ASTRALE_TELEMETRY_MAX_AGE_DAYS` and `ASTRALE_TELEMETRY_MAX_BYTES` override the
172
+ config for one invocation. Values that are not positive numbers are ignored in
173
+ favour of the default, so a typo cannot leave the store unbounded.
151
174
 
152
175
  ## Development
153
176
 
package/dist/astrale.js CHANGED
@@ -2578,7 +2578,7 @@ var package_default;
2578
2578
  var init_package = __esm(() => {
2579
2579
  package_default = {
2580
2580
  name: "@astrale-os/cli",
2581
- version: "1.0.0-beta.27",
2581
+ version: "1.0.0-beta.28",
2582
2582
  description: "Astrale CLI — connect to existing Astrale kernels",
2583
2583
  keywords: [
2584
2584
  "astrale",
@@ -41751,7 +41751,7 @@ var require_node_gyp_build_optional_packages = __commonJS(function(exports, modu
41751
41751
 
41752
41752
  // node_modules/.pnpm/msgpackr-extract@3.0.4/node_modules/msgpackr-extract/index.js
41753
41753
  var require_msgpackr_extract = __commonJS(function(exports, module) {
41754
- var __dirname = "/private/tmp/astrale-cli-beta27-a3227da/node_modules/.pnpm/msgpackr-extract@3.0.4/node_modules/msgpackr-extract";
41754
+ var __dirname = "/home/runner/work/cli/cli/node_modules/.pnpm/msgpackr-extract@3.0.4/node_modules/msgpackr-extract";
41755
41755
  module.exports = require_node_gyp_build_optional_packages()(__dirname);
41756
41756
  });
41757
41757
 
@@ -82639,7 +82639,7 @@ var init_get = __esm(() => {
82639
82639
  description: "Get one canonical node by Path or ID",
82640
82640
  afterHelpText: `
82641
82641
  Behavior:
82642
- Resolves one exact Kernel V2 Path and prints the canonical Node
82642
+ Resolves one exact Kernel Path and prints the canonical Node
82643
82643
  { id, class, props }. Missing and authorization-masked nodes remain
82644
82644
  intentionally indistinguishable. @self is expanded before dispatch.
82645
82645
 
@@ -83452,11 +83452,8 @@ Behavior:
83452
83452
  Calls the public Kernel journal syscall and emits its { records, cursor }
83453
83453
  page. Topic selection is exact or prefix-based; cursors and timestamps are
83454
83454
  opaque strings owned by the journal backend. --follow reuses one Client Session
83455
- and advances only with the returned cursor. Machine follow output is NDJSON:
83456
- one complete admitted record per line; YAML follow is unsupported.
83457
-
83458
- Historical event-glob lowering and the application-specific services-domain
83459
- log buffer are not part of the Kernel V2 journal contract.
83455
+ and advances only with the returned cursor. With --json, follow output is
83456
+ NDJSON with one complete admitted record per line; YAML follow is unsupported.
83460
83457
 
83461
83458
  Examples:
83462
83459
  $ astrale logs -i staging --limit 50
@@ -90026,7 +90023,7 @@ async function installDirect(target2, opts, dependencies = {}) {
90026
90023
  }
90027
90024
  host = validateInstallUrl(target2);
90028
90025
  operation = opts.operation === undefined ? direct.createOperationId() : direct.acceptOperationId(opts.operation);
90029
- consentedOrigin = await ensureIdentityOverrideConsent(target2, host, opts.allowIdentityOverride ?? false);
90026
+ consentedOrigin = await ensureIdentityOverrideConsent(target2, host, opts.allowIdentityOverride ?? false, isMachine(opts));
90030
90027
  } catch (e) {
90031
90028
  fatal(e, opts);
90032
90029
  }
@@ -90046,7 +90043,9 @@ async function installDirect(target2, opts, dependencies = {}) {
90046
90043
  log.success(`Domain ${installed.status}: ${installed.origin}@${installed.revision}`);
90047
90044
  log.dim(` operation: ${installed.operation}`);
90048
90045
  if (isIdentityOverride(installed.origin, host) && installed.origin !== consentedOrigin) {
90049
- log.warn(`Installed origin "${installed.origin}" differs from the serving host "${host}" ` + `and was not confirmed before install (the worker Publication was unavailable). ` + `Every ${installed.origin}/* call on this instance now routes to ${host}.`);
90046
+ if (!isMachine(fmtOpts)) {
90047
+ log.warn(`Installed origin "${installed.origin}" differs from the serving host "${host}" ` + `and was not confirmed before install (the worker Publication was unavailable). ` + `Every ${installed.origin}/* call on this instance now routes to ${host}.`);
90048
+ }
90050
90049
  }
90051
90050
  }
90052
90051
  });
@@ -90070,16 +90069,20 @@ function validateInstallUrl(value3) {
90070
90069
  function isIdentityOverride(origin2, host) {
90071
90070
  return origin2.toLowerCase() !== host.toLowerCase();
90072
90071
  }
90073
- async function ensureIdentityOverrideConsent(url3, host, allow) {
90072
+ async function ensureIdentityOverrideConsent(url3, host, allow, machine) {
90074
90073
  const origin2 = await probeDeclaredOrigin(url3);
90075
90074
  if (origin2 === undefined) {
90076
- log.warn(`Could not read a declared origin from ` + `${new URL("/.well-known/astrale/domain.json", url3).href} — ` + `skipping the pre-install identity check (the installed origin is verified after install).`);
90075
+ if (!machine) {
90076
+ log.warn(`Could not read a declared origin from ` + `${new URL("/.well-known/astrale/domain.json", url3).href} — ` + `skipping the pre-install identity check (the installed origin is verified after install).`);
90077
+ }
90077
90078
  return;
90078
90079
  }
90079
90080
  if (!isIdentityOverride(origin2, host))
90080
90081
  return;
90081
90082
  if (allow) {
90082
- log.warn(`Identity override consented via --allow-identity-override: ${origin2} ← ${host}`);
90083
+ if (!machine) {
90084
+ log.warn(`Identity override consented via --allow-identity-override: ${origin2} ← ${host}`);
90085
+ }
90083
90086
  return origin2;
90084
90087
  }
90085
90088
  const banner = source_default.red.bold("⚠ DANGER — IDENTITY OVERRIDE") + `
@@ -90558,10 +90561,9 @@ Behavior:
90558
90561
  its result and stores the same target-bound registration. The callable owns
90559
90562
  authorization; the CLI never receives installed Domain authority.
90560
90563
 
90561
- Kernel V2 Nodes have opaque identity and no caller-assigned storage path, so
90562
- the historical --path option no longer exists. The CLI binds the key proof
90563
- to the exact provision fingerprint and target Kernel audience, then caches
90564
- the returned (issuer, subject) for subsequent calls.
90564
+ The Kernel assigns each Node ID; callers do not choose a storage path. The
90565
+ CLI binds the key proof to the exact provision fingerprint and target Kernel
90566
+ audience, then caches the returned (issuer, subject) for subsequent calls.
90565
90567
 
90566
90568
  Example:
90567
90569
  $ astrale identity register alice --class /:accounts.example:class.User --props '{"accounts.example:class.User.property.name":"Alice"}' -i staging
@@ -91270,6 +91272,53 @@ function metaPath(id) {
91270
91272
  function markerPath(id) {
91271
91273
  return join19(sessionDir(id), ".analyzed");
91272
91274
  }
91275
+ function sessionIds() {
91276
+ try {
91277
+ return readdirSync2(sessionsRoot()).filter((name) => !name.startsWith("."));
91278
+ } catch {
91279
+ return [];
91280
+ }
91281
+ }
91282
+ function scanSessions(now = Date.now()) {
91283
+ return sessionIds().map((id) => {
91284
+ let lastEventAt = null;
91285
+ try {
91286
+ lastEventAt = statSync3(eventsPath(id)).mtime;
91287
+ } catch {}
91288
+ return {
91289
+ id,
91290
+ lastEventAt,
91291
+ closed: lastEventAt !== null && now - lastEventAt.getTime() > IDLE_WINDOW_MS,
91292
+ analyzed: existsSync9(markerPath(id))
91293
+ };
91294
+ }).sort((a, b) => (b.lastEventAt?.getTime() ?? 0) - (a.lastEventAt?.getTime() ?? 0));
91295
+ }
91296
+ function directoryBytes(dir) {
91297
+ let entries;
91298
+ try {
91299
+ entries = readdirSync2(dir, { withFileTypes: true });
91300
+ } catch {
91301
+ return 0;
91302
+ }
91303
+ let total = 0;
91304
+ for (const entry2 of entries) {
91305
+ const path8 = join19(dir, entry2.name);
91306
+ if (entry2.isDirectory()) {
91307
+ total += directoryBytes(path8);
91308
+ } else if (entry2.isFile()) {
91309
+ try {
91310
+ total += statSync3(path8).size;
91311
+ } catch {}
91312
+ }
91313
+ }
91314
+ return total;
91315
+ }
91316
+ function sessionBytes(id) {
91317
+ return directoryBytes(sessionDir(id));
91318
+ }
91319
+ function readMeta(id) {
91320
+ return readJsonSafe(metaPath(id));
91321
+ }
91273
91322
  function readJsonSafe(path8) {
91274
91323
  try {
91275
91324
  return JSON.parse(readFileSync3(path8, "utf-8"));
@@ -91294,13 +91343,7 @@ function inspectSession(id, now = Date.now()) {
91294
91343
  };
91295
91344
  }
91296
91345
  function listSessions2(now = Date.now()) {
91297
- let ids;
91298
- try {
91299
- ids = readdirSync2(sessionsRoot()).filter((n) => !n.startsWith("."));
91300
- } catch {
91301
- return [];
91302
- }
91303
- return ids.map((id) => inspectSession(id, now)).filter((s) => s !== null).sort((a, b) => (b.lastEventAt?.getTime() ?? 0) - (a.lastEventAt?.getTime() ?? 0));
91346
+ return sessionIds().map((id) => inspectSession(id, now)).filter((s) => s !== null).sort((a, b) => (b.lastEventAt?.getTime() ?? 0) - (a.lastEventAt?.getTime() ?? 0));
91304
91347
  }
91305
91348
  var IDLE_WINDOW_MS;
91306
91349
  var init_store = __esm(() => {
@@ -91816,25 +91859,108 @@ var init_analyze = __esm(() => {
91816
91859
 
91817
91860
  // src/telemetry/settings.ts
91818
91861
  import { readFileSync as readFileSync5 } from "node:fs";
91862
+ function readConfig2() {
91863
+ try {
91864
+ const parsed = JSON.parse(readFileSync5(createPaths().config, "utf-8"));
91865
+ const telemetry = parsed.telemetry;
91866
+ return typeof telemetry === "object" && telemetry !== null ? telemetry : {};
91867
+ } catch {
91868
+ return {};
91869
+ }
91870
+ }
91819
91871
  function telemetryEnabled() {
91820
91872
  const env2 = process.env.ASTRALE_TELEMETRY;
91821
91873
  if (env2 !== undefined && OFF_VALUES.has(env2.trim().toLowerCase()))
91822
91874
  return false;
91823
- try {
91824
- const parsed = JSON.parse(readFileSync5(createPaths().config, "utf-8"));
91825
- if (parsed.telemetry?.enabled === false)
91826
- return false;
91827
- } catch {}
91828
- return true;
91875
+ return readConfig2().enabled !== false;
91876
+ }
91877
+ function firstPositive(candidates) {
91878
+ for (const candidate2 of candidates) {
91879
+ if (candidate2 === undefined)
91880
+ continue;
91881
+ const n = typeof candidate2 === "string" ? Number(candidate2.trim()) : candidate2;
91882
+ if (Number.isFinite(n) && n > 0)
91883
+ return n;
91884
+ }
91885
+ return null;
91829
91886
  }
91830
- var OFF_VALUES;
91887
+ function retentionBudget() {
91888
+ const config2 = readConfig2();
91889
+ const days = firstPositive([process.env.ASTRALE_TELEMETRY_MAX_AGE_DAYS, config2.maxAgeDays]) ?? DEFAULT_MAX_AGE_DAYS;
91890
+ const bytes = firstPositive([process.env.ASTRALE_TELEMETRY_MAX_BYTES, config2.maxBytes]) ?? DEFAULT_MAX_BYTES;
91891
+ return { maxAgeMs: days * 24 * 60 * 60 * 1000, maxBytes: bytes };
91892
+ }
91893
+ var OFF_VALUES, DEFAULT_MAX_AGE_DAYS = 30, DEFAULT_MAX_BYTES;
91831
91894
  var init_settings = __esm(() => {
91832
91895
  init_state();
91833
91896
  OFF_VALUES = new Set(["0", "false", "off"]);
91897
+ DEFAULT_MAX_BYTES = 50 * 1024 * 1024;
91898
+ });
91899
+
91900
+ // src/telemetry/retention.ts
91901
+ import { rmSync as rmSync2 } from "node:fs";
91902
+ function remove(id, into) {
91903
+ try {
91904
+ rmSync2(sessionDir(id), { recursive: true, force: true });
91905
+ into.push(id);
91906
+ } catch {}
91907
+ }
91908
+ function sweepByAge(sessions, options = {}) {
91909
+ const { maxAgeMs } = options.budget ?? retentionBudget();
91910
+ const cutoff = (options.now ?? Date.now()) - maxAgeMs;
91911
+ const limit = options.limit ?? Number.POSITIVE_INFINITY;
91912
+ const protect = options.protect;
91913
+ const removed = [];
91914
+ for (const session3 of sessions) {
91915
+ if (removed.length >= limit)
91916
+ break;
91917
+ if (protect?.has(session3.id))
91918
+ continue;
91919
+ if (session3.lastEventAt !== null && session3.lastEventAt.getTime() < cutoff) {
91920
+ remove(session3.id, removed);
91921
+ }
91922
+ }
91923
+ return { removed };
91924
+ }
91925
+ function sweepToBudget(sessions, options = {}) {
91926
+ const { maxBytes } = options.budget ?? retentionBudget();
91927
+ const protect = options.protect;
91928
+ const removed = [];
91929
+ const sized = sessions.map((session3) => ({ session: session3, bytes: sessionBytes(session3.id) }));
91930
+ let total = sized.reduce((sum, entry2) => sum + entry2.bytes, 0);
91931
+ if (total <= maxBytes)
91932
+ return { removed };
91933
+ const evictable = sized.filter((entry2) => entry2.session.closed && !protect?.has(entry2.session.id)).sort((a, b) => (a.session.lastEventAt?.getTime() ?? 0) - (b.session.lastEventAt?.getTime() ?? 0));
91934
+ for (const analyzedFirst of [true, false]) {
91935
+ for (const entry2 of evictable) {
91936
+ if (total <= maxBytes)
91937
+ return { removed };
91938
+ if (entry2.session.analyzed !== analyzedFirst)
91939
+ continue;
91940
+ const before = removed.length;
91941
+ remove(entry2.session.id, removed);
91942
+ if (removed.length > before)
91943
+ total -= entry2.bytes;
91944
+ }
91945
+ }
91946
+ return { removed };
91947
+ }
91948
+ function sweepStore(options = {}) {
91949
+ const budget = options.budget ?? retentionBudget();
91950
+ const sessions = scanSessions();
91951
+ const byAge = sweepByAge(sessions, { ...options, budget });
91952
+ const gone = new Set(byAge.removed);
91953
+ const bySize = sweepToBudget(sessions.filter((session3) => !gone.has(session3.id)), { ...options, budget });
91954
+ return { removed: [...byAge.removed, ...bySize.removed] };
91955
+ }
91956
+ var MAX_REMOVALS_ON_START = 20;
91957
+ var init_retention = __esm(() => {
91958
+ init_settings();
91959
+ init_store();
91834
91960
  });
91835
91961
 
91836
91962
  // src/telemetry/trigger.ts
91837
- import { existsSync as existsSync12, mkdirSync as mkdirSync2, readFileSync as readFileSync6, rmSync as rmSync2, unlinkSync, writeFileSync as writeFileSync3 } from "node:fs";
91963
+ import { existsSync as existsSync12, mkdirSync as mkdirSync2, readFileSync as readFileSync6, unlinkSync, writeFileSync as writeFileSync3 } from "node:fs";
91838
91964
  import { join as join23 } from "node:path";
91839
91965
  function lockPath() {
91840
91966
  return join23(sessionsRoot(), ".analyzer.lock");
@@ -91849,12 +91975,12 @@ function restampLock() {
91849
91975
  writeFileSync3(lockPath(), JSON.stringify({ pid: process.pid, at: Date.now() }));
91850
91976
  } catch {}
91851
91977
  }
91852
- var LOCK_STALE_MS, GC_AGE_MS;
91978
+ var LOCK_STALE_MS;
91853
91979
  var init_trigger = __esm(() => {
91980
+ init_retention();
91854
91981
  init_settings();
91855
91982
  init_store();
91856
91983
  LOCK_STALE_MS = 30 * 60 * 1000;
91857
- GC_AGE_MS = 30 * 24 * 60 * 60 * 1000;
91858
91984
  });
91859
91985
 
91860
91986
  // src/commands/session/analyze.ts
@@ -91867,12 +91993,19 @@ var init_analyze2 = __esm(() => {
91867
91993
  init_source();
91868
91994
  init_log();
91869
91995
  init_analyze();
91996
+ init_retention();
91870
91997
  init_store();
91871
91998
  init_trigger();
91872
91999
  analyze_default = {
91873
92000
  name: "analyze",
91874
92001
  description: "Analyze a recorded session for DX frictions (dry-run: writes report.md)",
91875
- arguments: [{ name: "id", description: "Session id (default: most recent closed, unanalyzed)" }],
92002
+ arguments: [
92003
+ {
92004
+ name: "id",
92005
+ description: "Session id (default: most recent closed, unanalyzed)",
92006
+ required: false
92007
+ }
92008
+ ],
91876
92009
  options: [
91877
92010
  { flags: "--file", description: "File cleared findings as issues on the admin tracker" },
91878
92011
  { flags: "--model <model>", description: "Model for the analyzer pass" },
@@ -91882,10 +92015,10 @@ var init_analyze2 = __esm(() => {
91882
92015
  action: async (id, opts) => {
91883
92016
  if (opts.auto)
91884
92017
  restampLock();
92018
+ let target2 = id;
91885
92019
  try {
91886
- let target2 = id;
91887
92020
  if (!target2) {
91888
- const candidate2 = listSessions2().find((s) => s.closed && (opts.force || !s.analyzed));
92021
+ const candidate2 = scanSessions().find((s) => s.closed && (opts.force || !s.analyzed));
91889
92022
  if (!candidate2) {
91890
92023
  if (!opts.auto)
91891
92024
  log.dim("Nothing to analyze — no closed, unanalyzed session.");
@@ -91910,6 +92043,12 @@ var init_analyze2 = __esm(() => {
91910
92043
  } finally {
91911
92044
  if (opts.auto)
91912
92045
  releaseLock();
92046
+ try {
92047
+ const swept = sweepStore({ protect: new Set(target2 === undefined ? [] : [target2]) });
92048
+ if (!opts.auto && swept.removed.length > 0) {
92049
+ log.dim(`retention: removed ${swept.removed.length} session(s)`);
92050
+ }
92051
+ } catch {}
91913
92052
  }
91914
92053
  }
91915
92054
  };
@@ -92694,11 +92833,13 @@ function stamp(d = new Date) {
92694
92833
  const p = (n, w = 2) => String(n).padStart(w, "0");
92695
92834
  return `${p(d.getFullYear(), 4)}${p(d.getMonth() + 1)}${p(d.getDate())}${p(d.getHours())}${p(d.getMinutes())}`;
92696
92835
  }
92697
- function findOpenAmbient(root) {
92698
- for (const s of listSessions2()) {
92699
- if (s.meta?.root === root && s.meta.explicit === false && !s.closed && s.analyzed === null) {
92836
+ function findOpenAmbient(root, sessions) {
92837
+ for (const s of sessions) {
92838
+ if (s.closed || s.analyzed)
92839
+ continue;
92840
+ const meta3 = readMeta(s.id);
92841
+ if (meta3?.root === root && meta3.explicit === false)
92700
92842
  return s.id;
92701
- }
92702
92843
  }
92703
92844
  return null;
92704
92845
  }
@@ -92706,7 +92847,7 @@ function mintAmbientId(root) {
92706
92847
  const hash8 = createHash4("sha256").update(root).digest("hex").slice(0, 8);
92707
92848
  return `amb-${hash8}-${stamp()}`;
92708
92849
  }
92709
- function resolveSession2(cwd) {
92850
+ function resolveSession2(cwd, sessions) {
92710
92851
  const root = findGitRoot(cwd) ?? cwd;
92711
92852
  const pinned = process.env.ASTRALE_SESSION;
92712
92853
  if (pinned) {
@@ -92714,10 +92855,11 @@ function resolveSession2(cwd) {
92714
92855
  if (id.length > 0)
92715
92856
  return { id, root, explicit: true };
92716
92857
  }
92717
- return { id: findOpenAmbient(root) ?? mintAmbientId(root), root, explicit: false };
92858
+ const scan2 = sessions ?? scanSessions();
92859
+ return { id: findOpenAmbient(root, scan2) ?? mintAmbientId(root), root, explicit: false };
92718
92860
  }
92719
- function ensureSession(cwd) {
92720
- const resolved = resolveSession2(cwd);
92861
+ function ensureSession(cwd, sessions) {
92862
+ const resolved = resolveSession2(cwd, sessions);
92721
92863
  const dir = sessionDir(resolved.id);
92722
92864
  if (!existsSync13(dir)) {
92723
92865
  try {
@@ -92751,7 +92893,7 @@ var HELP_VERSION = new Set([
92751
92893
  function isHelpOrVersion(args) {
92752
92894
  return args.every((a) => HELP_VERSION.has(a));
92753
92895
  }
92754
- function beginInvocation(argv) {
92896
+ function beginInvocation(argv, sessions) {
92755
92897
  try {
92756
92898
  const args = argv.slice(2);
92757
92899
  if (!telemetryEnabled() || isHelpOrVersion(args))
@@ -92759,7 +92901,7 @@ function beginInvocation(argv) {
92759
92901
  const startMs = Date.now();
92760
92902
  const ts = new Date(startMs).toISOString();
92761
92903
  const cwd = process.cwd();
92762
- const { id, root } = ensureSession(cwd);
92904
+ const { id, root } = ensureSession(cwd, sessions);
92763
92905
  return (exitCode, errorName) => {
92764
92906
  try {
92765
92907
  const event = {
@@ -92783,37 +92925,61 @@ function beginInvocation(argv) {
92783
92925
  }
92784
92926
  }
92785
92927
 
92928
+ // src/telemetry/store.ts
92929
+ init_state();
92930
+ import { existsSync as existsSync14, readdirSync as readdirSync5, readFileSync as readFileSync7, statSync as statSync6 } from "node:fs";
92931
+ import { join as join25 } from "node:path";
92932
+ var IDLE_WINDOW_MS2 = 30 * 60 * 1000;
92933
+ function sessionsRoot2() {
92934
+ return join25(createPaths().home, "sessions");
92935
+ }
92936
+ function sessionDir2(id) {
92937
+ return join25(sessionsRoot2(), id);
92938
+ }
92939
+ function eventsPath2(id) {
92940
+ return join25(sessionDir2(id), "events.jsonl");
92941
+ }
92942
+ function markerPath2(id) {
92943
+ return join25(sessionDir2(id), ".analyzed");
92944
+ }
92945
+ function sessionIds2() {
92946
+ try {
92947
+ return readdirSync5(sessionsRoot2()).filter((name) => !name.startsWith("."));
92948
+ } catch {
92949
+ return [];
92950
+ }
92951
+ }
92952
+ function scanSessions2(now = Date.now()) {
92953
+ return sessionIds2().map((id) => {
92954
+ let lastEventAt = null;
92955
+ try {
92956
+ lastEventAt = statSync6(eventsPath2(id)).mtime;
92957
+ } catch {}
92958
+ return {
92959
+ id,
92960
+ lastEventAt,
92961
+ closed: lastEventAt !== null && now - lastEventAt.getTime() > IDLE_WINDOW_MS2,
92962
+ analyzed: existsSync14(markerPath2(id))
92963
+ };
92964
+ }).sort((a, b) => (b.lastEventAt?.getTime() ?? 0) - (a.lastEventAt?.getTime() ?? 0));
92965
+ }
92966
+
92786
92967
  // src/telemetry/trigger.ts
92968
+ init_retention();
92787
92969
  init_settings();
92788
92970
  init_store();
92789
92971
  import { spawn as spawn4 } from "node:child_process";
92790
- import { existsSync as existsSync14, mkdirSync as mkdirSync4, readFileSync as readFileSync7, rmSync as rmSync3, unlinkSync as unlinkSync2, writeFileSync as writeFileSync5 } from "node:fs";
92791
- import { join as join25 } from "node:path";
92972
+ import { existsSync as existsSync15, mkdirSync as mkdirSync4, readFileSync as readFileSync8, unlinkSync as unlinkSync2, writeFileSync as writeFileSync5 } from "node:fs";
92973
+ import { join as join26 } from "node:path";
92792
92974
  var LOCK_STALE_MS2 = 30 * 60 * 1000;
92793
- var GC_AGE_MS2 = 30 * 24 * 60 * 60 * 1000;
92794
- var GC_MAX_PER_RUN = 20;
92795
- function gcOldSessions(sessions) {
92796
- try {
92797
- const cutoff = Date.now() - GC_AGE_MS2;
92798
- let removed = 0;
92799
- for (const s of sessions) {
92800
- if (removed >= GC_MAX_PER_RUN)
92801
- break;
92802
- if (s.analyzed !== null && s.lastEventAt !== null && s.lastEventAt.getTime() < cutoff) {
92803
- rmSync3(sessionDir(s.id), { recursive: true, force: true });
92804
- removed++;
92805
- }
92806
- }
92807
- } catch {}
92808
- }
92809
92975
  function lockPath2() {
92810
- return join25(sessionsRoot(), ".analyzer.lock");
92976
+ return join26(sessionsRoot(), ".analyzer.lock");
92811
92977
  }
92812
92978
  function claimLock() {
92813
92979
  const path8 = lockPath2();
92814
92980
  try {
92815
- if (existsSync14(path8)) {
92816
- const lock = JSON.parse(readFileSync7(path8, "utf-8"));
92981
+ if (existsSync15(path8)) {
92982
+ const lock = JSON.parse(readFileSync8(path8, "utf-8"));
92817
92983
  const stale2 = Date.now() - lock.at > LOCK_STALE_MS2;
92818
92984
  let alive = false;
92819
92985
  try {
@@ -92831,17 +92997,17 @@ function claimLock() {
92831
92997
  return false;
92832
92998
  }
92833
92999
  }
92834
- function maybeTriggerAnalysis(argv) {
93000
+ function maybeTriggerAnalysis(argv, scan2) {
92835
93001
  try {
92836
- if (!telemetryEnabled())
92837
- return;
92838
93002
  if (process.env.ASTRALE_TELEMETRY_NO_TRIGGER === "1")
92839
93003
  return;
93004
+ const sessions = scan2 ?? scanSessions();
93005
+ const swept = new Set(sweepByAge(sessions, { limit: MAX_REMOVALS_ON_START }).removed);
93006
+ if (!telemetryEnabled())
93007
+ return;
92840
93008
  if (argv[2] === "session")
92841
93009
  return;
92842
- const sessions = listSessions2();
92843
- gcOldSessions(sessions);
92844
- const target2 = sessions.find((s) => s.closed && s.analyzed === null && s.lastEventAt !== null);
93010
+ const target2 = sessions.find((s) => s.closed && !s.analyzed && s.lastEventAt !== null && !swept.has(s.id));
92845
93011
  if (!target2)
92846
93012
  return;
92847
93013
  if (!claimLock())
@@ -92864,11 +93030,12 @@ function overrideExits(command) {
92864
93030
  for (const child3 of command.commands)
92865
93031
  overrideExits(child3);
92866
93032
  }
92867
- var finalize2 = process.argv[2] === "session" ? undefined : beginInvocation(process.argv);
93033
+ var sessions = scanSessions2();
93034
+ var finalize2 = process.argv[2] === "session" ? undefined : beginInvocation(process.argv, sessions);
92868
93035
  var errorName;
92869
93036
  if (finalize2)
92870
93037
  process.on("exit", (code) => finalize2(code ?? 0, errorName));
92871
- maybeTriggerAnalysis(process.argv);
93038
+ maybeTriggerAnalysis(process.argv, sessions);
92872
93039
  var argv = normalizeRootVersionArgv(process.argv);
92873
93040
  configureInvocation(argv);
92874
93041
  var program4 = await buildProgram();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrale-os/cli",
3
- "version": "1.0.0-beta.27",
3
+ "version": "1.0.0-beta.28",
4
4
  "description": "Astrale CLI — connect to existing Astrale kernels",
5
5
  "keywords": [
6
6
  "astrale",
@@ -93,6 +93,29 @@ describe('direct domain install operation identity', () => {
93
93
  ])
94
94
  })
95
95
 
96
+ test('keeps identity-override consent warnings out of machine output', async () => {
97
+ globalThis.fetch = mock(async () => publicationResponse()) as unknown as typeof fetch
98
+ const originalError = console.error
99
+ const warnings: unknown[][] = []
100
+ console.error = (...args: unknown[]) => {
101
+ warnings.push(args)
102
+ }
103
+ try {
104
+ await installDirect(
105
+ 'https://alias.test',
106
+ { direct: true, json: true, allowIdentityOverride: true },
107
+ {
108
+ createOperationId: () => GENERATED,
109
+ runKernelCommand: async () => undefined,
110
+ },
111
+ )
112
+ } finally {
113
+ console.error = originalError
114
+ }
115
+
116
+ expect(warnings).toEqual([])
117
+ })
118
+
96
119
  test('rejects a non-UUID operation before metadata or Kernel transport', async () => {
97
120
  const originalExit = process.exit
98
121
  const originalWrite = process.stderr.write
@@ -54,11 +54,11 @@ describe('declared-origin Publication probe', () => {
54
54
  expect(await probeDeclaredOrigin(url)).toBe('crm.acme.dev')
55
55
  })
56
56
 
57
- test('does not accept the removed pre-Kernel-V2 domainName shape', async () => {
58
- const legacy = servePublication(() =>
57
+ test('does not accept a Publication without the canonical origin field', async () => {
58
+ const invalid = servePublication(() =>
59
59
  Response.json({ iss: 'https://x', domainName: 'crm.acme.dev' }),
60
60
  )
61
- expect(await probeDeclaredOrigin(legacy)).toBeUndefined()
61
+ expect(await probeDeclaredOrigin(invalid)).toBeUndefined()
62
62
  })
63
63
 
64
64
  test('degrades to undefined on invalid Publication, non-200, bad JSON, or dead host', async () => {