@phnx-labs/agents-cli 1.22.13 → 1.22.14

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/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.22.14
4
+
5
+ - **`agents secrets view <bundle> --reveal` now resolves a locked keychain bundle
6
+ interactively at a real terminal.** The command hardcoded `agentOnly: true` on
7
+ both reveal call sites (`commands/secrets.ts`), so an explicit human `--reveal`
8
+ on a locked bundle went through the broker-only path and errored with an unlock
9
+ hint instead of raising the one Touch ID sheet the human just asked for. The
10
+ `agentOnly` flag is now `isHeadlessSecretsContext() || !isInteractiveTerminal()`
11
+ — under an agent (`AGENTS_RUNTIME`) or with no TTY it stays broker-only and
12
+ never prompts, but a deliberate `--reveal` typed at an interactive terminal
13
+ resolves the value with a single biometric sheet. This mirrors the existing
14
+ `reveal && !isInteractiveTerminal()` guard a few lines up. `export --plaintext`
15
+ and `exec` are untouched — they stay intentionally silent for release/CI
16
+ scripts. Source: `apps/cli/src/commands/secrets.ts`.
17
+
18
+ - **`agents sessions optimize` — compact the FTS5 session search index.** The scanner delete+inserts a session's docs into the `tool_call_text` / `session_text` full-text indexes on every rescan, and FTS5 never merges the resulting segments on its own — so over thousands of sessions the `%_data` shadow tables bloat with hundreds of thousands of unmerged segments (observed on a real fleet box: 701 MB of index for ~69 MB of content, 196K segments) and `agents sessions` slows to a crawl / hangs. The new command runs FTS5 `'optimize'` (merge all segments, purge tombstones), non-destructively — no searchable content is lost. Reclaimed space frees as reusable pages inside the DB file (VACUUM with the daemon stopped returns it to disk); wireable to a weekly routine so the index never re-bloats. Source: `apps/cli/src/lib/session/db.ts` (`optimizeSessionSearchIndex`), `apps/cli/src/commands/sessions-optimize.ts`.
19
+
3
20
  ## 1.22.13
4
21
 
5
22
  - **`agents sessions` accepts direct live-state flags and remains fleet-wide by default.** `--working`, `--idle`, `--waiting`, `--orphan`/`--orphaned`, `--crashed`, `--closed`, `--abandoned`, `--queued`, and `--unknown` each imply the live scan; multiple flags form a union. `--working` is narrower than `--active`: it excludes idle, waiting, and lifecycle-failure rows. Cross-device collection was already the default and stays that way; `--local` opts out, while `--all` continues to widen historical directory and time scope. Source: `apps/cli/src/commands/sessions.ts`, `apps/cli/src/commands/sessions.test.ts`.
package/dist/bin/agents CHANGED
Binary file
@@ -18,7 +18,7 @@ import { ensureDaemonStarted, isDaemonRunning } from '../lib/daemon.js';
18
18
  import { parseHostsOption, remoteResolveEnv, remoteSecretsRaw, remoteSecretsStream, resolveHostSshTarget, verifyRemoteKeychainPush, keychainWriteFailureMessage, } from '../lib/secrets/remote.js';
19
19
  import { remoteShellFor, buildWindowsStdinImportCommand } from '../lib/hosts/remote-cmd.js';
20
20
  import { resolveRemoteOsSync } from '../lib/hosts/remote-os.js';
