@phnx-labs/sessions-cli 0.1.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/CHANGELOG.md +9 -0
- package/LICENSE +105 -0
- package/README.md +26 -0
- package/dist/commands/sessions.d.ts +6 -0
- package/dist/commands/sessions.js +69 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +35 -0
- package/dist/lib/paths.d.ts +7 -0
- package/dist/lib/paths.js +16 -0
- package/dist/lib/query.d.ts +47 -0
- package/dist/lib/query.js +156 -0
- package/dist/lib/version.d.ts +1 -0
- package/dist/lib/version.js +18 -0
- package/dist/protocol-server.d.ts +4 -0
- package/dist/protocol-server.js +69 -0
- package/dist/protocol.d.ts +23 -0
- package/dist/protocol.js +1 -0
- package/package.json +40 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
- First release. Local FTS search, list, and id lookup against `sessions.db`.
|
|
6
|
+
- Argv fast paths `__query` and `__serve` intercept before commander.
|
|
7
|
+
- FTS input is tokenized the same way as agents-cli, so hyphenated terms like `secrets-cli` no longer crash MATCH as a column name.
|
|
8
|
+
- `__serve` rejects request bodies over 1 MiB.
|
|
9
|
+
- npm pack ships compiled JS only. Push `release/x.y.z` (or `release/x.y.z-pre.n`) to publish.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Functional Source License, Version 1.1, Apache 2.0 Future License
|
|
2
|
+
|
|
3
|
+
## Abbreviation
|
|
4
|
+
|
|
5
|
+
FSL-1.1-Apache-2.0
|
|
6
|
+
|
|
7
|
+
## Notice
|
|
8
|
+
|
|
9
|
+
Copyright 2026 Phoenix Horizon, Inc.
|
|
10
|
+
|
|
11
|
+
## Terms and Conditions
|
|
12
|
+
|
|
13
|
+
### Licensor ("We")
|
|
14
|
+
|
|
15
|
+
The party offering the Software under these Terms and Conditions.
|
|
16
|
+
|
|
17
|
+
### The Software
|
|
18
|
+
|
|
19
|
+
The "Software" is each version of the software that we make available under
|
|
20
|
+
these Terms and Conditions, as indicated by our inclusion of these Terms and
|
|
21
|
+
Conditions with the Software.
|
|
22
|
+
|
|
23
|
+
### License Grant
|
|
24
|
+
|
|
25
|
+
Subject to your compliance with this License Grant and the Patents,
|
|
26
|
+
Redistribution and Trademark clauses below, we hereby grant you the right to
|
|
27
|
+
use, copy, modify, create derivative works, publicly perform, publicly display
|
|
28
|
+
and redistribute the Software for any Permitted Purpose identified below.
|
|
29
|
+
|
|
30
|
+
### Permitted Purpose
|
|
31
|
+
|
|
32
|
+
A Permitted Purpose is any purpose other than a Competing Use. A Competing Use
|
|
33
|
+
means making the Software available to others in a commercial product or
|
|
34
|
+
service that:
|
|
35
|
+
|
|
36
|
+
1. substitutes for the Software;
|
|
37
|
+
|
|
38
|
+
2. substitutes for any other product or service we offer using the Software
|
|
39
|
+
that exists as of the date we make the Software available; or
|
|
40
|
+
|
|
41
|
+
3. offers the same or substantially similar functionality as the Software.
|
|
42
|
+
|
|
43
|
+
Permitted Purposes specifically include using the Software:
|
|
44
|
+
|
|
45
|
+
1. for your internal use and access;
|
|
46
|
+
|
|
47
|
+
2. for non-commercial education;
|
|
48
|
+
|
|
49
|
+
3. for non-commercial research; and
|
|
50
|
+
|
|
51
|
+
4. in connection with professional services that you provide to a licensee
|
|
52
|
+
using the Software in accordance with these Terms and Conditions.
|
|
53
|
+
|
|
54
|
+
### Patents
|
|
55
|
+
|
|
56
|
+
To the extent your use for a Permitted Purpose would necessarily infringe our
|
|
57
|
+
patents, the license grant above includes a license under our patents. If you
|
|
58
|
+
make a claim against any party that the Software infringes or contributes to
|
|
59
|
+
the infringement of any patent, then your patent license to the Software ends
|
|
60
|
+
immediately.
|
|
61
|
+
|
|
62
|
+
### Redistribution
|
|
63
|
+
|
|
64
|
+
The Terms and Conditions apply to all copies, modifications and derivatives of
|
|
65
|
+
the Software.
|
|
66
|
+
|
|
67
|
+
If you redistribute any copies, modifications or derivatives of the Software,
|
|
68
|
+
you must include a copy of or a link to these Terms and Conditions and not
|
|
69
|
+
remove any copyright notices provided in or with the Software.
|
|
70
|
+
|
|
71
|
+
### Disclaimer
|
|
72
|
+
|
|
73
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND WITHOUT WARRANTIES OF ANY KIND, EXPRESS OR
|
|
74
|
+
IMPLIED, INCLUDING WITHOUT LIMITATION WARRANTIES OF FITNESS FOR A PARTICULAR
|
|
75
|
+
PURPOSE, MERCHANTABILITY, TITLE OR NON-INFRINGEMENT.
|
|
76
|
+
|
|
77
|
+
IN NO EVENT WILL WE HAVE ANY LIABILITY TO YOU ARISING OUT OF OR RELATED TO THE
|
|
78
|
+
SOFTWARE, INCLUDING INDIRECT, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES,
|
|
79
|
+
EVEN IF WE HAVE BEEN INFORMED OF THEIR POSSIBILITY IN ADVANCE.
|
|
80
|
+
|
|
81
|
+
### Trademarks
|
|
82
|
+
|
|
83
|
+
Except for displaying the License Details and identifying us as the origin of
|
|
84
|
+
the Software, you have no right under these Terms and Conditions to use our
|
|
85
|
+
trademarks, trade names, service marks or product names.
|
|
86
|
+
|
|
87
|
+
## Grant of Future License
|
|
88
|
+
|
|
89
|
+
We hereby irrevocably grant you an additional license to use the Software under
|
|
90
|
+
the Apache License, Version 2.0 that is effective on the second anniversary of
|
|
91
|
+
the date we make the Software available. On or after that date, you may use the
|
|
92
|
+
Software under the Apache License, Version 2.0, in which case the following
|
|
93
|
+
will apply:
|
|
94
|
+
|
|
95
|
+
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
|
|
96
|
+
this file except in compliance with the License.
|
|
97
|
+
|
|
98
|
+
You may obtain a copy of the License at
|
|
99
|
+
|
|
100
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
101
|
+
|
|
102
|
+
Unless required by applicable law or agreed to in writing, software distributed
|
|
103
|
+
under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
|
104
|
+
CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
|
105
|
+
specific language governing permissions and limitations under the License.
|
package/README.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# sessions
|
|
2
|
+
|
|
3
|
+
High-performance session index for agents-cli. The installed binary is `sessions`.
|
|
4
|
+
|
|
5
|
+
It reads the existing SQLite index at `~/.agents/.history/sessions/sessions.db`
|
|
6
|
+
(override with `$SESSIONS_DB`). It does not boot agents-cli, does not fan out
|
|
7
|
+
over SSH, and does not walk transcript directories.
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
npm i -g @phnx-labs/sessions-cli
|
|
11
|
+
|
|
12
|
+
sessions "auth middleware" --json --limit 5
|
|
13
|
+
sessions a1b2c3d4 --json
|
|
14
|
+
sessions --limit 20
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Hidden fast paths (no commander tree):
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
sessions __query --json --limit 5 -- auth
|
|
21
|
+
sessions __serve # request JSON on fd 3, response on fd 4
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
v1 is the read engine. `resume` / `stop` / `inject` stay on `agents sessions <verb>`.
|
|
25
|
+
|
|
26
|
+
See PHNX-4012.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { type SessionRow } from '../lib/query.js';
|
|
3
|
+
export declare function printSessions(rows: SessionRow[], json: boolean): void;
|
|
4
|
+
export declare function runQuery(argv: string[]): number;
|
|
5
|
+
export declare function registerSessionsCommands(program: Command): void;
|
|
6
|
+
export declare function buildProgram(): Command;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { getCliVersion } from '../lib/version.js';
|
|
4
|
+
import { parseQueryArgs, querySessions } from '../lib/query.js';
|
|
5
|
+
function formatRow(row) {
|
|
6
|
+
const id = chalk.cyan(row.shortId.padEnd(8));
|
|
7
|
+
const agent = (row.agent || '-').padEnd(8);
|
|
8
|
+
const version = (row.version || '-').slice(0, 8).padEnd(8);
|
|
9
|
+
const project = (row.project || '-').slice(0, 16).padEnd(16);
|
|
10
|
+
const title = (row.label || row.topic || '-').replace(/\s+/g, ' ').slice(0, 40);
|
|
11
|
+
return `${id} ${agent} ${version} ${project} ${title}`;
|
|
12
|
+
}
|
|
13
|
+
export function printSessions(rows, json) {
|
|
14
|
+
if (json) {
|
|
15
|
+
process.stdout.write(JSON.stringify(rows, null, 2) + '\n');
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
if (rows.length === 0) {
|
|
19
|
+
process.stderr.write('No sessions matched.\n');
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
for (const row of rows)
|
|
23
|
+
process.stdout.write(formatRow(row) + '\n');
|
|
24
|
+
process.stderr.write(`${rows.length} session${rows.length === 1 ? '' : 's'}.\n`);
|
|
25
|
+
}
|
|
26
|
+
export function runQuery(argv) {
|
|
27
|
+
const json = argv.includes('--json');
|
|
28
|
+
const options = parseQueryArgs(argv);
|
|
29
|
+
try {
|
|
30
|
+
const rows = querySessions(options);
|
|
31
|
+
printSessions(rows, json);
|
|
32
|
+
return 0;
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
36
|
+
return 1;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export function registerSessionsCommands(program) {
|
|
40
|
+
program
|
|
41
|
+
.argument('[query...]', 'FTS query, or a session id')
|
|
42
|
+
.option('--json', 'JSON array of SessionMeta-shaped rows')
|
|
43
|
+
.option('--limit <n>', 'max rows', '20')
|
|
44
|
+
.option('--agent <id>', 'filter by harness (claude, grok, …)')
|
|
45
|
+
.option('--local', 'local index only (always true in v1; accepted for agents-cli flag parity)')
|
|
46
|
+
.option('--no-interactive', 'ignored; this CLI never opens a picker')
|
|
47
|
+
.allowExcessArguments()
|
|
48
|
+
.allowUnknownOption()
|
|
49
|
+
.action(() => {
|
|
50
|
+
process.exit(runQuery(process.argv.slice(2)));
|
|
51
|
+
});
|
|
52
|
+
program.addHelpText('after', `
|
|
53
|
+
Examples:
|
|
54
|
+
sessions "auth middleware" --json --limit 5
|
|
55
|
+
sessions a1b2c3d4 --json
|
|
56
|
+
sessions --limit 20
|
|
57
|
+
|
|
58
|
+
v1 is the read/index engine. resume/stop/inject stay on \`agents sessions <verb>\`.
|
|
59
|
+
Hidden fast paths (no commander): __query __serve
|
|
60
|
+
`);
|
|
61
|
+
}
|
|
62
|
+
export function buildProgram() {
|
|
63
|
+
const program = new Command('sessions')
|
|
64
|
+
.description('Search and list indexed agent sessions. Local, no SSH, no agents-cli boot.')
|
|
65
|
+
.version(getCliVersion(), '-v, --version')
|
|
66
|
+
.showHelpAfterError();
|
|
67
|
+
registerSessionsCommands(program);
|
|
68
|
+
return program;
|
|
69
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/usr/bin/env -S node --no-warnings=ExperimentalWarning
|
|
2
|
+
/**
|
|
3
|
+
* `sessions` — standalone session index CLI (PHNX-4012).
|
|
4
|
+
*
|
|
5
|
+
* Hidden entrypoints are intercepted BEFORE commander so they never pay for
|
|
6
|
+
* command-tree construction:
|
|
7
|
+
*
|
|
8
|
+
* - `__query` FTS / list / id lookup, JSON or text, then exit
|
|
9
|
+
* - `__serve` private consumer pipe: request on fd 3, response on fd 4
|
|
10
|
+
*/
|
|
11
|
+
import { getCliVersion } from './lib/version.js';
|
|
12
|
+
process.on('SIGPIPE', () => { });
|
|
13
|
+
const verb = process.argv[2];
|
|
14
|
+
if (verb === '__serve') {
|
|
15
|
+
const { runProtocolServer } = await import('./protocol-server.js');
|
|
16
|
+
runProtocolServer();
|
|
17
|
+
process.exit(process.exitCode ?? 0);
|
|
18
|
+
}
|
|
19
|
+
if (verb === '__query') {
|
|
20
|
+
const { runQuery } = await import('./commands/sessions.js');
|
|
21
|
+
process.exit(runQuery(process.argv.slice(3)));
|
|
22
|
+
}
|
|
23
|
+
if (verb === '--version' || verb === '-v') {
|
|
24
|
+
process.stdout.write(getCliVersion() + '\n');
|
|
25
|
+
process.exit(0);
|
|
26
|
+
}
|
|
27
|
+
const { buildProgram } = await import('./commands/sessions.js');
|
|
28
|
+
const program = buildProgram();
|
|
29
|
+
try {
|
|
30
|
+
await program.parseAsync(process.argv);
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Default index path — the same file agents-cli already writes. */
|
|
2
|
+
export declare function defaultSessionsDbPath(home?: string): string;
|
|
3
|
+
/**
|
|
4
|
+
* Resolve the SQLite index. `$SESSIONS_DB` wins (tests, power users),
|
|
5
|
+
* then `$AGENTS_SESSIONS_DB` (the agents-cli test seam), else the default.
|
|
6
|
+
*/
|
|
7
|
+
export declare function resolveSessionsDbPath(env?: NodeJS.ProcessEnv, home?: string): string;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import * as os from 'os';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
/** Default index path — the same file agents-cli already writes. */
|
|
4
|
+
export function defaultSessionsDbPath(home = os.homedir()) {
|
|
5
|
+
return path.join(home, '.agents', '.history', 'sessions', 'sessions.db');
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Resolve the SQLite index. `$SESSIONS_DB` wins (tests, power users),
|
|
9
|
+
* then `$AGENTS_SESSIONS_DB` (the agents-cli test seam), else the default.
|
|
10
|
+
*/
|
|
11
|
+
export function resolveSessionsDbPath(env = process.env, home = os.homedir()) {
|
|
12
|
+
const explicit = env.SESSIONS_DB?.trim() || env.AGENTS_SESSIONS_DB?.trim();
|
|
13
|
+
if (explicit)
|
|
14
|
+
return explicit;
|
|
15
|
+
return defaultSessionsDbPath(home);
|
|
16
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
2
|
+
export interface SessionRow {
|
|
3
|
+
id: string;
|
|
4
|
+
shortId: string;
|
|
5
|
+
agent: string;
|
|
6
|
+
version: string | null;
|
|
7
|
+
account: string | null;
|
|
8
|
+
timestamp: string;
|
|
9
|
+
lastActivity: string | null;
|
|
10
|
+
project: string | null;
|
|
11
|
+
cwd: string | null;
|
|
12
|
+
gitBranch: string | null;
|
|
13
|
+
topic: string | null;
|
|
14
|
+
label: string | null;
|
|
15
|
+
messageCount: number | null;
|
|
16
|
+
tokenCount: number | null;
|
|
17
|
+
filePath: string;
|
|
18
|
+
origin: string | null;
|
|
19
|
+
machine: string | null;
|
|
20
|
+
model: string | null;
|
|
21
|
+
harness: string | null;
|
|
22
|
+
ticketId: string | null;
|
|
23
|
+
prUrl: string | null;
|
|
24
|
+
durationMs: number | null;
|
|
25
|
+
costUsd: number | null;
|
|
26
|
+
}
|
|
27
|
+
export interface QueryOptions {
|
|
28
|
+
query?: string;
|
|
29
|
+
limit?: number;
|
|
30
|
+
idPrefix?: string;
|
|
31
|
+
agent?: string;
|
|
32
|
+
dbPath?: string;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Turn a raw user query into a safe FTS5 MATCH expression.
|
|
36
|
+
* Same tokenizer as agents-cli: split on non-alphanumeric, keep tokens of
|
|
37
|
+
* 2+ chars, prefix-match with OR. Hyphens, colons, and FTS operators never
|
|
38
|
+
* reach MATCH as syntax — `secrets-cli` cannot throw `no such column: cli`.
|
|
39
|
+
*/
|
|
40
|
+
export declare function buildFtsQuery(input: string): {
|
|
41
|
+
expr: string;
|
|
42
|
+
terms: string[];
|
|
43
|
+
};
|
|
44
|
+
export declare function openSessionsDb(dbPath?: string): DatabaseSync;
|
|
45
|
+
export declare function querySessions(options?: QueryOptions): SessionRow[];
|
|
46
|
+
export declare function looksLikeId(value: string): boolean;
|
|
47
|
+
export declare function parseQueryArgs(argv: string[]): QueryOptions;
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read-only session index. Opens sessions.db, runs FTS / listing / id lookup,
|
|
3
|
+
* never walks the filesystem, never writes, never talks to SSH.
|
|
4
|
+
*
|
|
5
|
+
* Column names match agents-cli SessionMeta JSON so `agents sessions --json`
|
|
6
|
+
* consumers keep working after the passthrough cutover.
|
|
7
|
+
*/
|
|
8
|
+
import * as fs from 'fs';
|
|
9
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
10
|
+
import { resolveSessionsDbPath } from './paths.js';
|
|
11
|
+
const SELECT_COLS = `
|
|
12
|
+
s.id,
|
|
13
|
+
s.short_id AS shortId,
|
|
14
|
+
s.agent,
|
|
15
|
+
s.version,
|
|
16
|
+
s.account,
|
|
17
|
+
s.timestamp,
|
|
18
|
+
s.last_activity AS lastActivity,
|
|
19
|
+
s.project,
|
|
20
|
+
s.cwd,
|
|
21
|
+
s.git_branch AS gitBranch,
|
|
22
|
+
s.topic,
|
|
23
|
+
s.label,
|
|
24
|
+
s.message_count AS messageCount,
|
|
25
|
+
s.token_count AS tokenCount,
|
|
26
|
+
s.file_path AS filePath,
|
|
27
|
+
s.origin,
|
|
28
|
+
s.machine,
|
|
29
|
+
s.model,
|
|
30
|
+
s.harness,
|
|
31
|
+
s.ticket_id AS ticketId,
|
|
32
|
+
s.pr_url AS prUrl,
|
|
33
|
+
s.duration_ms AS durationMs,
|
|
34
|
+
s.cost_usd AS costUsd
|
|
35
|
+
`;
|
|
36
|
+
/**
|
|
37
|
+
* Turn a raw user query into a safe FTS5 MATCH expression.
|
|
38
|
+
* Same tokenizer as agents-cli: split on non-alphanumeric, keep tokens of
|
|
39
|
+
* 2+ chars, prefix-match with OR. Hyphens, colons, and FTS operators never
|
|
40
|
+
* reach MATCH as syntax — `secrets-cli` cannot throw `no such column: cli`.
|
|
41
|
+
*/
|
|
42
|
+
export function buildFtsQuery(input) {
|
|
43
|
+
const terms = input.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length >= 2);
|
|
44
|
+
if (terms.length === 0)
|
|
45
|
+
return { expr: '', terms: [] };
|
|
46
|
+
return { expr: terms.map((t) => `${t}*`).join(' OR '), terms };
|
|
47
|
+
}
|
|
48
|
+
export function openSessionsDb(dbPath = resolveSessionsDbPath()) {
|
|
49
|
+
return new DatabaseSync(dbPath, { readOnly: true });
|
|
50
|
+
}
|
|
51
|
+
function rowFrom(raw) {
|
|
52
|
+
return {
|
|
53
|
+
id: String(raw.id),
|
|
54
|
+
shortId: String(raw.shortId ?? ''),
|
|
55
|
+
agent: String(raw.agent ?? ''),
|
|
56
|
+
version: raw.version ?? null,
|
|
57
|
+
account: raw.account ?? null,
|
|
58
|
+
timestamp: String(raw.timestamp ?? ''),
|
|
59
|
+
lastActivity: raw.lastActivity ?? null,
|
|
60
|
+
project: raw.project ?? null,
|
|
61
|
+
cwd: raw.cwd ?? null,
|
|
62
|
+
gitBranch: raw.gitBranch ?? null,
|
|
63
|
+
topic: raw.topic ?? null,
|
|
64
|
+
label: raw.label ?? null,
|
|
65
|
+
messageCount: typeof raw.messageCount === 'number' ? raw.messageCount : null,
|
|
66
|
+
tokenCount: typeof raw.tokenCount === 'number' ? raw.tokenCount : null,
|
|
67
|
+
filePath: String(raw.filePath ?? ''),
|
|
68
|
+
origin: raw.origin ?? null,
|
|
69
|
+
machine: raw.machine ?? null,
|
|
70
|
+
model: raw.model ?? null,
|
|
71
|
+
harness: raw.harness ?? null,
|
|
72
|
+
ticketId: raw.ticketId ?? null,
|
|
73
|
+
prUrl: raw.prUrl ?? null,
|
|
74
|
+
durationMs: typeof raw.durationMs === 'number' ? raw.durationMs : null,
|
|
75
|
+
costUsd: typeof raw.costUsd === 'number' ? raw.costUsd : null,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
export function querySessions(options = {}) {
|
|
79
|
+
const dbPath = options.dbPath ?? resolveSessionsDbPath();
|
|
80
|
+
const limit = Math.max(1, Math.min(options.limit ?? 20, 500));
|
|
81
|
+
if (!fs.existsSync(dbPath) || fs.statSync(dbPath).size === 0)
|
|
82
|
+
return [];
|
|
83
|
+
const db = openSessionsDb(dbPath);
|
|
84
|
+
try {
|
|
85
|
+
if (options.idPrefix && looksLikeId(options.idPrefix) && !options.query) {
|
|
86
|
+
const stmt = db.prepare(`SELECT ${SELECT_COLS} FROM sessions s
|
|
87
|
+
WHERE (s.id = ? OR s.id LIKE ? OR s.short_id = ?)
|
|
88
|
+
AND s.archived_at IS NULL
|
|
89
|
+
LIMIT ?`);
|
|
90
|
+
const prefix = options.idPrefix;
|
|
91
|
+
const rows = stmt.all(prefix, `${prefix}%`, prefix, limit);
|
|
92
|
+
return rows.map(rowFrom);
|
|
93
|
+
}
|
|
94
|
+
if (options.query && options.query.trim()) {
|
|
95
|
+
const { expr } = buildFtsQuery(options.query);
|
|
96
|
+
if (!expr)
|
|
97
|
+
return [];
|
|
98
|
+
const stmt = db.prepare(`SELECT ${SELECT_COLS}
|
|
99
|
+
FROM sessions s
|
|
100
|
+
JOIN session_text t ON t.session_id = s.id
|
|
101
|
+
WHERE session_text MATCH ?
|
|
102
|
+
AND s.archived_at IS NULL
|
|
103
|
+
${options.agent ? 'AND s.agent = ?' : ''}
|
|
104
|
+
ORDER BY rank
|
|
105
|
+
LIMIT ?`);
|
|
106
|
+
const bind = options.agent ? [expr, options.agent, limit] : [expr, limit];
|
|
107
|
+
const rows = stmt.all(...bind);
|
|
108
|
+
return rows.map(rowFrom);
|
|
109
|
+
}
|
|
110
|
+
const stmt = db.prepare(`SELECT ${SELECT_COLS} FROM sessions s
|
|
111
|
+
WHERE s.archived_at IS NULL
|
|
112
|
+
${options.agent ? 'AND s.agent = ?' : ''}
|
|
113
|
+
ORDER BY s.last_activity DESC, s.timestamp DESC
|
|
114
|
+
LIMIT ?`);
|
|
115
|
+
const bind = options.agent ? [options.agent, limit] : [limit];
|
|
116
|
+
const rows = stmt.all(...bind);
|
|
117
|
+
return rows.map(rowFrom);
|
|
118
|
+
}
|
|
119
|
+
finally {
|
|
120
|
+
db.close();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
export function looksLikeId(value) {
|
|
124
|
+
return /^[0-9a-f]{8,}$/i.test(value) || /^[0-9a-f-]{8,}$/i.test(value);
|
|
125
|
+
}
|
|
126
|
+
export function parseQueryArgs(argv) {
|
|
127
|
+
const options = {};
|
|
128
|
+
const rest = [];
|
|
129
|
+
for (let i = 0; i < argv.length; i++) {
|
|
130
|
+
const arg = argv[i];
|
|
131
|
+
if (arg === '--json' || arg === '--local' || arg === '--no-interactive')
|
|
132
|
+
continue;
|
|
133
|
+
if (arg === '--limit' && argv[i + 1]) {
|
|
134
|
+
options.limit = Number(argv[++i]);
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (arg.startsWith('--limit=')) {
|
|
138
|
+
options.limit = Number(arg.slice('--limit='.length));
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (arg === '--agent' && argv[i + 1]) {
|
|
142
|
+
options.agent = argv[++i];
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (arg.startsWith('--'))
|
|
146
|
+
continue;
|
|
147
|
+
rest.push(arg);
|
|
148
|
+
}
|
|
149
|
+
if (rest.length === 1 && looksLikeId(rest[0]) && !rest[0].includes(' ')) {
|
|
150
|
+
options.idPrefix = rest[0];
|
|
151
|
+
}
|
|
152
|
+
else if (rest.length > 0) {
|
|
153
|
+
options.query = rest.join(' ');
|
|
154
|
+
}
|
|
155
|
+
return options;
|
|
156
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function getCliVersion(pkgJsonPath?: string): string;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
5
|
+
const PKG_JSON = path.join(__dirname, '..', '..', 'package.json');
|
|
6
|
+
let cached = null;
|
|
7
|
+
export function getCliVersion(pkgJsonPath = PKG_JSON) {
|
|
8
|
+
if (cached)
|
|
9
|
+
return cached;
|
|
10
|
+
try {
|
|
11
|
+
const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'));
|
|
12
|
+
cached = String(pkg.version || '') || 'unknown';
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
cached = 'unknown';
|
|
16
|
+
}
|
|
17
|
+
return cached;
|
|
18
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import { PROTOCOL_VERSION } from './protocol.js';
|
|
3
|
+
import { querySessions } from './lib/query.js';
|
|
4
|
+
/** Cap the __serve request body. Local caller, but unbounded read is a DoS. */
|
|
5
|
+
export const MAX_REQUEST_BYTES = 1024 * 1024;
|
|
6
|
+
export function readFd(fd, maxBytes = MAX_REQUEST_BYTES) {
|
|
7
|
+
const chunks = [];
|
|
8
|
+
let total = 0;
|
|
9
|
+
for (;;) {
|
|
10
|
+
const buf = Buffer.alloc(64 * 1024);
|
|
11
|
+
const n = fs.readSync(fd, buf, 0, buf.length, null);
|
|
12
|
+
if (n === 0)
|
|
13
|
+
break;
|
|
14
|
+
total += n;
|
|
15
|
+
if (total > maxBytes) {
|
|
16
|
+
throw new Error(`request exceeds ${maxBytes} bytes`);
|
|
17
|
+
}
|
|
18
|
+
chunks.push(buf.subarray(0, n));
|
|
19
|
+
}
|
|
20
|
+
return Buffer.concat(chunks);
|
|
21
|
+
}
|
|
22
|
+
export function runProtocolServer() {
|
|
23
|
+
let raw;
|
|
24
|
+
try {
|
|
25
|
+
raw = readFd(3);
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
process.stderr.write(`sessions __serve: failed to read fd 3: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
let request;
|
|
33
|
+
try {
|
|
34
|
+
request = JSON.parse(raw.toString('utf8'));
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
writeResponse({
|
|
38
|
+
v: PROTOCOL_VERSION,
|
|
39
|
+
id: '',
|
|
40
|
+
ok: false,
|
|
41
|
+
error: { code: 'INVALID_RESPONSE', message: 'request is not JSON' },
|
|
42
|
+
});
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
const rows = querySessions({
|
|
47
|
+
query: request.op === 'get' ? undefined : request.args?.query,
|
|
48
|
+
idPrefix: request.op === 'get' ? request.args?.idPrefix : request.args?.idPrefix,
|
|
49
|
+
limit: request.args?.limit,
|
|
50
|
+
agent: request.args?.agent,
|
|
51
|
+
});
|
|
52
|
+
writeResponse({ v: PROTOCOL_VERSION, id: request.id, ok: true, result: rows });
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
writeResponse({
|
|
56
|
+
v: PROTOCOL_VERSION,
|
|
57
|
+
id: request.id,
|
|
58
|
+
ok: false,
|
|
59
|
+
error: {
|
|
60
|
+
code: 'OPERATION_FAILED',
|
|
61
|
+
message: error instanceof Error ? error.message : String(error),
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function writeResponse(response) {
|
|
67
|
+
const body = Buffer.from(JSON.stringify(response), 'utf8');
|
|
68
|
+
fs.writeSync(4, body);
|
|
69
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export declare const PROTOCOL_VERSION = 1;
|
|
2
|
+
export type SessionsOp = 'query' | 'list' | 'get';
|
|
3
|
+
export interface SessionsRequest {
|
|
4
|
+
v: number;
|
|
5
|
+
id: string;
|
|
6
|
+
op: SessionsOp;
|
|
7
|
+
args: {
|
|
8
|
+
query?: string;
|
|
9
|
+
limit?: number;
|
|
10
|
+
idPrefix?: string;
|
|
11
|
+
agent?: string;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export interface SessionsResponse {
|
|
15
|
+
v: number;
|
|
16
|
+
id: string;
|
|
17
|
+
ok: boolean;
|
|
18
|
+
result?: unknown;
|
|
19
|
+
error?: {
|
|
20
|
+
code: string;
|
|
21
|
+
message: string;
|
|
22
|
+
};
|
|
23
|
+
}
|
package/dist/protocol.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const PROTOCOL_VERSION = 1;
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@phnx-labs/sessions-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "High-performance session index: search, list, and preview agent transcripts",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"sessions": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist/**/*.js",
|
|
11
|
+
"dist/**/*.d.ts",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE",
|
|
14
|
+
"CHANGELOG.md"
|
|
15
|
+
],
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=22.5.0"
|
|
18
|
+
},
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/phnx-labs/sessions-cli.git"
|
|
22
|
+
},
|
|
23
|
+
"license": "FSL-1.1-Apache-2.0",
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "bash scripts/build.sh",
|
|
26
|
+
"test": "bash scripts/test.sh",
|
|
27
|
+
"scan": "bash scripts/scan.sh",
|
|
28
|
+
"release": "bash scripts/release.sh"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"commander": "15.0.0",
|
|
32
|
+
"chalk": "5.6.2"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/node": "26.1.0",
|
|
36
|
+
"tsx": "4.21.0",
|
|
37
|
+
"typescript": "6.0.3",
|
|
38
|
+
"vitest": "4.1.9"
|
|
39
|
+
}
|
|
40
|
+
}
|