ai-hist 0.15.3 → 0.15.6
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 +4 -2
- package/dist/cli.js +241 -98
- package/dist/cli.js.map +1 -1
- package/dist/cloud-auth-bundle.d.ts +2 -0
- package/dist/cloud-auth-bundle.d.ts.map +1 -0
- package/dist/cloud-auth-bundle.js +1112 -0
- package/dist/cloud-auth-bundle.js.map +7 -0
- package/dist/cloud-preflight.d.ts +29 -0
- package/dist/cloud-preflight.d.ts.map +1 -0
- package/dist/cloud-preflight.js +72 -0
- package/dist/cloud-preflight.js.map +1 -0
- package/dist/index.d.ts +37 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +74 -3
- package/dist/index.js.map +1 -1
- package/package.json +10 -3
package/README.md
CHANGED
|
@@ -190,14 +190,16 @@ See [the migration guide](https://github.com/AgentWorkforce/relayhistory/blob/ma
|
|
|
190
190
|
|
|
191
191
|
## Cloud opt-in
|
|
192
192
|
|
|
193
|
-
Run `ai-hist enable-cloud` to log in, drain local history and keep pushing. Use `--once` to exit after draining. The async SDK exports `enableCloud`, `pushCloud`, `installGitHooks` and `createShareableTrace`;
|
|
193
|
+
Run `ai-hist enable-cloud` to log in, drain local history and keep pushing. Use `--once` to exit after draining. The npm CLI bundles Agent Relay Cloud login, so no separate `agent-relay` CLI install is required; a run without a TTY fails promptly with interactive-login and token guidance. The async SDK exports `enableCloud`, `pushCloud`, `installGitHooks` and `createShareableTrace`; RelayHistory exchange, transport, and stage-scoped auth stay in Rust. See [cloud setup](https://github.com/AgentWorkforce/relayhistory/blob/main/docs/enable-cloud.md).
|
|
194
194
|
|
|
195
195
|
## Cloud token and replay
|
|
196
196
|
|
|
197
197
|
The npm CLI uses the same Rust engine as the public async SDK:
|
|
198
198
|
|
|
199
199
|
```bash
|
|
200
|
-
export
|
|
200
|
+
# Shell-safe token export (fails if ai-hist token fails)
|
|
201
|
+
RTH_TOKEN="$(ai-hist token)" || { echo "Failed to get token" >&2; exit 1; }
|
|
202
|
+
export RTH_TOKEN
|
|
201
203
|
ai-hist replay SESSION_ID
|
|
202
204
|
ai-hist replay SESSION_ID --json --out transcript.json
|
|
203
205
|
```
|
package/dist/cli.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { readFile } from 'node:fs/promises';
|
|
3
|
-
import {
|
|
4
|
-
|
|
3
|
+
import { discoverSessions, ensureLocalStore, formatSessionRow, getSession, getSessionEventsPage, getSessionFileEditsPage, getSessionRelationships, getSessionToolCallsPage, getSessionTree, hydrateSession, listSessionCatalogPage, loadStoredRelayhistoryAuth, login, recent, resumeCommand, search, stats, sync, enableCloud, accessToken, replay, validateCloudExchangeBaseUrl, } from './index.js';
|
|
4
|
+
import { prepareCloudSessionForEnableCloud } from './cloud-preflight.js';
|
|
5
|
+
const BOOLEAN_FLAGS = new Set(['all', 'fts', 'help', 'json', 'local', 'no-bootstrap', 'no-related', 'no-warning', 'once', 'pretty', 'remote', 'version']);
|
|
5
6
|
const VALUE_FLAGS = new Set([
|
|
6
|
-
'base-url', 'interval', 'max-content', 'out', 'after', 'after-ms', 'after-session-id', 'after-source', 'before-ms', 'db', 'limit',
|
|
7
|
-
'max-depth', 'max-nodes', 'project', 'source', 'tag', 'tokens',
|
|
7
|
+
'base-url', 'interval', 'label', 'max-content', 'out', 'after', 'after-ms', 'after-session-id', 'after-source', 'before-ms', 'db', 'limit',
|
|
8
|
+
'max-depth', 'max-nodes', 'project', 'source', 'tag', 'token', 'tokens',
|
|
8
9
|
]);
|
|
9
10
|
const KNOWN_FLAGS = new Set([...BOOLEAN_FLAGS, ...VALUE_FLAGS]);
|
|
10
11
|
function versionTriple(value) {
|
|
@@ -100,6 +101,18 @@ function textFlag(args, name) {
|
|
|
100
101
|
const value = args.flags.get(name)?.at(-1);
|
|
101
102
|
return typeof value === 'string' ? value : undefined;
|
|
102
103
|
}
|
|
104
|
+
async function relayAccessTokenForCloudCommand(command, rawArgs, args, reuseStoredAuth) {
|
|
105
|
+
const explicitToken = textFlag(args, 'token');
|
|
106
|
+
if (explicitToken !== undefined)
|
|
107
|
+
return explicitToken;
|
|
108
|
+
const baseUrl = textFlag(args, 'base-url');
|
|
109
|
+
if (reuseStoredAuth && await loadStoredRelayhistoryAuth(baseUrl))
|
|
110
|
+
return undefined;
|
|
111
|
+
const preparedToken = await prepareCloudSessionForEnableCloud(command, rawArgs, process.env);
|
|
112
|
+
if (preparedToken !== null)
|
|
113
|
+
await validateCloudExchangeBaseUrl(baseUrl);
|
|
114
|
+
return preparedToken ?? undefined;
|
|
115
|
+
}
|
|
103
116
|
function textFlags(args, name) {
|
|
104
117
|
return (args.flags.get(name) ?? []).filter((value) => typeof value === 'string');
|
|
105
118
|
}
|
|
@@ -188,12 +201,40 @@ function output(value, json) {
|
|
|
188
201
|
process.stdout.write(`${String(value)}\n`);
|
|
189
202
|
}
|
|
190
203
|
}
|
|
204
|
+
function showHelp() {
|
|
205
|
+
process.stdout.write(`Usage:
|
|
206
|
+
ai-hist [--no-bootstrap] [--db PATH] [--json] [--help]
|
|
207
|
+
ai-hist enable-cloud [--base-url URL] [--token TOKEN] [--db PATH] [--interval SECONDS] [--once] [--json]
|
|
208
|
+
ai-hist sessions list [--pretty] [--local | --remote | --all] [--source SOURCE]... [--limit N] [--before-ms MS] [--after JSON | --after-source SOURCE --after-session-id ID [--after-ms MS]] [--json]
|
|
209
|
+
ai-hist sessions discover [--local | --remote | --all] [--source SOURCE] [--limit N] [--json]
|
|
210
|
+
ai-hist sessions hydrate SOURCE SESSION_ID [--local | --remote | --all] [--no-related] [--db PATH] [--json]
|
|
211
|
+
ai-hist sessions relationships SOURCE SESSION_ID [--db PATH] [--json]
|
|
212
|
+
ai-hist sessions tree SOURCE SESSION_ID [--max-depth N] [--max-nodes N] [--db PATH] [--json]
|
|
213
|
+
ai-hist sessions tools SOURCE SESSION_ID [--limit N] [--after JSON] [--db PATH] [--json]
|
|
214
|
+
ai-hist sessions edits SOURCE SESSION_ID [--limit N] [--after JSON] [--db PATH] [--json]
|
|
215
|
+
ai-hist search QUERY... [--local | --remote | --all] [--source SOURCE] [--project PATH] [--limit N] [--json]
|
|
216
|
+
ai-hist recent [N] [--local | --remote | --all] [--source SOURCE] [--project PATH] [--json]
|
|
217
|
+
ai-hist session SESSION_ID [--source SOURCE] [--json]
|
|
218
|
+
ai-hist events SESSION_ID [--source SOURCE] [--limit N] [--after JSON] [--json]
|
|
219
|
+
ai-hist resume QUERY... [--local | --remote | --all] [--db PATH] [--fts] [--json]
|
|
220
|
+
ai-hist pack QUERY... [--local | --remote | --all] [--source SOURCE] [--project PATH] [--tag TAG] [--limit N] [--tokens N] [--db PATH] [--fts] [--json]
|
|
221
|
+
ai-hist login [--base-url URL] [--token TOKEN] [--label LABEL] [--json]
|
|
222
|
+
ai-hist token [--base-url URL]
|
|
223
|
+
ai-hist replay SESSION_ID [--base-url URL] [--limit N] [--max-content N] [--json] [--out PATH] [--help]
|
|
224
|
+
ai-hist stats [--local | --remote | --all] [--json]
|
|
225
|
+
ai-hist sync [--local | --remote | --all] [--db PATH] [--json]
|
|
226
|
+
|
|
227
|
+
Every command that reads local history indexes it on first use; pass
|
|
228
|
+
--no-bootstrap to answer from the store exactly as it stands.
|
|
229
|
+
`);
|
|
230
|
+
process.exit(0);
|
|
231
|
+
}
|
|
191
232
|
function usage(message) {
|
|
192
233
|
if (message)
|
|
193
234
|
process.stderr.write(`ai-hist: ${message}\n\n`);
|
|
194
235
|
process.stderr.write(`Usage:
|
|
195
|
-
ai-hist [--no-bootstrap] [--db PATH] [--json]
|
|
196
|
-
ai-hist enable-cloud [--base-url URL] [--db PATH] [--interval SECONDS] [--once] [--json]
|
|
236
|
+
ai-hist [--no-bootstrap] [--db PATH] [--json] [--help]
|
|
237
|
+
ai-hist enable-cloud [--base-url URL] [--token TOKEN] [--db PATH] [--interval SECONDS] [--once] [--json]
|
|
197
238
|
ai-hist sessions list [--pretty] [--local | --remote | --all] [--source SOURCE]... [--limit N] [--before-ms MS] [--after JSON | --after-source SOURCE --after-session-id ID [--after-ms MS]] [--json]
|
|
198
239
|
ai-hist sessions discover [--local | --remote | --all] [--source SOURCE] [--limit N] [--json]
|
|
199
240
|
ai-hist sessions hydrate SOURCE SESSION_ID [--local | --remote | --all] [--no-related] [--db PATH] [--json]
|
|
@@ -207,10 +248,14 @@ function usage(message) {
|
|
|
207
248
|
ai-hist events SESSION_ID [--source SOURCE] [--limit N] [--after JSON] [--json]
|
|
208
249
|
ai-hist resume QUERY... [--local | --remote | --all] [--db PATH] [--fts] [--json]
|
|
209
250
|
ai-hist pack QUERY... [--local | --remote | --all] [--source SOURCE] [--project PATH] [--tag TAG] [--limit N] [--tokens N] [--db PATH] [--fts] [--json]
|
|
251
|
+
ai-hist login [--base-url URL] [--token TOKEN] [--label LABEL] [--json]
|
|
210
252
|
ai-hist token [--base-url URL]
|
|
211
|
-
ai-hist replay SESSION_ID [--base-url URL] [--limit N] [--max-content N] [--json] [--out PATH]
|
|
253
|
+
ai-hist replay SESSION_ID [--base-url URL] [--limit N] [--max-content N] [--json] [--out PATH] [--help]
|
|
212
254
|
ai-hist stats [--local | --remote | --all] [--json]
|
|
213
255
|
ai-hist sync [--local | --remote | --all] [--db PATH] [--json]
|
|
256
|
+
|
|
257
|
+
Every command that reads local history indexes it on first use; pass
|
|
258
|
+
--no-bootstrap to answer from the store exactly as it stands.
|
|
214
259
|
`);
|
|
215
260
|
process.exit(2);
|
|
216
261
|
}
|
|
@@ -364,7 +409,6 @@ function queryPositionals(subcommand, rest, command) {
|
|
|
364
409
|
return query;
|
|
365
410
|
}
|
|
366
411
|
async function runResume(args, subcommand, rest, json) {
|
|
367
|
-
validateFlags(args, 'resume', ['all', 'db', 'fts', 'json', 'local', 'remote']);
|
|
368
412
|
const query = queryPositionals(subcommand, rest, 'resume');
|
|
369
413
|
// Matches the Rust CLI: search is capped to the single best match, then that
|
|
370
414
|
// one row is checked for a usable session id rather than scanning further.
|
|
@@ -418,9 +462,6 @@ function takeCodePoints(text, limit) {
|
|
|
418
462
|
return { truncated: true, text: points.slice(0, limit).join('') };
|
|
419
463
|
}
|
|
420
464
|
async function runPack(args, subcommand, rest, json) {
|
|
421
|
-
validateFlags(args, 'pack', [
|
|
422
|
-
'all', 'db', 'fts', 'json', 'limit', 'local', 'project', 'remote', 'source', 'tag', 'tokens',
|
|
423
|
-
]);
|
|
424
465
|
const query = queryPositionals(subcommand, rest, 'pack');
|
|
425
466
|
const queryStr = query.join(' ');
|
|
426
467
|
const tokens = nonNegativeIntFlag(args, 'tokens') ?? 0;
|
|
@@ -495,6 +536,120 @@ function outputTree(value, json) {
|
|
|
495
536
|
if (value.truncated)
|
|
496
537
|
process.stdout.write('truncated: node/depth budget reached\n');
|
|
497
538
|
}
|
|
539
|
+
function validateInterval(args) {
|
|
540
|
+
// --interval is seconds; validate before converting so the error names the
|
|
541
|
+
// unit the caller actually typed rather than a millisecond bound.
|
|
542
|
+
const seconds = numberFlag(args, 'interval') ?? 60;
|
|
543
|
+
if (!Number.isSafeInteger(seconds) || seconds < 1 || seconds > 2_147_483) {
|
|
544
|
+
usage('--interval must be a whole number of seconds between 1 and 2147483');
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
// The whole dispatch surface in one table. `readsLocalStore` is the decision
|
|
548
|
+
// that used to live only in the bare-invocation branch: keeping it here is what
|
|
549
|
+
// stops `search` and `ai-hist` disagreeing about whether a store exists.
|
|
550
|
+
const COMMANDS = new Map([
|
|
551
|
+
['', { name: 'ai-hist', positionals: [0, 0], allowed: ['db', 'json', 'help'], readsLocalStore: true }],
|
|
552
|
+
['login', { name: 'login', positionals: [0, 0],
|
|
553
|
+
allowed: ['base-url', 'json', 'label', 'token'],
|
|
554
|
+
validate: (args) => {
|
|
555
|
+
if (textFlag(args, 'token') && !textFlag(args, 'base-url'))
|
|
556
|
+
usage('login requires --base-url with --token');
|
|
557
|
+
} }],
|
|
558
|
+
['token', { name: 'token', positionals: [0, 0], allowed: ['base-url'] }],
|
|
559
|
+
['replay', { name: 'replay', positionals: [1, 1], requires: 'replay requires SESSION_ID',
|
|
560
|
+
allowed: ['base-url', 'limit', 'max-content', 'json', 'out', 'help'] }],
|
|
561
|
+
['enable-cloud', { name: 'enable-cloud', positionals: [0, 0], validate: validateInterval,
|
|
562
|
+
allowed: ['base-url', 'db', 'interval', 'once', 'json', 'token'] }],
|
|
563
|
+
['sessions list', { name: 'sessions list', positionals: [0, 0], readsLocalStore: true,
|
|
564
|
+
validate: (args) => {
|
|
565
|
+
if (args.flags.has('json') && args.flags.has('pretty'))
|
|
566
|
+
usage('--pretty and --json are mutually exclusive');
|
|
567
|
+
},
|
|
568
|
+
allowed: [
|
|
569
|
+
'after', 'after-ms', 'after-session-id', 'after-source', 'all', 'before-ms', 'db',
|
|
570
|
+
'json', 'limit', 'local', 'pretty', 'remote', 'source',
|
|
571
|
+
] }],
|
|
572
|
+
['sessions discover', { name: 'sessions discover', positionals: [0, 0],
|
|
573
|
+
allowed: ['all', 'db', 'json', 'limit', 'local', 'remote', 'source'] }],
|
|
574
|
+
['sessions hydrate', { name: 'sessions hydrate', positionals: [2, 2], readsLocalStore: true,
|
|
575
|
+
requires: 'sessions hydrate requires SOURCE and SESSION_ID',
|
|
576
|
+
allowed: ['all', 'db', 'json', 'local', 'no-related', 'remote'] }],
|
|
577
|
+
['sessions relationships', { name: 'sessions relationships', positionals: [2, 2], readsLocalStore: true,
|
|
578
|
+
rejectsScope: true, requires: 'sessions relationships requires SOURCE and SESSION_ID',
|
|
579
|
+
allowed: ['all', 'db', 'json', 'local', 'remote'] }],
|
|
580
|
+
['sessions tree', { name: 'sessions tree', positionals: [2, 2], readsLocalStore: true, rejectsScope: true,
|
|
581
|
+
requires: 'sessions tree requires SOURCE and SESSION_ID',
|
|
582
|
+
allowed: ['all', 'db', 'json', 'local', 'max-depth', 'max-nodes', 'remote'] }],
|
|
583
|
+
['sessions tools', { name: 'sessions tools', positionals: [2, 2], readsLocalStore: true,
|
|
584
|
+
requires: 'sessions tools requires SOURCE and SESSION_ID', allowed: ['after', 'db', 'json', 'limit'] }],
|
|
585
|
+
['sessions edits', { name: 'sessions edits', positionals: [2, 2], readsLocalStore: true,
|
|
586
|
+
requires: 'sessions edits requires SOURCE and SESSION_ID', allowed: ['after', 'db', 'json', 'limit'] }],
|
|
587
|
+
['search', { name: 'search', positionals: [1, null], requires: 'search requires a query', readsLocalStore: true,
|
|
588
|
+
allowed: ['all', 'before-ms', 'db', 'fts', 'json', 'limit', 'local', 'project', 'remote', 'source', 'tag'] }],
|
|
589
|
+
['recent', { name: 'recent', positionals: [0, 1], readsLocalStore: true,
|
|
590
|
+
validate: (args) => {
|
|
591
|
+
const count = args.positional[1];
|
|
592
|
+
if (count !== undefined && !Number.isFinite(Number(count))) {
|
|
593
|
+
usage(`recent count must be a number (got '${count}')`);
|
|
594
|
+
}
|
|
595
|
+
},
|
|
596
|
+
allowed: ['all', 'before-ms', 'db', 'json', 'limit', 'local', 'project', 'remote', 'source', 'tag'] }],
|
|
597
|
+
['session', { name: 'session', positionals: [1, 1], requires: 'session requires SESSION_ID',
|
|
598
|
+
readsLocalStore: true, rejectsScope: true,
|
|
599
|
+
allowed: ['all', 'db', 'json', 'local', 'remote', 'source', 'tag'] }],
|
|
600
|
+
['events', { name: 'events', positionals: [1, 1], requires: 'events requires SESSION_ID',
|
|
601
|
+
readsLocalStore: true, rejectsScope: true,
|
|
602
|
+
allowed: ['after', 'all', 'db', 'json', 'limit', 'local', 'remote', 'source'] }],
|
|
603
|
+
['resume', { name: 'resume', positionals: [1, null], requires: 'resume requires a query', readsLocalStore: true,
|
|
604
|
+
allowed: ['all', 'db', 'fts', 'json', 'local', 'remote'] }],
|
|
605
|
+
['pack', { name: 'pack', positionals: [1, null], requires: 'pack requires a query', readsLocalStore: true,
|
|
606
|
+
validate: (args) => { nonNegativeIntFlag(args, 'tokens'); },
|
|
607
|
+
allowed: ['all', 'db', 'fts', 'json', 'limit', 'local', 'project', 'remote', 'source', 'tag', 'tokens'] }],
|
|
608
|
+
['stats', { name: 'stats', positionals: [0, 0], readsLocalStore: true,
|
|
609
|
+
allowed: ['all', 'db', 'json', 'local', 'remote', 'tag'] }],
|
|
610
|
+
// sync and `sessions discover` build the store rather than read it, so they
|
|
611
|
+
// do not bootstrap first; running them is itself the remedy for an empty one.
|
|
612
|
+
['sync', { name: 'sync', positionals: [0, 0], allowed: ['all', 'db', 'json', 'local', 'remote'] }],
|
|
613
|
+
]);
|
|
614
|
+
/** Command words consumed before the positional arguments start. */
|
|
615
|
+
function commandWords(command) {
|
|
616
|
+
if (command === undefined)
|
|
617
|
+
return 0;
|
|
618
|
+
return command === 'sessions' ? 2 : 1;
|
|
619
|
+
}
|
|
620
|
+
function commandSpec(command, subcommand) {
|
|
621
|
+
if (command === undefined)
|
|
622
|
+
return COMMANDS.get('');
|
|
623
|
+
if (command === 'sessions')
|
|
624
|
+
return subcommand ? COMMANDS.get(`sessions ${subcommand}`) : undefined;
|
|
625
|
+
return COMMANDS.get(command);
|
|
626
|
+
}
|
|
627
|
+
function unknownCommandMessage(command, subcommand) {
|
|
628
|
+
if (command === undefined)
|
|
629
|
+
return 'invalid usage';
|
|
630
|
+
if (command === 'sessions') {
|
|
631
|
+
if (subcommand === undefined)
|
|
632
|
+
return 'sessions requires a subcommand';
|
|
633
|
+
return `unknown sessions subcommand '${subcommand}'`;
|
|
634
|
+
}
|
|
635
|
+
return `unknown command '${command}'`;
|
|
636
|
+
}
|
|
637
|
+
// A store that was never built and a store holding no match are different
|
|
638
|
+
// answers, and `No results.` is only true of the second. Wording and exit code
|
|
639
|
+
// separate them; the query is not run against a store that cannot hold one.
|
|
640
|
+
function reportUnusableStore(readiness, json) {
|
|
641
|
+
if (readiness.status === 'ready' || readiness.status === 'skipped')
|
|
642
|
+
return false;
|
|
643
|
+
const message = readiness.status === 'unbuilt'
|
|
644
|
+
? 'No local index yet: run ai-hist (or ai-hist sync) to build one.'
|
|
645
|
+
: 'No searchable local sessions found. Start a coding-agent session, then run ai-hist again.';
|
|
646
|
+
if (json)
|
|
647
|
+
output({ status: readiness.status, indexedPrompts: readiness.indexedPrompts, message }, true);
|
|
648
|
+
else
|
|
649
|
+
process.stdout.write(`${message}\n`);
|
|
650
|
+
process.exitCode = 1;
|
|
651
|
+
return true;
|
|
652
|
+
}
|
|
498
653
|
async function main() {
|
|
499
654
|
const rawArgs = process.argv.slice(2);
|
|
500
655
|
const versionArgs = rawArgs.filter((arg) => arg !== '--no-warning');
|
|
@@ -504,30 +659,85 @@ async function main() {
|
|
|
504
659
|
await maybePrintUpdateNotice(version, rawArgs);
|
|
505
660
|
return;
|
|
506
661
|
}
|
|
507
|
-
const args = parse(rawArgs);
|
|
662
|
+
const args = parse(rawArgs.map((arg) => arg === '-h' ? '--help' : arg));
|
|
508
663
|
const [command, subcommand, ...rest] = args.positional;
|
|
509
664
|
const json = args.flags.has('json');
|
|
665
|
+
// Handle help before command resolution to prevent unknown command errors
|
|
666
|
+
if (args.flags.has('help') && command === undefined) {
|
|
667
|
+
showHelp();
|
|
668
|
+
}
|
|
669
|
+
if (command === 'help' && subcommand === undefined && rest.length === 0) {
|
|
670
|
+
showHelp();
|
|
671
|
+
}
|
|
672
|
+
if (command === 'sessions' && subcommand === undefined && args.flags.has('help')) {
|
|
673
|
+
showHelp();
|
|
674
|
+
}
|
|
675
|
+
const spec = commandSpec(command, subcommand);
|
|
676
|
+
if (!spec)
|
|
677
|
+
usage(unknownCommandMessage(command, subcommand));
|
|
678
|
+
if (args.flags.has('help') && spec.allowed.includes('help')) {
|
|
679
|
+
showHelp();
|
|
680
|
+
}
|
|
681
|
+
// The whole command line is checked before any work: a line that is going to
|
|
682
|
+
// be rejected must not first spend a first-run bootstrap only to exit 2.
|
|
683
|
+
validateFlags(args, spec.name, spec.readsLocalStore ? [...spec.allowed, 'no-bootstrap'] : spec.allowed);
|
|
684
|
+
if (spec.rejectsScope)
|
|
685
|
+
rejectScopeFlag(args, spec.name);
|
|
686
|
+
const tail = args.positional.slice(commandWords(command));
|
|
687
|
+
const [least, most] = spec.positionals;
|
|
688
|
+
if (tail.length < least)
|
|
689
|
+
usage(spec.requires);
|
|
690
|
+
if (most !== null && tail.length > most)
|
|
691
|
+
rejectSurplusPositionals(tail.slice(most), spec.name);
|
|
692
|
+
spec.validate?.(args);
|
|
693
|
+
const intervalSeconds = numberFlag(args, 'interval') ?? 60;
|
|
694
|
+
const sessionSource = command === 'sessions' ? tail[0] : undefined;
|
|
695
|
+
const sessionId = command === 'sessions' ? tail[1] : undefined;
|
|
696
|
+
const recentFallback = command === 'recent' && tail.length > 0 ? Number(tail[0]) : undefined;
|
|
697
|
+
let readiness = null;
|
|
698
|
+
if (spec.readsLocalStore) {
|
|
699
|
+
readiness = await ensureLocalStore({
|
|
700
|
+
dbPath: textFlag(args, 'db'), scope: scopeFlag(args), bootstrap: !args.flags.has('no-bootstrap'),
|
|
701
|
+
});
|
|
702
|
+
if (readiness.bootstrap?.status === 'partial') {
|
|
703
|
+
process.stderr.write('Some local sessions could not be fully indexed; run ai-hist --json for diagnostics.\n');
|
|
704
|
+
}
|
|
705
|
+
// The bare invocation reports the store's condition as its result and
|
|
706
|
+
// succeeds either way. Every command that asks the store a question refuses
|
|
707
|
+
// to answer out of one that cannot hold an answer.
|
|
708
|
+
if (command !== undefined && reportUnusableStore(readiness, json))
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
510
711
|
if (command === undefined) {
|
|
511
|
-
|
|
512
|
-
|
|
712
|
+
// --no-bootstrap answers from the catalog as it stands; the bootstrap path
|
|
713
|
+
// reports what it just indexed.
|
|
714
|
+
if (!readiness?.bootstrap) {
|
|
513
715
|
output(await listSessionCatalogPage({ dbPath: textFlag(args, 'db') }), json);
|
|
514
716
|
return;
|
|
515
717
|
}
|
|
516
|
-
const result = await bootstrapLocal({ dbPath: textFlag(args, 'db') });
|
|
517
718
|
if (json)
|
|
518
|
-
output(
|
|
719
|
+
output(readiness.bootstrap, true);
|
|
519
720
|
else {
|
|
520
|
-
process.stdout.write(
|
|
521
|
-
? `Ready: ${
|
|
721
|
+
process.stdout.write(readiness.indexedPrompts > 0
|
|
722
|
+
? `Ready: ${readiness.indexedPrompts} indexed prompt(s). Search with: ai-hist search "your query"\n`
|
|
522
723
|
: 'No searchable local sessions found. Start a coding-agent session, then run ai-hist again.\n');
|
|
523
|
-
if (result.status === 'partial')
|
|
524
|
-
process.stderr.write('Some local sessions could not be fully indexed; run ai-hist --json for diagnostics.\n');
|
|
525
724
|
}
|
|
526
725
|
return;
|
|
527
726
|
}
|
|
727
|
+
if (command === 'login') {
|
|
728
|
+
const relayAccessToken = await relayAccessTokenForCloudCommand(command, rawArgs, args, false);
|
|
729
|
+
const auth = await login({
|
|
730
|
+
baseUrl: textFlag(args, 'base-url'),
|
|
731
|
+
relayAccessToken,
|
|
732
|
+
label: textFlag(args, 'label'),
|
|
733
|
+
});
|
|
734
|
+
if (json)
|
|
735
|
+
output({ ok: true, baseUrl: auth.baseUrl }, true);
|
|
736
|
+
else
|
|
737
|
+
process.stdout.write(`Logged in to ${auth.baseUrl} (session stored).\n`);
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
528
740
|
if (command === 'token') {
|
|
529
|
-
validateFlags(args, 'token', ['base-url']);
|
|
530
|
-
rejectSurplusPositionals(args.positional.slice(1), 'token');
|
|
531
741
|
const token = await accessToken({ baseUrl: textFlag(args, 'base-url') });
|
|
532
742
|
if (process.stdout.isTTY)
|
|
533
743
|
process.stderr.write('Warning: this access token is a secret and will remain in terminal scrollback.\n');
|
|
@@ -535,10 +745,6 @@ async function main() {
|
|
|
535
745
|
return;
|
|
536
746
|
}
|
|
537
747
|
if (command === 'replay') {
|
|
538
|
-
validateFlags(args, 'replay', ['base-url', 'limit', 'max-content', 'json', 'out']);
|
|
539
|
-
if (!subcommand)
|
|
540
|
-
usage('replay requires SESSION_ID');
|
|
541
|
-
rejectSurplusPositionals(rest, 'replay');
|
|
542
748
|
const result = await replay(subcommand, {
|
|
543
749
|
baseUrl: textFlag(args, 'base-url'), limit: nonNegativeIntFlag(args, 'limit'),
|
|
544
750
|
maxContent: nonNegativeIntFlag(args, 'max-content'), json, out: textFlag(args, 'out'),
|
|
@@ -548,16 +754,10 @@ async function main() {
|
|
|
548
754
|
return;
|
|
549
755
|
}
|
|
550
756
|
if (command === 'enable-cloud') {
|
|
551
|
-
|
|
552
|
-
rejectSurplusPositionals(args.positional.slice(1), 'enable-cloud');
|
|
553
|
-
// --interval is seconds; validate before converting so the error names the
|
|
554
|
-
// unit the caller actually typed rather than a millisecond bound.
|
|
555
|
-
const intervalSeconds = numberFlag(args, 'interval') ?? 60;
|
|
556
|
-
if (!Number.isSafeInteger(intervalSeconds) || intervalSeconds < 1 || intervalSeconds > 2_147_483) {
|
|
557
|
-
usage('--interval must be a whole number of seconds between 1 and 2147483');
|
|
558
|
-
}
|
|
757
|
+
const relayAccessToken = await relayAccessTokenForCloudCommand(command, rawArgs, args, true);
|
|
559
758
|
const handle = await enableCloud({
|
|
560
759
|
baseUrl: textFlag(args, 'base-url'), dbPath: textFlag(args, 'db'),
|
|
760
|
+
relayAccessToken,
|
|
561
761
|
intervalMs: intervalSeconds * 1000,
|
|
562
762
|
watch: !args.flags.has('once'),
|
|
563
763
|
onPush: (result) => output(result, json),
|
|
@@ -572,14 +772,7 @@ async function main() {
|
|
|
572
772
|
return;
|
|
573
773
|
}
|
|
574
774
|
if (command === 'sessions' && subcommand === 'list') {
|
|
575
|
-
validateFlags(args, 'sessions list', [
|
|
576
|
-
'after', 'after-ms', 'after-session-id', 'after-source', 'all', 'before-ms', 'db',
|
|
577
|
-
'json', 'limit', 'local', 'pretty', 'remote', 'source',
|
|
578
|
-
]);
|
|
579
|
-
rejectSurplusPositionals(rest, 'sessions list');
|
|
580
775
|
const sources = textFlags(args, 'source');
|
|
581
|
-
if (json && args.flags.has('pretty'))
|
|
582
|
-
usage('--pretty and --json are mutually exclusive');
|
|
583
776
|
const page = await listSessionCatalogPage({
|
|
584
777
|
dbPath: textFlag(args, 'db'), scope: scopeFlag(args), sources: sources.length ? sources : undefined,
|
|
585
778
|
limit: numberFlag(args, 'limit'), beforeMs: numberFlag(args, 'before-ms'),
|
|
@@ -598,8 +791,6 @@ async function main() {
|
|
|
598
791
|
return;
|
|
599
792
|
}
|
|
600
793
|
if (command === 'sessions' && subcommand === 'discover') {
|
|
601
|
-
validateFlags(args, 'sessions discover', ['all', 'db', 'json', 'limit', 'local', 'remote', 'source']);
|
|
602
|
-
rejectSurplusPositionals(rest, 'sessions discover');
|
|
603
794
|
const sources = textFlags(args, 'source');
|
|
604
795
|
outputDiscovery(await discoverSessions({
|
|
605
796
|
dbPath: textFlag(args, 'db'), scope: scopeFlag(args), sources: sources.length ? sources : undefined,
|
|
@@ -608,14 +799,9 @@ async function main() {
|
|
|
608
799
|
return;
|
|
609
800
|
}
|
|
610
801
|
if (command === 'sessions' && subcommand === 'hydrate') {
|
|
611
|
-
validateFlags(args, 'sessions hydrate', ['all', 'db', 'json', 'local', 'no-related', 'remote']);
|
|
612
|
-
const [source, sessionId, ...surplus] = rest;
|
|
613
|
-
if (!source || !sessionId)
|
|
614
|
-
usage('sessions hydrate requires SOURCE and SESSION_ID');
|
|
615
|
-
rejectSurplusPositionals(surplus, 'sessions hydrate');
|
|
616
802
|
outputHydration(await hydrateSession({
|
|
617
|
-
source:
|
|
618
|
-
sessionId,
|
|
803
|
+
source: sessionSource,
|
|
804
|
+
sessionId: sessionId,
|
|
619
805
|
scope: scopeFlag(args),
|
|
620
806
|
dbPath: textFlag(args, 'db'),
|
|
621
807
|
includeRelated: !args.flags.has('no-related'),
|
|
@@ -623,85 +809,46 @@ async function main() {
|
|
|
623
809
|
return;
|
|
624
810
|
}
|
|
625
811
|
if (command === 'sessions' && subcommand === 'relationships') {
|
|
626
|
-
validateFlags(args, 'sessions relationships', ['all', 'db', 'json', 'local', 'remote']);
|
|
627
|
-
const [source, sessionId, ...surplus] = rest;
|
|
628
|
-
if (!source || !sessionId)
|
|
629
|
-
usage('sessions relationships requires SOURCE and SESSION_ID');
|
|
630
|
-
rejectSurplusPositionals(surplus, 'sessions relationships');
|
|
631
|
-
rejectScopeFlag(args, 'sessions relationships');
|
|
632
812
|
outputRelationships(await getSessionRelationships({
|
|
633
|
-
source:
|
|
813
|
+
source: sessionSource, sessionId: sessionId, dbPath: textFlag(args, 'db'),
|
|
634
814
|
}), json);
|
|
635
815
|
return;
|
|
636
816
|
}
|
|
637
817
|
if (command === 'sessions' && subcommand === 'tree') {
|
|
638
|
-
validateFlags(args, 'sessions tree', ['all', 'db', 'json', 'local', 'max-depth', 'max-nodes', 'remote']);
|
|
639
|
-
const [source, sessionId, ...surplus] = rest;
|
|
640
|
-
if (!source || !sessionId)
|
|
641
|
-
usage('sessions tree requires SOURCE and SESSION_ID');
|
|
642
|
-
rejectSurplusPositionals(surplus, 'sessions tree');
|
|
643
|
-
rejectScopeFlag(args, 'sessions tree');
|
|
644
818
|
outputTree(await getSessionTree({
|
|
645
|
-
source:
|
|
819
|
+
source: sessionSource, sessionId: sessionId, dbPath: textFlag(args, 'db'),
|
|
646
820
|
maxDepth: numberFlag(args, 'max-depth'), maxNodes: numberFlag(args, 'max-nodes'),
|
|
647
821
|
}), json);
|
|
648
822
|
return;
|
|
649
823
|
}
|
|
650
824
|
if (command === 'sessions' && (subcommand === 'tools' || subcommand === 'edits')) {
|
|
651
825
|
const name = `sessions ${subcommand}`;
|
|
652
|
-
validateFlags(args, name, ['after', 'db', 'json', 'limit']);
|
|
653
|
-
const [source, sessionId, ...surplus] = rest;
|
|
654
|
-
if (!source || !sessionId)
|
|
655
|
-
usage(`${name} requires SOURCE and SESSION_ID`);
|
|
656
|
-
rejectSurplusPositionals(surplus, name);
|
|
657
826
|
const options = {
|
|
658
827
|
dbPath: textFlag(args, 'db'),
|
|
659
828
|
limit: numberFlag(args, 'limit'),
|
|
660
829
|
after: evidenceCursorFlag(args),
|
|
661
830
|
};
|
|
662
831
|
if (subcommand === 'tools') {
|
|
663
|
-
outputToolCalls(await getSessionToolCallsPage(
|
|
832
|
+
outputToolCalls(await getSessionToolCallsPage(sessionSource, sessionId, options), json);
|
|
664
833
|
}
|
|
665
834
|
else {
|
|
666
|
-
outputFileEdits(await getSessionFileEditsPage(
|
|
835
|
+
outputFileEdits(await getSessionFileEditsPage(sessionSource, sessionId, options), json);
|
|
667
836
|
}
|
|
668
837
|
return;
|
|
669
838
|
}
|
|
670
839
|
if (command === 'search') {
|
|
671
|
-
validateFlags(args, 'search', [
|
|
672
|
-
'all', 'before-ms', 'db', 'fts', 'json', 'limit', 'local', 'project', 'remote', 'source', 'tag',
|
|
673
|
-
]);
|
|
674
|
-
if (!subcommand)
|
|
675
|
-
usage();
|
|
676
840
|
output(await search([subcommand, ...rest].join(' '), { ...common(args), rawFts: args.flags.has('fts') }), json);
|
|
677
841
|
return;
|
|
678
842
|
}
|
|
679
843
|
if (command === 'recent') {
|
|
680
|
-
|
|
681
|
-
'all', 'before-ms', 'db', 'json', 'limit', 'local', 'project', 'remote', 'source', 'tag',
|
|
682
|
-
]);
|
|
683
|
-
rejectSurplusPositionals(rest, 'recent');
|
|
684
|
-
const fallback = subcommand ? Number(subcommand) : undefined;
|
|
685
|
-
if (fallback !== undefined && !Number.isFinite(fallback))
|
|
686
|
-
usage(`recent count must be a number (got '${subcommand}')`);
|
|
687
|
-
output(await recent({ ...common(args), limit: numberFlag(args, 'limit') ?? fallback }), json);
|
|
844
|
+
output(await recent({ ...common(args), limit: numberFlag(args, 'limit') ?? recentFallback }), json);
|
|
688
845
|
return;
|
|
689
846
|
}
|
|
690
847
|
if (command === 'session') {
|
|
691
|
-
validateFlags(args, 'session', ['all', 'db', 'json', 'local', 'remote', 'source', 'tag']);
|
|
692
|
-
if (!subcommand)
|
|
693
|
-
usage();
|
|
694
|
-
rejectSurplusPositionals(rest, 'session');
|
|
695
|
-
rejectScopeFlag(args, 'session');
|
|
696
848
|
output(await getSession(subcommand, { dbPath: textFlag(args, 'db'), source: textFlag(args, 'source'), tag: textFlag(args, 'tag') }), json);
|
|
697
849
|
return;
|
|
698
850
|
}
|
|
699
851
|
if (command === 'events') {
|
|
700
|
-
validateFlags(args, 'events', ['after', 'all', 'db', 'json', 'limit', 'local', 'remote', 'source']);
|
|
701
|
-
if (!subcommand)
|
|
702
|
-
usage();
|
|
703
|
-
rejectSurplusPositionals(rest, 'events');
|
|
704
|
-
rejectScopeFlag(args, 'events');
|
|
705
852
|
output(await getSessionEventsPage(subcommand, {
|
|
706
853
|
dbPath: textFlag(args, 'db'), source: textFlag(args, 'source'),
|
|
707
854
|
limit: numberFlag(args, 'limit'), after: cursorFlag(args),
|
|
@@ -717,14 +864,10 @@ async function main() {
|
|
|
717
864
|
return;
|
|
718
865
|
}
|
|
719
866
|
if (command === 'stats') {
|
|
720
|
-
validateFlags(args, 'stats', ['all', 'db', 'json', 'local', 'remote', 'tag']);
|
|
721
|
-
rejectSurplusPositionals([subcommand, ...rest].filter((value) => value !== undefined), 'stats');
|
|
722
867
|
output(await stats({ dbPath: textFlag(args, 'db'), scope: scopeFlag(args), tag: textFlag(args, 'tag') }), json);
|
|
723
868
|
return;
|
|
724
869
|
}
|
|
725
870
|
if (command === 'sync') {
|
|
726
|
-
validateFlags(args, 'sync', ['all', 'db', 'json', 'local', 'remote']);
|
|
727
|
-
rejectSurplusPositionals([subcommand, ...rest].filter((value) => value !== undefined), 'sync');
|
|
728
871
|
output(await sync({ dbPath: textFlag(args, 'db'), scope: scopeFlag(args) }), json);
|
|
729
872
|
return;
|
|
730
873
|
}
|