@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 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
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env -S node --no-warnings=ExperimentalWarning
2
+ export {};
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,4 @@
1
+ /** Cap the __serve request body. Local caller, but unbounded read is a DoS. */
2
+ export declare const MAX_REQUEST_BYTES: number;
3
+ export declare function readFd(fd: number, maxBytes?: number): Buffer;
4
+ export declare function runProtocolServer(): void;
@@ -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
+ }
@@ -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
+ }