21
- import { bundleBackend, bundleExists, bundleItemStore, bundlePolicy, deleteBundle, describeBundle, keychainItemsForBundle, keychainRef, listBundles, healKeychainBundleMetadataAclOnce, migrateLegacyBundles, parseDotenv, readAndResolveBundleEnv, readBundle, readBundleIfDecryptable, reAclBundleItems, renameBundle, rotateBundleSecret, sanitizeProcessEnv, validateBundleName, validateEnvKey, validateExpiresFutureDated, validateSecretType, writeBundle, writeBundleWithItems, SECRET_TYPES, } from '../lib/secrets/bundles.js';
21
+ import { bundleBackend, bundleExists, bundleItemStore, bundlePolicy, deleteBundle, describeBundle, keychainItemsForBundle, keychainRef, listBundles, healKeychainBundleMetadataAclOnce, isHeadlessSecretsContext, migrateLegacyBundles, parseDotenv, readAndResolveBundleEnv, readBundle, readBundleIfDecryptable, reAclBundleItems, renameBundle, rotateBundleSecret, sanitizeProcessEnv, validateBundleName, validateEnvKey, validateExpiresFutureDated, validateSecretType, writeBundle, writeBundleWithItems, SECRET_TYPES, } from '../lib/secrets/bundles.js';
22
22
  import { parseListFilters, bundleMatchesFilter, bundleExpiry, filterIsActive, describeFilter, parseSortField, sortBundles, SORT_FIELDS, REF_KINDS, DEFAULT_EXPIRING_DAYS, } from '../lib/secrets/list-filter.js';
23
23
  import { encryptForFallback, decryptForFallback } from '../lib/secrets/filestore.js';
24
24
  import { getKeychainToken, hasKeychainToken, secretsKeychainItem, setKeychainToken, maybeAutoRekey, } from '../lib/secrets/index.js';
