dsh-reference-anything 0.3.0 → 0.3.2
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/LICENSE +22 -22
- package/NOTICE.md +52 -25
- package/README.md +339 -286
- package/cordis.patch.yml +89 -18
- package/lib/client.js +13538 -591
- package/lib/contract.d.ts +155 -16
- package/lib/contract.js +68 -7
- package/lib/download-directory.d.ts +18 -0
- package/lib/download-directory.js +55 -0
- package/lib/errors.d.ts +4 -0
- package/lib/host.d.ts +96 -8
- package/lib/host.js +88 -19
- package/lib/index.d.ts +22 -2
- package/lib/index.js +57 -7
- package/lib/opencli.d.ts +5 -3
- package/lib/opencli.js +13 -6
- package/lib/openlist/host.d.ts +157 -0
- package/lib/openlist/host.js +873 -0
- package/lib/openlist/index.d.ts +84 -0
- package/lib/openlist/index.js +507 -0
- package/lib/render.d.ts +12 -7
- package/lib/render.js +27 -21
- package/lib/sources/cloud-drive/cache.d.ts +60 -0
- package/lib/sources/cloud-drive/cache.js +107 -0
- package/lib/sources/cloud-drive/index.d.ts +190 -0
- package/lib/sources/cloud-drive/index.js +526 -0
- package/lib/sources/cloud-drive/providers/http.d.ts +27 -0
- package/lib/sources/cloud-drive/providers/http.js +73 -0
- package/lib/sources/cloud-drive/providers/openlist.d.ts +59 -0
- package/lib/sources/cloud-drive/providers/openlist.js +535 -0
- package/lib/sources/cloud-drive/registry.d.ts +53 -0
- package/lib/sources/cloud-drive/registry.js +101 -0
- package/lib/sources/cloud-drive/types.d.ts +169 -0
- package/lib/sources/cloud-drive/types.js +17 -0
- package/lib/sources/file.d.ts +2 -0
- package/lib/sources/file.js +2 -0
- package/lib/sources/local-agent/adapters/claude-code.d.ts +16 -0
- package/lib/sources/local-agent/adapters/claude-code.js +171 -0
- package/lib/sources/local-agent/adapters/codex.d.ts +20 -0
- package/lib/sources/local-agent/adapters/codex.js +204 -0
- package/lib/sources/local-agent/adapters/cursor.d.ts +23 -0
- package/lib/sources/local-agent/adapters/cursor.js +121 -0
- package/lib/sources/local-agent/adapters/gemini-cli.d.ts +20 -0
- package/lib/sources/local-agent/adapters/gemini-cli.js +141 -0
- package/lib/sources/local-agent/adapters/grokbuild.d.ts +22 -0
- package/lib/sources/local-agent/adapters/grokbuild.js +76 -0
- package/lib/sources/local-agent/adapters/hermes.d.ts +19 -0
- package/lib/sources/local-agent/adapters/hermes.js +85 -0
- package/lib/sources/local-agent/adapters/kimi.d.ts +28 -0
- package/lib/sources/local-agent/adapters/kimi.js +261 -0
- package/lib/sources/local-agent/adapters/openclaw.d.ts +21 -0
- package/lib/sources/local-agent/adapters/openclaw.js +120 -0
- package/lib/sources/local-agent/adapters/opencode.d.ts +44 -0
- package/lib/sources/local-agent/adapters/opencode.js +236 -0
- package/lib/sources/local-agent/adapters/pi.d.ts +26 -0
- package/lib/sources/local-agent/adapters/pi.js +242 -0
- package/lib/sources/local-agent/adapters/qoder.d.ts +19 -0
- package/lib/sources/local-agent/adapters/qoder.js +93 -0
- package/lib/sources/local-agent/adapters/reasonix.d.ts +23 -0
- package/lib/sources/local-agent/adapters/reasonix.js +90 -0
- package/lib/sources/local-agent/adapters/shared.d.ts +163 -0
- package/lib/sources/local-agent/adapters/shared.js +415 -0
- package/lib/sources/local-agent/adapters/sqlite-shared.d.ts +92 -0
- package/lib/sources/local-agent/adapters/sqlite-shared.js +215 -0
- package/lib/sources/local-agent/adapters/zcode.d.ts +41 -0
- package/lib/sources/local-agent/adapters/zcode.js +159 -0
- package/lib/sources/local-agent/index.d.ts +222 -0
- package/lib/sources/local-agent/index.js +656 -0
- package/lib/sources/local-agent/page.d.ts +55 -0
- package/lib/sources/local-agent/page.js +83 -0
- package/lib/sources/local-agent/path.d.ts +8 -0
- package/lib/sources/local-agent/path.js +43 -0
- package/lib/sources/local-agent/registry.d.ts +68 -0
- package/lib/sources/local-agent/registry.js +116 -0
- package/lib/sources/local-agent/scan.d.ts +175 -0
- package/lib/sources/local-agent/scan.js +472 -0
- package/lib/sources/local-agent/sqlite.d.ts +45 -0
- package/lib/sources/local-agent/sqlite.js +113 -0
- package/lib/sources/local-agent/store.d.ts +93 -0
- package/lib/sources/local-agent/store.js +113 -0
- package/lib/sources/local-agent/types.d.ts +338 -0
- package/lib/sources/local-agent/types.js +20 -0
- package/lib/sources/web-chat/index.d.ts +5 -1
- package/lib/sources/web-chat/index.js +47 -8
- package/lib/store/spec.d.ts +50 -4
- package/lib/store/spec.js +2 -2
- package/lib/sync/index.d.ts +1 -0
- package/lib/sync/index.js +28 -8
- package/lib/tool.d.ts +5 -0
- package/lib/tool.js +418 -45
- package/lib/typert.js +13 -2
- package/lib/types.d.ts +28 -2
- package/lib/update.d.ts +6 -0
- package/lib/update.js +29 -1
- package/lib/web.js +5 -1
- package/lib/wire.d.ts +80 -9
- package/lib/wire.js +30 -4
- package/opencli-plugin/chatgpt.js +30 -27
- package/opencli-plugin/claude.js +12 -8
- package/opencli-plugin/common.js +266 -187
- package/opencli-plugin/deepseek.js +8 -5
- package/opencli-plugin/gemini.js +42 -38
- package/opencli-plugin/grok.js +22 -21
- package/opencli-plugin/kimi.js +34 -31
- package/opencli-plugin/opencli-plugin.json +6 -6
- package/opencli-plugin/package.json +8 -8
- package/package.json +182 -167
- package/lib/profile-mode.d.ts +0 -30
- package/lib/profile-mode.js +0 -90
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Continuation tokens for paging backwards through a transcript.
|
|
3
|
+
*
|
|
4
|
+
* A cursor is opaque to callers but not to this file, and it carries the one
|
|
5
|
+
* thing a stateless read needs: where the previous page began. Which "where"
|
|
6
|
+
* that is depends on the branch the read took — an exact turn index when the
|
|
7
|
+
* whole file was streamed, a byte offset when only its tail was reachable — so
|
|
8
|
+
* the two are separate members rather than one nullable field.
|
|
9
|
+
*
|
|
10
|
+
* @module dsh-reference-anything/local-agent/page
|
|
11
|
+
*/
|
|
12
|
+
import { ReferenceAnythingError } from '../../errors.ts';
|
|
13
|
+
/** Where the previous page started, and what it started in. */
|
|
14
|
+
export type AgentCursor = {
|
|
15
|
+
readonly kind: 'index';
|
|
16
|
+
readonly size: number;
|
|
17
|
+
readonly index: number;
|
|
18
|
+
} | {
|
|
19
|
+
readonly kind: 'offset';
|
|
20
|
+
readonly size: number;
|
|
21
|
+
readonly offset: number;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Encode a continuation token.
|
|
25
|
+
* @param ref - the reference id this token belongs to; a token is not portable.
|
|
26
|
+
* @param cursor - where the next page should start.
|
|
27
|
+
* @returns an opaque base64url token.
|
|
28
|
+
*/
|
|
29
|
+
export declare function encodeAgentCursor(ref: string, cursor: AgentCursor): string;
|
|
30
|
+
/**
|
|
31
|
+
* Decode a continuation token minted for this reference.
|
|
32
|
+
*
|
|
33
|
+
* Rejects a token from another reference as firmly as a malformed one: a
|
|
34
|
+
* cursor that silently paged through a different transcript would hand the
|
|
35
|
+
* model somebody else's conversation under the label it asked for.
|
|
36
|
+
* @param value - the token as the caller supplied it.
|
|
37
|
+
* @param expectedRef - the reference id it must belong to.
|
|
38
|
+
* @returns where the next page starts.
|
|
39
|
+
*/
|
|
40
|
+
export declare function decodeAgentCursor(value: string, expectedRef: string): AgentCursor;
|
|
41
|
+
/**
|
|
42
|
+
* Decide whether a cursor still describes the file in front of us.
|
|
43
|
+
*
|
|
44
|
+
* Deliberately not revision equality. A live agent appends to its transcript
|
|
45
|
+
* continuously, and under pure append both cursor kinds stay true: a byte
|
|
46
|
+
* offset still points at the same record and a turn index still names the same
|
|
47
|
+
* turn. Only shrinkage — a rotated, truncated, or replaced file — moves the
|
|
48
|
+
* ground under a token.
|
|
49
|
+
* @param cursor - the decoded token.
|
|
50
|
+
* @param currentSize - the transcript's size right now, in bytes.
|
|
51
|
+
* @returns whether the cursor may still be used.
|
|
52
|
+
*/
|
|
53
|
+
export declare function cursorStillValid(cursor: AgentCursor, currentSize: number): boolean;
|
|
54
|
+
/** Raise the expiry error, so callers do not each phrase it differently. */
|
|
55
|
+
export declare function cursorExpired(): ReferenceAnythingError;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Continuation tokens for paging backwards through a transcript.
|
|
3
|
+
*
|
|
4
|
+
* A cursor is opaque to callers but not to this file, and it carries the one
|
|
5
|
+
* thing a stateless read needs: where the previous page began. Which "where"
|
|
6
|
+
* that is depends on the branch the read took — an exact turn index when the
|
|
7
|
+
* whole file was streamed, a byte offset when only its tail was reachable — so
|
|
8
|
+
* the two are separate members rather than one nullable field.
|
|
9
|
+
*
|
|
10
|
+
* @module dsh-reference-anything/local-agent/page
|
|
11
|
+
*/
|
|
12
|
+
import { ReferenceAnythingError } from "../../errors.js";
|
|
13
|
+
/** Format version, so a token minted by an older build is rejected cleanly. */
|
|
14
|
+
const VERSION = 1;
|
|
15
|
+
/**
|
|
16
|
+
* Encode a continuation token.
|
|
17
|
+
* @param ref - the reference id this token belongs to; a token is not portable.
|
|
18
|
+
* @param cursor - where the next page should start.
|
|
19
|
+
* @returns an opaque base64url token.
|
|
20
|
+
*/
|
|
21
|
+
export function encodeAgentCursor(ref, cursor) {
|
|
22
|
+
const body = cursor.kind === 'index'
|
|
23
|
+
? { v: VERSION, ref, size: cursor.size, index: cursor.index }
|
|
24
|
+
: { v: VERSION, ref, size: cursor.size, offset: cursor.offset };
|
|
25
|
+
return Buffer.from(JSON.stringify(body)).toString('base64url');
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Decode a continuation token minted for this reference.
|
|
29
|
+
*
|
|
30
|
+
* Rejects a token from another reference as firmly as a malformed one: a
|
|
31
|
+
* cursor that silently paged through a different transcript would hand the
|
|
32
|
+
* model somebody else's conversation under the label it asked for.
|
|
33
|
+
* @param value - the token as the caller supplied it.
|
|
34
|
+
* @param expectedRef - the reference id it must belong to.
|
|
35
|
+
* @returns where the next page starts.
|
|
36
|
+
*/
|
|
37
|
+
export function decodeAgentCursor(value, expectedRef) {
|
|
38
|
+
let row;
|
|
39
|
+
try {
|
|
40
|
+
const parsed = JSON.parse(Buffer.from(value, 'base64url').toString('utf8'));
|
|
41
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed))
|
|
42
|
+
throw new Error('not an object');
|
|
43
|
+
row = parsed;
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
throw malformed();
|
|
47
|
+
}
|
|
48
|
+
if (row['v'] !== VERSION || row['ref'] !== expectedRef)
|
|
49
|
+
throw malformed();
|
|
50
|
+
const size = row['size'];
|
|
51
|
+
if (!isCount(size))
|
|
52
|
+
throw malformed();
|
|
53
|
+
if (isCount(row['index']))
|
|
54
|
+
return { kind: 'index', size, index: row['index'] };
|
|
55
|
+
if (isCount(row['offset']))
|
|
56
|
+
return { kind: 'offset', size, offset: row['offset'] };
|
|
57
|
+
throw malformed();
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Decide whether a cursor still describes the file in front of us.
|
|
61
|
+
*
|
|
62
|
+
* Deliberately not revision equality. A live agent appends to its transcript
|
|
63
|
+
* continuously, and under pure append both cursor kinds stay true: a byte
|
|
64
|
+
* offset still points at the same record and a turn index still names the same
|
|
65
|
+
* turn. Only shrinkage — a rotated, truncated, or replaced file — moves the
|
|
66
|
+
* ground under a token.
|
|
67
|
+
* @param cursor - the decoded token.
|
|
68
|
+
* @param currentSize - the transcript's size right now, in bytes.
|
|
69
|
+
* @returns whether the cursor may still be used.
|
|
70
|
+
*/
|
|
71
|
+
export function cursorStillValid(cursor, currentSize) {
|
|
72
|
+
return currentSize >= cursor.size;
|
|
73
|
+
}
|
|
74
|
+
/** Raise the expiry error, so callers do not each phrase it differently. */
|
|
75
|
+
export function cursorExpired() {
|
|
76
|
+
return new ReferenceAnythingError('the transcript was truncated or replaced since this page was read, so the continuation no longer points at the same turns', 'REFERENCE_CURSOR_EXPIRED');
|
|
77
|
+
}
|
|
78
|
+
function isCount(value) {
|
|
79
|
+
return typeof value === 'number' && Number.isInteger(value) && value >= 0;
|
|
80
|
+
}
|
|
81
|
+
function malformed() {
|
|
82
|
+
return new ReferenceAnythingError('transcript continuation token is malformed or belongs to another transcript', 'REFERENCE_INVALID_CURSOR');
|
|
83
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Join path segments without changing an explicitly supplied path dialect. */
|
|
2
|
+
export declare function joinLocalPath(base: string, ...segments: string[]): string;
|
|
3
|
+
/** Normalize a path without changing an explicitly supplied path dialect. */
|
|
4
|
+
export declare function normalizeLocalPath(value: string): string;
|
|
5
|
+
/** Whether a path is absolute according to its explicit (or native) dialect. */
|
|
6
|
+
export declare function isAbsoluteLocalPath(value: string): boolean;
|
|
7
|
+
/** Whether a recorded working directory is the workspace or one of its descendants. */
|
|
8
|
+
export declare function inWorkspace(cwd: string, workspace: string): boolean;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import path, { posix, win32 } from 'node:path';
|
|
2
|
+
function explicitDialect(value) {
|
|
3
|
+
if (value.startsWith('/'))
|
|
4
|
+
return 'posix';
|
|
5
|
+
if (/^[A-Za-z]:[\\/]/.test(value) || value.startsWith('\\\\') || value.includes('\\'))
|
|
6
|
+
return 'win32';
|
|
7
|
+
return 'native';
|
|
8
|
+
}
|
|
9
|
+
function implementation(dialect) {
|
|
10
|
+
if (dialect === 'posix')
|
|
11
|
+
return posix;
|
|
12
|
+
if (dialect === 'win32')
|
|
13
|
+
return win32;
|
|
14
|
+
return path;
|
|
15
|
+
}
|
|
16
|
+
/** Join path segments without changing an explicitly supplied path dialect. */
|
|
17
|
+
export function joinLocalPath(base, ...segments) {
|
|
18
|
+
return implementation(explicitDialect(base)).join(base, ...segments);
|
|
19
|
+
}
|
|
20
|
+
/** Normalize a path without changing an explicitly supplied path dialect. */
|
|
21
|
+
export function normalizeLocalPath(value) {
|
|
22
|
+
return implementation(explicitDialect(value)).normalize(value);
|
|
23
|
+
}
|
|
24
|
+
/** Whether a path is absolute according to its explicit (or native) dialect. */
|
|
25
|
+
export function isAbsoluteLocalPath(value) {
|
|
26
|
+
return implementation(explicitDialect(value)).isAbsolute(value);
|
|
27
|
+
}
|
|
28
|
+
/** Whether a recorded working directory is the workspace or one of its descendants. */
|
|
29
|
+
export function inWorkspace(cwd, workspace) {
|
|
30
|
+
if (cwd === '' || workspace === '')
|
|
31
|
+
return false;
|
|
32
|
+
const cwdDialect = explicitDialect(cwd);
|
|
33
|
+
const workspaceDialect = explicitDialect(workspace);
|
|
34
|
+
if (cwdDialect !== 'native' && workspaceDialect !== 'native' && cwdDialect !== workspaceDialect)
|
|
35
|
+
return false;
|
|
36
|
+
const dialect = cwdDialect !== 'native' ? cwdDialect : workspaceDialect;
|
|
37
|
+
const paths = implementation(dialect);
|
|
38
|
+
const normalizedCwd = paths.resolve(cwd);
|
|
39
|
+
const normalizedWorkspace = paths.resolve(workspace);
|
|
40
|
+
const relative = paths.relative(normalizedWorkspace, normalizedCwd);
|
|
41
|
+
return relative === ''
|
|
42
|
+
|| (!paths.isAbsolute(relative) && relative !== '..' && !relative.startsWith(`..${paths.sep}`));
|
|
43
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which transcript formats this package can read, and where they live.
|
|
3
|
+
*
|
|
4
|
+
* One place to add a format: an adapter registered here becomes visible to
|
|
5
|
+
* discovery, to reading, and to the `@` menu without any other file learning
|
|
6
|
+
* its name.
|
|
7
|
+
*
|
|
8
|
+
* @module dsh-reference-anything/local-agent/registry
|
|
9
|
+
*/
|
|
10
|
+
import type { AgentAdapter, AgentKind, QueryAdapter, TranscriptAdapter } from './types.ts';
|
|
11
|
+
/**
|
|
12
|
+
* Every supported adapter, in menu order.
|
|
13
|
+
*
|
|
14
|
+
* Ordered by how well the format is validated rather than alphabetically. The
|
|
15
|
+
* first two were built against real corpora on the machine this was written
|
|
16
|
+
* on — 541 Claude Code transcripts and 212 Codex rollouts — and the rest were
|
|
17
|
+
* ported from format documentation and exercised only against the fixtures in
|
|
18
|
+
* `tests/local-agent-converters.spec.ts`. That distinction is real enough to
|
|
19
|
+
* belong in the README, and the order here is what it looks like in code.
|
|
20
|
+
*
|
|
21
|
+
* The last three are database-backed and sit at the end for a second reason:
|
|
22
|
+
* they are the only ones a build without `node:sqlite` cannot read at all.
|
|
23
|
+
*/
|
|
24
|
+
export declare const AGENT_ADAPTERS: readonly AgentAdapter[];
|
|
25
|
+
/**
|
|
26
|
+
* The formats read by walking a file, which is all but the databases.
|
|
27
|
+
*
|
|
28
|
+
* Kept as a narrowed array rather than filtered at each use so the discovery
|
|
29
|
+
* walk and the read path agree on which seam an adapter answers to.
|
|
30
|
+
*/
|
|
31
|
+
export declare const FILE_ADAPTERS: readonly TranscriptAdapter[];
|
|
32
|
+
/** The formats read by querying a database. */
|
|
33
|
+
export declare const QUERY_ADAPTERS: readonly QueryAdapter[];
|
|
34
|
+
/** Whether a format is read by querying a database rather than by walking a file. */
|
|
35
|
+
export declare function isQueryKind(kind: string): boolean;
|
|
36
|
+
/** Every supported {@link AgentKind}, in the same order. */
|
|
37
|
+
export declare const AGENT_KINDS: readonly AgentKind[];
|
|
38
|
+
/**
|
|
39
|
+
* The format an extra root is assumed to hold when its entry does not say.
|
|
40
|
+
*
|
|
41
|
+
* Named rather than indexed off {@link AGENT_KINDS} so the choice reads as a
|
|
42
|
+
* choice — the best-validated format is the safe thing to guess.
|
|
43
|
+
*/
|
|
44
|
+
export declare const DEFAULT_AGENT_KIND: AgentKind;
|
|
45
|
+
/**
|
|
46
|
+
* Find the adapter for a format.
|
|
47
|
+
* @param kind - a format name, which may have come from config or from a reference id.
|
|
48
|
+
* @returns the adapter, or undefined when nothing reads that format.
|
|
49
|
+
*/
|
|
50
|
+
export declare function adapterFor(kind: string): AgentAdapter | undefined;
|
|
51
|
+
/**
|
|
52
|
+
* Whether a string names a supported format.
|
|
53
|
+
* @param kind - the candidate name.
|
|
54
|
+
* @returns whether {@link adapterFor} would resolve it.
|
|
55
|
+
*/
|
|
56
|
+
export declare function isAgentKind(kind: string): kind is AgentKind;
|
|
57
|
+
/**
|
|
58
|
+
* Resolve a configured root to an absolute path.
|
|
59
|
+
*
|
|
60
|
+
* Roots are written as portable `~/`-prefixed strings rather than absolute
|
|
61
|
+
* ones so a profile can be shared between machines and users. Expansion is
|
|
62
|
+
* plain string work on purpose: an external plugin overlay is not permitted to
|
|
63
|
+
* evaluate code, so a root must never need any.
|
|
64
|
+
* @param root - a configured root, `~`-relative or absolute.
|
|
65
|
+
* @param home - the user's home directory.
|
|
66
|
+
* @returns the absolute path, or undefined when the root is empty or relative.
|
|
67
|
+
*/
|
|
68
|
+
export declare function expandRoot(root: string, home: string): string | undefined;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which transcript formats this package can read, and where they live.
|
|
3
|
+
*
|
|
4
|
+
* One place to add a format: an adapter registered here becomes visible to
|
|
5
|
+
* discovery, to reading, and to the `@` menu without any other file learning
|
|
6
|
+
* its name.
|
|
7
|
+
*
|
|
8
|
+
* @module dsh-reference-anything/local-agent/registry
|
|
9
|
+
*/
|
|
10
|
+
import { claudeCodeAdapter } from "./adapters/claude-code.js";
|
|
11
|
+
import { codexAdapter } from "./adapters/codex.js";
|
|
12
|
+
import { cursorAdapter } from "./adapters/cursor.js";
|
|
13
|
+
import { geminiCliAdapter } from "./adapters/gemini-cli.js";
|
|
14
|
+
import { grokbuildAdapter } from "./adapters/grokbuild.js";
|
|
15
|
+
import { hermesAdapter } from "./adapters/hermes.js";
|
|
16
|
+
import { kimiAdapter } from "./adapters/kimi.js";
|
|
17
|
+
import { mimocodeAdapter, opencodeAdapter } from "./adapters/opencode.js";
|
|
18
|
+
import { openclawAdapter } from "./adapters/openclaw.js";
|
|
19
|
+
import { piAdapter } from "./adapters/pi.js";
|
|
20
|
+
import { qoderAdapter } from "./adapters/qoder.js";
|
|
21
|
+
import { reasonixAdapter } from "./adapters/reasonix.js";
|
|
22
|
+
import { zcodeAdapter } from "./adapters/zcode.js";
|
|
23
|
+
import { isQueryAdapter } from "./types.js";
|
|
24
|
+
import { isAbsoluteLocalPath, joinLocalPath, normalizeLocalPath } from "./path.js";
|
|
25
|
+
/**
|
|
26
|
+
* Every supported adapter, in menu order.
|
|
27
|
+
*
|
|
28
|
+
* Ordered by how well the format is validated rather than alphabetically. The
|
|
29
|
+
* first two were built against real corpora on the machine this was written
|
|
30
|
+
* on — 541 Claude Code transcripts and 212 Codex rollouts — and the rest were
|
|
31
|
+
* ported from format documentation and exercised only against the fixtures in
|
|
32
|
+
* `tests/local-agent-converters.spec.ts`. That distinction is real enough to
|
|
33
|
+
* belong in the README, and the order here is what it looks like in code.
|
|
34
|
+
*
|
|
35
|
+
* The last three are database-backed and sit at the end for a second reason:
|
|
36
|
+
* they are the only ones a build without `node:sqlite` cannot read at all.
|
|
37
|
+
*/
|
|
38
|
+
export const AGENT_ADAPTERS = [
|
|
39
|
+
claudeCodeAdapter,
|
|
40
|
+
codexAdapter,
|
|
41
|
+
cursorAdapter,
|
|
42
|
+
qoderAdapter,
|
|
43
|
+
reasonixAdapter,
|
|
44
|
+
openclawAdapter,
|
|
45
|
+
kimiAdapter,
|
|
46
|
+
grokbuildAdapter,
|
|
47
|
+
hermesAdapter,
|
|
48
|
+
geminiCliAdapter,
|
|
49
|
+
piAdapter,
|
|
50
|
+
opencodeAdapter,
|
|
51
|
+
mimocodeAdapter,
|
|
52
|
+
zcodeAdapter,
|
|
53
|
+
];
|
|
54
|
+
/**
|
|
55
|
+
* The formats read by walking a file, which is all but the databases.
|
|
56
|
+
*
|
|
57
|
+
* Kept as a narrowed array rather than filtered at each use so the discovery
|
|
58
|
+
* walk and the read path agree on which seam an adapter answers to.
|
|
59
|
+
*/
|
|
60
|
+
export const FILE_ADAPTERS = AGENT_ADAPTERS.filter((adapter) => !isQueryAdapter(adapter));
|
|
61
|
+
/** The formats read by querying a database. */
|
|
62
|
+
export const QUERY_ADAPTERS = AGENT_ADAPTERS.filter(isQueryAdapter);
|
|
63
|
+
/** Whether a format is read by querying a database rather than by walking a file. */
|
|
64
|
+
export function isQueryKind(kind) {
|
|
65
|
+
const adapter = BY_KIND.get(kind);
|
|
66
|
+
return adapter !== undefined && isQueryAdapter(adapter);
|
|
67
|
+
}
|
|
68
|
+
/** Every supported {@link AgentKind}, in the same order. */
|
|
69
|
+
export const AGENT_KINDS = AGENT_ADAPTERS.map(adapter => adapter.kind);
|
|
70
|
+
/**
|
|
71
|
+
* The format an extra root is assumed to hold when its entry does not say.
|
|
72
|
+
*
|
|
73
|
+
* Named rather than indexed off {@link AGENT_KINDS} so the choice reads as a
|
|
74
|
+
* choice — the best-validated format is the safe thing to guess.
|
|
75
|
+
*/
|
|
76
|
+
export const DEFAULT_AGENT_KIND = claudeCodeAdapter.kind;
|
|
77
|
+
const BY_KIND = new Map(AGENT_ADAPTERS.map(adapter => [adapter.kind, adapter]));
|
|
78
|
+
/**
|
|
79
|
+
* Find the adapter for a format.
|
|
80
|
+
* @param kind - a format name, which may have come from config or from a reference id.
|
|
81
|
+
* @returns the adapter, or undefined when nothing reads that format.
|
|
82
|
+
*/
|
|
83
|
+
export function adapterFor(kind) {
|
|
84
|
+
return BY_KIND.get(kind);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Whether a string names a supported format.
|
|
88
|
+
* @param kind - the candidate name.
|
|
89
|
+
* @returns whether {@link adapterFor} would resolve it.
|
|
90
|
+
*/
|
|
91
|
+
export function isAgentKind(kind) {
|
|
92
|
+
return BY_KIND.has(kind);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Resolve a configured root to an absolute path.
|
|
96
|
+
*
|
|
97
|
+
* Roots are written as portable `~/`-prefixed strings rather than absolute
|
|
98
|
+
* ones so a profile can be shared between machines and users. Expansion is
|
|
99
|
+
* plain string work on purpose: an external plugin overlay is not permitted to
|
|
100
|
+
* evaluate code, so a root must never need any.
|
|
101
|
+
* @param root - a configured root, `~`-relative or absolute.
|
|
102
|
+
* @param home - the user's home directory.
|
|
103
|
+
* @returns the absolute path, or undefined when the root is empty or relative.
|
|
104
|
+
*/
|
|
105
|
+
export function expandRoot(root, home) {
|
|
106
|
+
const trimmed = root.trim();
|
|
107
|
+
if (trimmed === '')
|
|
108
|
+
return undefined;
|
|
109
|
+
if (trimmed === '~')
|
|
110
|
+
return home;
|
|
111
|
+
if (trimmed.startsWith('~/') || trimmed.startsWith('~\\'))
|
|
112
|
+
return normalizeLocalPath(joinLocalPath(home, trimmed.slice(2)));
|
|
113
|
+
// A relative root would resolve against whatever directory the harness
|
|
114
|
+
// happened to launch in, which is not a boundary anyone chose.
|
|
115
|
+
return isAbsoluteLocalPath(trimmed) ? normalizeLocalPath(trimmed) : undefined;
|
|
116
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filesystem access for local agent transcripts.
|
|
3
|
+
*
|
|
4
|
+
* Everything that touches the disk lives here, so the adapters stay pure and
|
|
5
|
+
* testable against inline fixtures. Two facts about the corpus shape every
|
|
6
|
+
* function below, both measured rather than assumed:
|
|
7
|
+
*
|
|
8
|
+
* - **Transcripts are large.** 541 Claude files totalling 734 MB and 212 Codex
|
|
9
|
+
* rollouts totalling 2.24 GB sit on the machine this was written against, the
|
|
10
|
+
* largest single rollout being 192 MB. Nothing here may read a whole file.
|
|
11
|
+
* - **Lines are large and wildly uneven.** In one 21 MB rollout: p50 = 812 B,
|
|
12
|
+
* p90 = 14 KB, p99 = 1.4 MB, max = 4.0 MB. A fixed 32 KiB probe window
|
|
13
|
+
* recovers exactly one complete line there — which is why probes escalate by
|
|
14
|
+
* *lines wanted* under a byte ceiling, and why the streaming pass budgets in
|
|
15
|
+
* bytes rather than lines.
|
|
16
|
+
*
|
|
17
|
+
* @module dsh-reference-anything/local-agent/scan
|
|
18
|
+
*/
|
|
19
|
+
import type { AgentAdapter, AgentKind, ConvertOptions, ParsedTurn, QueryAdapter, SessionTurns, TranscriptAdapter, TranscriptSession } from './types.ts';
|
|
20
|
+
/** One directory to search, and the agent whose transcripts live in it. */
|
|
21
|
+
export interface ScanRoot {
|
|
22
|
+
readonly kind: AgentKind;
|
|
23
|
+
/** Absolute path; already expanded from any `~/` prefix. */
|
|
24
|
+
readonly path: string;
|
|
25
|
+
}
|
|
26
|
+
/** One transcript found on disk, described without reading a byte of it. */
|
|
27
|
+
export interface TranscriptDescriptor {
|
|
28
|
+
readonly kind: AgentKind;
|
|
29
|
+
/** Absolute path, for reading. */
|
|
30
|
+
readonly path: string;
|
|
31
|
+
/**
|
|
32
|
+
* Path relative to its root — the stable half of the reference id.
|
|
33
|
+
*
|
|
34
|
+
* For a database-backed format this is `<database>#<session id>`, because one
|
|
35
|
+
* file holds many conversations and a reference has to name which. The `#`
|
|
36
|
+
* costs nothing elsewhere: it is not a path separator, so the id round-trip
|
|
37
|
+
* and the bookmark key are unchanged, and a real filename containing one
|
|
38
|
+
* still belongs to a file-backed format that never splits on it.
|
|
39
|
+
*/
|
|
40
|
+
readonly relPath: string;
|
|
41
|
+
readonly mtimeMs: number;
|
|
42
|
+
readonly size: number;
|
|
43
|
+
/**
|
|
44
|
+
* The session row, for a database-backed format only.
|
|
45
|
+
*
|
|
46
|
+
* A `session` row already carries the title, working directory, and times a
|
|
47
|
+
* probe would otherwise have to read a file to find, so a query descriptor
|
|
48
|
+
* arrives complete and never needs backfilling.
|
|
49
|
+
*/
|
|
50
|
+
readonly session?: TranscriptSession;
|
|
51
|
+
}
|
|
52
|
+
/** Separates a database from the session inside it, in a reference id. */
|
|
53
|
+
export declare const SESSION_SEPARATOR = "#";
|
|
54
|
+
/** How far a probe may read before giving up on a transcript's metadata. */
|
|
55
|
+
export interface ProbeLimits {
|
|
56
|
+
readonly headLines: number;
|
|
57
|
+
readonly tailLines: number;
|
|
58
|
+
readonly maxProbeBytes: number;
|
|
59
|
+
}
|
|
60
|
+
/** Complete records recovered from the two ends of a transcript. */
|
|
61
|
+
export interface TranscriptEnds {
|
|
62
|
+
readonly headLines: readonly string[];
|
|
63
|
+
readonly tailLines: readonly string[];
|
|
64
|
+
/**
|
|
65
|
+
* Whether `headLines` is the whole file rather than a window onto its start.
|
|
66
|
+
*
|
|
67
|
+
* Only true when the first probe window already reached EOF, which is what
|
|
68
|
+
* lets a caller conclude something from an *absence*: a transcript that folds
|
|
69
|
+
* to no turns is only known to hold no conversation if every one of its
|
|
70
|
+
* records was seen. Without this flag a short probe of a long file looks
|
|
71
|
+
* identical to a complete probe of an aborted one.
|
|
72
|
+
*/
|
|
73
|
+
readonly complete: boolean;
|
|
74
|
+
}
|
|
75
|
+
/** How much of a transcript one read may touch. */
|
|
76
|
+
export interface ReadLimits {
|
|
77
|
+
/** Total bytes a single read may consume before it falls back to the tail. */
|
|
78
|
+
readonly maxScanBytes: number;
|
|
79
|
+
/** Longest single line that will be materialized; longer ones are skipped. */
|
|
80
|
+
readonly maxLineBytes: number;
|
|
81
|
+
}
|
|
82
|
+
/** Which turns to materialize. */
|
|
83
|
+
export interface TurnRequest {
|
|
84
|
+
readonly limit: number;
|
|
85
|
+
/** Exclusive upper bound in turn indices; absent means the newest turns. */
|
|
86
|
+
readonly before?: number;
|
|
87
|
+
/** Tail-anchored paging: only bytes below this offset may be read. */
|
|
88
|
+
readonly beforeOffset?: number;
|
|
89
|
+
}
|
|
90
|
+
/** The turns one read produced, and where they sit in the transcript. */
|
|
91
|
+
export interface TurnPage {
|
|
92
|
+
readonly items: readonly ParsedTurn[];
|
|
93
|
+
/** Index of `items[0]`; always 0 when the read was tail-anchored. */
|
|
94
|
+
readonly startIndex: number;
|
|
95
|
+
/** Exact count, or undefined when the read never saw the beginning. */
|
|
96
|
+
readonly totalTurns?: number;
|
|
97
|
+
readonly hasOlder: boolean;
|
|
98
|
+
/** Whether the transcript records a compaction, so earlier turns are gone. */
|
|
99
|
+
readonly compacted: boolean;
|
|
100
|
+
/** Whether the read was tail-anchored and so could not see the beginning. */
|
|
101
|
+
readonly anchored: boolean;
|
|
102
|
+
/** Byte offset the first returned turn began at; only set when anchored. */
|
|
103
|
+
readonly startOffset?: number;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Find every transcript under the given roots.
|
|
107
|
+
*
|
|
108
|
+
* Reads directory entries and stats only — no file contents — because this runs
|
|
109
|
+
* behind a keystroke debounce. A root that does not exist is an ordinary state
|
|
110
|
+
* (that agent is not installed) and is skipped rather than raised.
|
|
111
|
+
* @param roots - directories to search, each tagged with its agent.
|
|
112
|
+
* @param adapters - adapters by kind, used to test filenames.
|
|
113
|
+
* @param maxTranscripts - most descriptors to return, newest first.
|
|
114
|
+
* @param signal - cancellation from the caller.
|
|
115
|
+
* @returns descriptors sorted newest-modified first, at most `maxTranscripts`.
|
|
116
|
+
*/
|
|
117
|
+
export declare function listTranscripts(roots: readonly ScanRoot[], adapters: ReadonlyMap<AgentKind, AgentAdapter>, maxTranscripts: number, signal?: AbortSignal): Promise<TranscriptDescriptor[]>;
|
|
118
|
+
/**
|
|
119
|
+
* Look up the row that describes one conversation.
|
|
120
|
+
*
|
|
121
|
+
* A read may arrive without a listing having preceded it — the model can hold a
|
|
122
|
+
* reference id across turns, and `locate()` resolves a path rather than
|
|
123
|
+
* replaying discovery — so this is what gives a database-backed snapshot its
|
|
124
|
+
* title, working directory, and times.
|
|
125
|
+
* @param path - absolute path to the database.
|
|
126
|
+
* @param adapter - the adapter that knows this schema.
|
|
127
|
+
* @param sessionId - which conversation to describe.
|
|
128
|
+
* @param signal - cancellation from the caller.
|
|
129
|
+
* @returns the session row, or undefined when the database or the row is unreadable.
|
|
130
|
+
*/
|
|
131
|
+
export declare function readSessionInfo(path: string, adapter: QueryAdapter, sessionId: string, signal?: AbortSignal): Promise<TranscriptSession | undefined>;
|
|
132
|
+
/**
|
|
133
|
+
* Read one conversation out of a database.
|
|
134
|
+
* @param path - absolute path to the database.
|
|
135
|
+
* @param adapter - the adapter that knows this schema.
|
|
136
|
+
* @param sessionId - which conversation to fold.
|
|
137
|
+
* @param options - projection settings.
|
|
138
|
+
* @param maxRecords - most message rows to materialize, counted from the end.
|
|
139
|
+
* @param signal - cancellation from the caller.
|
|
140
|
+
* @returns the folded turns, oldest first.
|
|
141
|
+
*/
|
|
142
|
+
export declare function readSessionTurns(path: string, adapter: QueryAdapter, sessionId: string, options: ConvertOptions, maxRecords: number, signal?: AbortSignal): Promise<SessionTurns>;
|
|
143
|
+
/**
|
|
144
|
+
* Recover complete records from both ends of a transcript.
|
|
145
|
+
*
|
|
146
|
+
* Escalates through {@link PROBE_STEPS} until enough complete lines are in hand
|
|
147
|
+
* or the byte ceiling is reached, because a fixed window cannot serve a corpus
|
|
148
|
+
* whose p99 line is 1.4 MB. Giving up returns whatever was recovered rather
|
|
149
|
+
* than throwing: a transcript with no readable metadata still lists, under a
|
|
150
|
+
* fallback label.
|
|
151
|
+
* @param path - absolute path to the transcript.
|
|
152
|
+
* @param size - its size in bytes, from a prior stat.
|
|
153
|
+
* @param limits - how many lines are wanted and how far to read for them.
|
|
154
|
+
* @param signal - cancellation from the caller.
|
|
155
|
+
* @returns the complete records found at each end.
|
|
156
|
+
*/
|
|
157
|
+
export declare function probeEnds(path: string, size: number, limits: ProbeLimits, signal?: AbortSignal): Promise<TranscriptEnds>;
|
|
158
|
+
/**
|
|
159
|
+
* Read one window of turns from a transcript.
|
|
160
|
+
*
|
|
161
|
+
* Takes one of two branches. Under `maxScanBytes` it streams the whole file
|
|
162
|
+
* once, so `startIndex` and `totalTurns` are exact. Over it — the 192 MB
|
|
163
|
+
* rollout — it walks backwards from the end far enough to fill the window,
|
|
164
|
+
* which is honest but cannot see the beginning: `totalTurns` is then undefined
|
|
165
|
+
* and the caller must report the snapshot as partial.
|
|
166
|
+
* @param path - absolute path to the transcript.
|
|
167
|
+
* @param size - its size in bytes, from a prior stat.
|
|
168
|
+
* @param adapter - the format's adapter.
|
|
169
|
+
* @param options - projection settings.
|
|
170
|
+
* @param request - which turns to return.
|
|
171
|
+
* @param limits - how much of the file this read may touch.
|
|
172
|
+
* @param signal - cancellation from the caller.
|
|
173
|
+
* @returns the turns and their position in the transcript.
|
|
174
|
+
*/
|
|
175
|
+
export declare function readTurns(path: string, size: number, adapter: TranscriptAdapter, options: ConvertOptions, request: TurnRequest, limits: ReadLimits, signal?: AbortSignal): Promise<TurnPage>;
|