@@ -1254,7 +1254,10 @@ export function registerSecretsCommands(program) {
1254
1254
  const { env } = readAndResolveBundleEnv(bundle.name, {
1255
1255
  caller: 'view --reveal --json',
1256
1256
  keyMode: 'storage',
1257
- agentOnly: true,
1257
+ // An explicit human `--reveal` at a terminal is a deliberate value
1258
+ // access → resolve interactively (one Touch ID). Under an agent
1259
+ // (AGENTS_RUNTIME) or headless (no TTY) it stays broker-only.
1260
+ agentOnly: isHeadlessSecretsContext() || !isInteractiveTerminal(),
1258
1261
  });
1259
1262
  for (const entry of entries) {
1260
1263
  if (entry.kind === 'keychain' && env[entry.key] !== undefined) {
@@ -1369,15 +1372,16 @@ export function registerSecretsCommands(program) {
1369
1372
  console.error(chalk.red('--reveal in a non-TTY requires --plaintext.'));
1370
1373
  process.exit(1);
1371
1374
  }
1372
- // Resolve through the broker / durable-session path. A locked keychain
1373
- // bundle errors with the explicit unlock hint; viewing never raises a
1374
- // Touch ID sheet itself.
1375
+ // An explicit human `view --reveal` at a terminal is a deliberate value
1376
+ // access: resolve interactively (one Touch ID) so a locked bundle shows
1377
+ // its values. Under an agent (AGENTS_RUNTIME) or headless (no TTY) it
1378
+ // stays broker-only and errors with the unlock hint — never a sheet.
1375
1379
  const revealedValues = new Map();
1376
1380
  if (reveal) {
1377
1381
  const { env } = readAndResolveBundleEnv(bundle.name, {
1378
1382
  caller: 'view --reveal',
1379
1383
  keyMode: 'storage',
1380
- agentOnly: true,
1384
+ agentOnly: isHeadlessSecretsContext() || !isInteractiveTerminal(),
1381
1385
  });
1382
1386
  for (const entry of entries) {
1383
1387
  if (entry.kind === 'keychain' && env[entry.key] !== undefined) {
@@ -0,0 +1,14 @@
1
+ /**
2
+ * `agents sessions optimize` — compact the FTS5 session/tool-call search index.
3
+ *
4
+ * The scanner delete+inserts a session's docs into the `tool_call_text` /
5
+ * `session_text` FTS5 indexes on every rescan (and on every extractor-version
6
+ * bump), and FTS5 never merges the resulting segments on its own. Over thousands
7
+ * of sessions and re-index passes the `%_data` shadow tables bloat with hundreds
8
+ * of thousands of unmerged segments — gigabytes of index for tens of MB of
9
+ * actual content — and `agents sessions` queries slow to a crawl. This runs the
10
+ * FTS5 `'optimize'` command to merge every segment into one and purge tombstones.
11
+ * Non-destructive: no searchable content is lost.
12
+ */
13
+ import type { Command } from 'commander';
14
+ export declare function registerSessionsOptimizeCommand(sessionsCmd: Command): void;
@@ -0,0 +1,41 @@
1
+ import chalk from 'chalk';
2
+ import { optimizeSessionSearchIndex } from '../lib/session/db.js';
3
+ import { setHelpSections } from '../lib/help.js';
4
+ export function registerSessionsOptimizeCommand(sessionsCmd) {
5
+ const cmd = sessionsCmd
6
+ .command('optimize')
7
+ .description('Compact the session search index (FTS5), reclaiming bloat from repeated re-indexing')
8
+ .option('--json', 'Emit machine-readable JSON')
9
+ .action((_opts, c) => {
10
+ const opts = c.optsWithGlobals();
11
+ const results = optimizeSessionSearchIndex();
12
+ if (opts.json) {
13
+ console.log(JSON.stringify(results, null, 2));
14
+ return;
15
+ }
16
+ for (const r of results) {
17
+ const merged = Math.max(0, r.segmentsBefore - r.segmentsAfter);
18
+ console.log(` ${chalk.cyan(r.table)}: ${r.segmentsBefore} -> ${r.segmentsAfter} segments ` +
19
+ `(${chalk.green(merged)} merged)`);
20
+ }
21
+ console.log(chalk.gray(' Reclaimed space stays as reusable pages inside the file; VACUUM (daemon stopped) returns it to disk.'));
22
+ });
23
+ setHelpSections(cmd, {
24
+ examples: `
25
+ # Compact the session/tool search index once it has grown fragmented
26
+ agents sessions optimize
27
+
28
+ # Machine-readable segment counts
29
+ agents sessions optimize --json
30
+
31
+ # Wire it to a weekly routine so the index never re-bloats
32
+ agents routines add sessions-optimize --schedule "0 4 * * 0" --agent claude \\
33
+ --prompt "Run: agents sessions optimize"
34
+ `,
35
+ notes: `
36
+ - FTS5 appends a segment on every insert and tombstones every delete; the scanner delete+inserts a session's docs on each rescan and never self-merges, so \`tool_call_text_data\` / \`session_text_data\` bloat with unmerged segments — GBs of index for tens of MB of content, and queries slow down.
37
+ - This runs FTS5 \`'optimize'\`: it merges every segment into one and purges tombstones. Non-destructive — no searchable content is lost.
38
+ - Reclaimed space becomes reusable free pages inside the DB file. To return it to the OS, stop the daemon (\`agents routines stop\`) and run \`VACUUM\` against \`~/.agents/.history/sessions/sessions.db\`.
39
+ `,
40
+ });
41
+ }
@@ -61,6 +61,7 @@ import { registerSessionsImportCommand } from './sessions-import.js';
61
61
  import { registerSessionsMigrateCommand, registerSessionsMigrationsCommand } from './sessions-migrate.js';
62
62
  import { registerSessionsBackfillCommand } from './sessions-backfill.js';
63
63
  import { registerSessionsStatsCommand } from './sessions-stats.js';
64
+ import { registerSessionsOptimizeCommand } from './sessions-optimize.js';
64
65
  import { runBrowserSessions } from '../lib/browser/sessions-list.js';
65
66
  import { countToolProgramOccurrences, parseToolProgramCountClause, readToolIndexCoverage, searchToolCalls, TOOL_QUERY_MAX_CLAUSE_BYTES, TOOL_QUERY_MAX_CLAUSES, TOOL_QUERY_MAX_RESULT_SESSIONS, serializeToolSearchEnvelope, toolSearchRemoteReceiveBudget, } from '../lib/session/tool-index.js';
66
67
  const SESSION_AGENT_FILTER_HELP = `Filter by agent, e.g. claude, codex, claude@2.0.65`;
@@ -3811,6 +3812,7 @@ export function registerSessionsCommands(program) {
3811
3812
  registerSessionsMigrationsCommand(sessionsCmd);
3812
3813
  registerSessionsBackfillCommand(sessionsCmd);
3813
3814
  registerSessionsStatsCommand(sessionsCmd);
3815
+ registerSessionsOptimizeCommand(sessionsCmd);
3814
3816
  // Observe-umbrella alias (Phase 3): roster → sessions --active.
3815
3817
  registerSessionsObserveAliases(program);
3816
3818
  }
@@ -120,6 +120,25 @@ export interface QueryOptions {
120
120
  export declare function getDB(): Database.Database;
121
121
  /** Close the cached database connection. */
122
122
  export declare function closeDB(): void;
123
+ export interface FtsOptimizeResult {
124
+ table: string;
125
+ segmentsBefore: number;
126
+ segmentsAfter: number;
127
+ }
128
+ /**
129
+ * Compact the session + tool-call full-text-search indexes.
130
+ *
131
+ * FTS5 appends a new segment on every insert and leaves a tombstone on every
132
+ * delete. The scanner delete+inserts a session's docs on each rescan (and on
133
+ * every extractor-version bump), and FTS5 never merges those segments on its
134
+ * own — so `tool_call_text_data` / `session_text_data` accumulate hundreds of
135
+ * thousands of unmerged segments, gigabytes of index for tens of MB of content,
136
+ * and `agents sessions` queries slow to a crawl. The FTS5 `'optimize'` command
137
+ * merges every segment into one and purges tombstones. Non-destructive: no
138
+ * searchable content is lost. Reclaimed space becomes reusable free pages inside
139
+ * the DB file; run VACUUM (with the daemon stopped) to return it to the OS.
140
+ */
141
+ export declare function optimizeSessionSearchIndex(): FtsOptimizeResult[];
123
142
  /**
124
143
  * Try to claim the right to run the incremental scan. Returns true if this
125
144
  * process should proceed with scanning, false if another live process is
@@ -820,6 +820,30 @@ export function closeDB() {
820
820
  cachedStmts = {};
821
821
  }
822
822
  }
823
+ /**
824
+ * Compact the session + tool-call full-text-search indexes.
825
+ *
826
+ * FTS5 appends a new segment on every insert and leaves a tombstone on every
827
+ * delete. The scanner delete+inserts a session's docs on each rescan (and on
828
+ * every extractor-version bump), and FTS5 never merges those segments on its
829
+ * own — so `tool_call_text_data` / `session_text_data` accumulate hundreds of
830
+ * thousands of unmerged segments, gigabytes of index for tens of MB of content,
831
+ * and `agents sessions` queries slow to a crawl. The FTS5 `'optimize'` command
832
+ * merges every segment into one and purges tombstones. Non-destructive: no
833
+ * searchable content is lost. Reclaimed space becomes reusable free pages inside
834
+ * the DB file; run VACUUM (with the daemon stopped) to return it to the OS.
835
+ */
836
+ export function optimizeSessionSearchIndex() {
837
+ const db = getDB();
838
+ // Hardcoded literals — never interpolate caller input into an identifier.
839
+ const tables = ['tool_call_text', 'session_text'];
840
+ const segments = (table) => db.prepare(`SELECT count(*) AS n FROM ${table}_data`).get().n;
841
+ return tables.map((table) => {
842
+ const segmentsBefore = segments(table);
843
+ db.prepare(`INSERT INTO ${table}(${table}) VALUES('optimize')`).run();
844
+ return { table, segmentsBefore, segmentsAfter: segments(table) };
845
+ });
846
+ }
823
847
  // ---------------------------------------------------------------------------
824
848
  // Scan coordinator — prevents concurrent full scans across processes
825
849
  // ---------------------------------------------------------------------------
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phnx-labs/agents-cli",
3
- "version": "1.22.13",
3
+ "version": "1.22.14",
4
4
  "description": "One CLI for all your AI coding agents - versions, config, cloud dispatch, sessions, and teams (now with first-class Grok Build CLI support)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",