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,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex CLI rollouts: `~/.codex/sessions/<yyyy>/<mm>/<dd>/rollout-*.jsonl`.
|
|
3
|
+
*
|
|
4
|
+
* Each line is an envelope `{timestamp, ordinal?, type, payload}`. Measured
|
|
5
|
+
* across 170 rollouts, seven envelope types occur — `response_item`,
|
|
6
|
+
* `event_msg`, `session_meta`, `turn_context`, `world_state`, `compacted`, and
|
|
7
|
+
* `inter_agent_communication_metadata` — but only `response_item` carries
|
|
8
|
+
* conversation.
|
|
9
|
+
*
|
|
10
|
+
* `event_msg` is the trap. Its `agent_message` and `user_message` payloads look
|
|
11
|
+
* like the assistant and the user speaking, and they are: they restate what
|
|
12
|
+
* `response_item` already recorded. Counted per file, `event_msg.agent_message`
|
|
13
|
+
* matched `response_item.message` with `role: 'assistant'` one for one across
|
|
14
|
+
* every rollout that had any, so consuming both would double the transcript.
|
|
15
|
+
*
|
|
16
|
+
* @module dsh-reference-anything/local-agent/adapters/codex
|
|
17
|
+
*/
|
|
18
|
+
import { joinLocalPath } from "../path.js";
|
|
19
|
+
import { arrayField, cleanUserText, createSharedState, emitUser, flushAssistant, normalizeTitle, objectField, parseRecord, parseTimestamp, pushAssistant, renderToolCall, renderToolResult, stringField, } from "./shared.js";
|
|
20
|
+
/** Reads Codex CLI rollout logs. */
|
|
21
|
+
export const codexAdapter = {
|
|
22
|
+
kind: 'codex',
|
|
23
|
+
displayName: 'Codex',
|
|
24
|
+
defaultRoots(home) {
|
|
25
|
+
return [joinLocalPath(home, '.codex', 'sessions')];
|
|
26
|
+
},
|
|
27
|
+
matches(relativePath) {
|
|
28
|
+
return relativePath.toLowerCase().endsWith('.jsonl');
|
|
29
|
+
},
|
|
30
|
+
createState() {
|
|
31
|
+
return createSharedState();
|
|
32
|
+
},
|
|
33
|
+
step(line, state, options) {
|
|
34
|
+
const record = parseRecord(line);
|
|
35
|
+
if (record === undefined)
|
|
36
|
+
return [];
|
|
37
|
+
const envelope = record['type'];
|
|
38
|
+
if (envelope === 'compacted') {
|
|
39
|
+
// Codex's analogue of a compact boundary: `payload.replacement_history`
|
|
40
|
+
// stands in for everything before it, which is no longer in this file.
|
|
41
|
+
state.compacted = true;
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
if (envelope !== 'response_item')
|
|
45
|
+
return [];
|
|
46
|
+
const payload = objectField(record, 'payload');
|
|
47
|
+
if (payload === undefined)
|
|
48
|
+
return [];
|
|
49
|
+
switch (payload['type']) {
|
|
50
|
+
case 'message':
|
|
51
|
+
return stepMessage(payload, state, options);
|
|
52
|
+
case 'reasoning':
|
|
53
|
+
if (options.includeThinking)
|
|
54
|
+
pushAssistant(state, reasoningText(payload));
|
|
55
|
+
return [];
|
|
56
|
+
case 'function_call':
|
|
57
|
+
pushAssistant(state, renderToolCall(payload['name'], payload['arguments'], options.toolCalls, options.toolSummaryChars));
|
|
58
|
+
return [];
|
|
59
|
+
case 'custom_tool_call':
|
|
60
|
+
case 'local_shell_call':
|
|
61
|
+
pushAssistant(state, renderToolCall(payload['name'], payload['input'], options.toolCalls, options.toolSummaryChars));
|
|
62
|
+
return [];
|
|
63
|
+
case 'function_call_output':
|
|
64
|
+
case 'custom_tool_call_output':
|
|
65
|
+
pushAssistant(state, renderToolResult(payload['output'], options.toolResults, options.toolSummaryChars));
|
|
66
|
+
return [];
|
|
67
|
+
default:
|
|
68
|
+
// `agent_message` here is not
|
|
69
|
+
// the assistant answering the user — it carries `author` and
|
|
70
|
+
// `recipient` and is one agent dispatching work to another — so it is
|
|
71
|
+
// plumbing too.
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
flush(state, _options) {
|
|
76
|
+
return flushAssistant(state);
|
|
77
|
+
},
|
|
78
|
+
head(headLines, tailLines) {
|
|
79
|
+
let cwd;
|
|
80
|
+
let createdAt;
|
|
81
|
+
let firstPrompt;
|
|
82
|
+
let firstReply;
|
|
83
|
+
for (const line of headLines) {
|
|
84
|
+
const record = parseRecord(line);
|
|
85
|
+
if (record === undefined)
|
|
86
|
+
continue;
|
|
87
|
+
const envelope = record['type'];
|
|
88
|
+
const payload = objectField(record, 'payload');
|
|
89
|
+
if (envelope === 'session_meta' && payload !== undefined) {
|
|
90
|
+
cwd ??= stringField(payload, 'cwd');
|
|
91
|
+
createdAt ??= parseTimestamp(payload['timestamp'] ?? record['timestamp']);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (envelope === 'turn_context' && payload !== undefined) {
|
|
95
|
+
cwd ??= stringField(payload, 'cwd');
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (envelope !== 'response_item' || payload === undefined)
|
|
99
|
+
continue;
|
|
100
|
+
if (firstPrompt === undefined && payload['role'] === 'user')
|
|
101
|
+
firstPrompt = messageText(payload, true);
|
|
102
|
+
if (firstReply === undefined && payload['role'] === 'assistant')
|
|
103
|
+
firstReply = messageText(payload, true);
|
|
104
|
+
}
|
|
105
|
+
createdAt ??= parseTimestamp(parseRecord(headLines[0] ?? '')?.['timestamp']);
|
|
106
|
+
// A rollout carries no title of its own, so the opening prompt is the name.
|
|
107
|
+
// The tail can still supply one when the head's probe fell short of the
|
|
108
|
+
// first real prompt — Codex's harness preamble alone can run past a
|
|
109
|
+
// megabyte, and a rollout whose head is all preamble would otherwise show
|
|
110
|
+
// as its filename.
|
|
111
|
+
if (firstPrompt === undefined) {
|
|
112
|
+
for (const line of tailLines) {
|
|
113
|
+
const record = parseRecord(line);
|
|
114
|
+
const payload = objectField(record, 'payload');
|
|
115
|
+
if (record?.['type'] !== 'response_item' || payload?.['role'] !== 'user')
|
|
116
|
+
continue;
|
|
117
|
+
firstPrompt = messageText(payload, true) ?? firstPrompt;
|
|
118
|
+
if (firstPrompt !== undefined)
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
// A rollout a parent agent dispatched has no user text at all: its task
|
|
123
|
+
// arrived as an `agent_message` whose payload is encrypted, leaving the
|
|
124
|
+
// injected preamble as the only user-role record. Measured over 25 recent
|
|
125
|
+
// rollouts, 7 were of this kind. The assistant's opening line is then the
|
|
126
|
+
// only readable account of what the session was for, and naming the entry
|
|
127
|
+
// after it beats naming it after its filename.
|
|
128
|
+
const title = normalizeTitle(firstPrompt ?? firstReply ?? '');
|
|
129
|
+
return {
|
|
130
|
+
...title === '' ? {} : { title },
|
|
131
|
+
...cwd === undefined ? {} : { cwd },
|
|
132
|
+
...firstPrompt === undefined ? {} : { firstPrompt },
|
|
133
|
+
...createdAt === undefined ? {} : { createdAt },
|
|
134
|
+
};
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
/** Fold one `response_item.message`, whose role decides whether it is conversation at all. */
|
|
138
|
+
function stepMessage(payload, state, options) {
|
|
139
|
+
const role = payload['role'];
|
|
140
|
+
if (role === 'user') {
|
|
141
|
+
const text = messageText(payload, options.stripEnvironmentPreamble);
|
|
142
|
+
return text === undefined ? [] : emitUser(state, text);
|
|
143
|
+
}
|
|
144
|
+
if (role === 'assistant') {
|
|
145
|
+
pushAssistant(state, messageText(payload, false));
|
|
146
|
+
return [];
|
|
147
|
+
}
|
|
148
|
+
// `developer` is the largest role by count in a modern rollout — skills
|
|
149
|
+
// instructions, tool contracts, environment context. Projecting it would
|
|
150
|
+
// read as though the user had said those things.
|
|
151
|
+
return [];
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Flatten a message's content blocks to text.
|
|
155
|
+
* @param payload - a `message` payload.
|
|
156
|
+
* @param stripPreamble - drop the message entirely when it is a harness-injected block.
|
|
157
|
+
* @returns the text, or undefined when the message contributes nothing.
|
|
158
|
+
*/
|
|
159
|
+
function messageText(payload, stripPreamble) {
|
|
160
|
+
const blocks = arrayField(payload, 'content');
|
|
161
|
+
if (blocks === undefined) {
|
|
162
|
+
const direct = stringField(payload, 'text');
|
|
163
|
+
return direct === undefined ? undefined : normalizeBody(direct, stripPreamble);
|
|
164
|
+
}
|
|
165
|
+
const parts = [];
|
|
166
|
+
for (const block of blocks) {
|
|
167
|
+
if (typeof block !== 'object' || block === null)
|
|
168
|
+
continue;
|
|
169
|
+
const entry = block;
|
|
170
|
+
// Assistant blocks arrive as `output_text` and, on some builds, as
|
|
171
|
+
// `input_text`; both were present in the measured corpus.
|
|
172
|
+
if (entry['type'] === 'input_text' || entry['type'] === 'output_text' || entry['type'] === 'text') {
|
|
173
|
+
if (typeof entry['text'] === 'string')
|
|
174
|
+
parts.push(entry['text']);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return parts.length === 0 ? undefined : normalizeBody(parts.join('\n'), stripPreamble);
|
|
178
|
+
}
|
|
179
|
+
/** Apply the preamble rules and collapse an empty result to `undefined`. */
|
|
180
|
+
function normalizeBody(text, stripPreamble) {
|
|
181
|
+
const cleaned = stripPreamble ? cleanUserText(text) : text.trim();
|
|
182
|
+
return cleaned === '' ? undefined : cleaned;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* The visible part of a `reasoning` payload.
|
|
186
|
+
*
|
|
187
|
+
* Codex encrypts its reasoning and leaves `summary` empty in every rollout
|
|
188
|
+
* measured here, so this usually yields nothing — which is why
|
|
189
|
+
* `includeThinking` costs a Codex read almost nothing.
|
|
190
|
+
*/
|
|
191
|
+
function reasoningText(payload) {
|
|
192
|
+
const summary = arrayField(payload, 'summary') ?? [];
|
|
193
|
+
const parts = [];
|
|
194
|
+
for (const block of summary) {
|
|
195
|
+
if (typeof block === 'string')
|
|
196
|
+
parts.push(block);
|
|
197
|
+
else if (typeof block === 'object' && block !== null) {
|
|
198
|
+
const text = block['text'];
|
|
199
|
+
if (typeof text === 'string')
|
|
200
|
+
parts.push(text);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return parts.length === 0 ? undefined : parts.join('\n');
|
|
204
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cursor agent transcripts:
|
|
3
|
+
* `~/.cursor/projects/<slug>/agent-transcripts/<composer>/<composer>.jsonl`.
|
|
4
|
+
*
|
|
5
|
+
* The record shape is the Messages API's without an envelope — each line is
|
|
6
|
+
* `{role, message: {content: [...]}}` and nothing else — so the fold is the
|
|
7
|
+
* shortest of any supported format. Three details are Cursor's own:
|
|
8
|
+
*
|
|
9
|
+
* - The opening prompt arrives wrapped in `<user_query>…</user_query>`, which
|
|
10
|
+
* is markup Cursor adds rather than anything a person typed.
|
|
11
|
+
* - Assistant text carries `[REDACTED]` sentinels where the client stripped
|
|
12
|
+
* something before writing. Left in, they read as though the model said the
|
|
13
|
+
* word.
|
|
14
|
+
* - There is no `tool_result`, no timestamp, and no `cwd` anywhere in the
|
|
15
|
+
* file: results live in a separate bubble store and the composer uuid is the
|
|
16
|
+
* only identity. So a Cursor transcript can never be workspace-scoped from
|
|
17
|
+
* its contents, which the source handles by listing it under `scope: 'all'`.
|
|
18
|
+
*
|
|
19
|
+
* @module dsh-reference-anything/local-agent/adapters/cursor
|
|
20
|
+
*/
|
|
21
|
+
import type { TranscriptAdapter } from '../types.ts';
|
|
22
|
+
/** Reads Cursor agent transcripts. */
|
|
23
|
+
export declare const cursorAdapter: TranscriptAdapter;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cursor agent transcripts:
|
|
3
|
+
* `~/.cursor/projects/<slug>/agent-transcripts/<composer>/<composer>.jsonl`.
|
|
4
|
+
*
|
|
5
|
+
* The record shape is the Messages API's without an envelope — each line is
|
|
6
|
+
* `{role, message: {content: [...]}}` and nothing else — so the fold is the
|
|
7
|
+
* shortest of any supported format. Three details are Cursor's own:
|
|
8
|
+
*
|
|
9
|
+
* - The opening prompt arrives wrapped in `<user_query>…</user_query>`, which
|
|
10
|
+
* is markup Cursor adds rather than anything a person typed.
|
|
11
|
+
* - Assistant text carries `[REDACTED]` sentinels where the client stripped
|
|
12
|
+
* something before writing. Left in, they read as though the model said the
|
|
13
|
+
* word.
|
|
14
|
+
* - There is no `tool_result`, no timestamp, and no `cwd` anywhere in the
|
|
15
|
+
* file: results live in a separate bubble store and the composer uuid is the
|
|
16
|
+
* only identity. So a Cursor transcript can never be workspace-scoped from
|
|
17
|
+
* its contents, which the source handles by listing it under `scope: 'all'`.
|
|
18
|
+
*
|
|
19
|
+
* @module dsh-reference-anything/local-agent/adapters/cursor
|
|
20
|
+
*/
|
|
21
|
+
import { joinLocalPath } from "../path.js";
|
|
22
|
+
import { arrayField, createSharedState, emitUser, flushAssistant, normalizeTitle, objectField, parseRecord, pushAssistant, renderToolCall, } from "./shared.js";
|
|
23
|
+
/** Wrapper Cursor puts around the prompt it sends, not something anyone typed. */
|
|
24
|
+
const USER_QUERY_TAG = /<\/?user_query>/gu;
|
|
25
|
+
/** Placeholder the client leaves where it removed content before writing. */
|
|
26
|
+
const REDACTED = /\[REDACTED\]/gu;
|
|
27
|
+
/** Reads Cursor agent transcripts. */
|
|
28
|
+
export const cursorAdapter = {
|
|
29
|
+
kind: 'cursor',
|
|
30
|
+
displayName: 'Cursor',
|
|
31
|
+
defaultRoots(home) {
|
|
32
|
+
return [joinLocalPath(home, '.cursor', 'projects')];
|
|
33
|
+
},
|
|
34
|
+
matches(relativePath) {
|
|
35
|
+
const path = relativePath.toLowerCase();
|
|
36
|
+
// Scoped to the transcript directory rather than to `.jsonl` alone: the
|
|
37
|
+
// project tree holds other line-delimited state this adapter cannot read.
|
|
38
|
+
return path.includes('agent-transcripts/') && path.endsWith('.jsonl');
|
|
39
|
+
},
|
|
40
|
+
createState() {
|
|
41
|
+
return createSharedState();
|
|
42
|
+
},
|
|
43
|
+
step(line, state, options) {
|
|
44
|
+
const record = parseRecord(line);
|
|
45
|
+
if (record === undefined)
|
|
46
|
+
return [];
|
|
47
|
+
const blocks = arrayField(objectField(record, 'message'), 'content') ?? [];
|
|
48
|
+
if (record['role'] === 'user') {
|
|
49
|
+
const text = userText(blocks);
|
|
50
|
+
return text === undefined ? [] : emitUser(state, text);
|
|
51
|
+
}
|
|
52
|
+
if (record['role'] !== 'assistant')
|
|
53
|
+
return [];
|
|
54
|
+
for (const block of blocks) {
|
|
55
|
+
if (typeof block !== 'object' || block === null)
|
|
56
|
+
continue;
|
|
57
|
+
const entry = block;
|
|
58
|
+
if (entry['type'] === 'text') {
|
|
59
|
+
pushAssistant(state, scrub(entry['text']));
|
|
60
|
+
}
|
|
61
|
+
else if (entry['type'] === 'tool_use') {
|
|
62
|
+
// `input` is already an object here; Cursor does not JSON-encode it the
|
|
63
|
+
// way the wire format does.
|
|
64
|
+
pushAssistant(state, renderToolCall(entry['name'], entry['input'], options.toolCalls, options.toolSummaryChars));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return [];
|
|
68
|
+
},
|
|
69
|
+
flush(state, _options) {
|
|
70
|
+
return flushAssistant(state);
|
|
71
|
+
},
|
|
72
|
+
head(headLines, tailLines) {
|
|
73
|
+
let firstPrompt;
|
|
74
|
+
for (const line of [...headLines, ...tailLines]) {
|
|
75
|
+
const record = parseRecord(line);
|
|
76
|
+
if (record?.['role'] !== 'user')
|
|
77
|
+
continue;
|
|
78
|
+
firstPrompt = userText(arrayField(objectField(record, 'message'), 'content') ?? []);
|
|
79
|
+
if (firstPrompt !== undefined)
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
const title = normalizeTitle(firstPrompt ?? '');
|
|
83
|
+
// No `createdAt` and no `cwd`: the format records neither, and inventing
|
|
84
|
+
// one from the file's mtime would claim the session started when it was
|
|
85
|
+
// last written to.
|
|
86
|
+
return {
|
|
87
|
+
...title === '' ? {} : { title },
|
|
88
|
+
...firstPrompt === undefined ? {} : { firstPrompt },
|
|
89
|
+
};
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
/**
|
|
93
|
+
* The words a person typed, from a user record's content blocks.
|
|
94
|
+
* @param blocks - the `message.content` array.
|
|
95
|
+
* @returns the prompt, or undefined when the record carried no text.
|
|
96
|
+
*/
|
|
97
|
+
function userText(blocks) {
|
|
98
|
+
const parts = [];
|
|
99
|
+
for (const block of blocks) {
|
|
100
|
+
if (typeof block !== 'object' || block === null)
|
|
101
|
+
continue;
|
|
102
|
+
const entry = block;
|
|
103
|
+
if (entry['type'] !== 'text' || typeof entry['text'] !== 'string')
|
|
104
|
+
continue;
|
|
105
|
+
const text = entry['text'].replace(USER_QUERY_TAG, '').trim();
|
|
106
|
+
if (text !== '')
|
|
107
|
+
parts.push(text);
|
|
108
|
+
}
|
|
109
|
+
return parts.length === 0 ? undefined : parts.join('\n');
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Assistant text with the client's redaction sentinels removed.
|
|
113
|
+
* @param value - the block's `text` field, of unknown type.
|
|
114
|
+
* @returns the text, or undefined when nothing survived the scrub.
|
|
115
|
+
*/
|
|
116
|
+
function scrub(value) {
|
|
117
|
+
if (typeof value !== 'string')
|
|
118
|
+
return undefined;
|
|
119
|
+
const text = value.replace(REDACTED, '').trim();
|
|
120
|
+
return text === '' ? undefined : text;
|
|
121
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gemini CLI chats: `~/.gemini/history/<slot>/chats/session-*.json`.
|
|
3
|
+
*
|
|
4
|
+
* The only supported format that is not line-delimited. One file is one JSON
|
|
5
|
+
* object — `{sessionId, projectHash, startTime, directories, messages}` — so
|
|
6
|
+
* half of it is not a shorter conversation, and the adapter declares itself
|
|
7
|
+
* {@link TranscriptAdapter.document} to say so: records are held verbatim
|
|
8
|
+
* until the file ends, then parsed once.
|
|
9
|
+
*
|
|
10
|
+
* The messages themselves are the simplest of any agent: `user` carries a
|
|
11
|
+
* parts array, `gemini` carries a plain string plus its `thoughts` and
|
|
12
|
+
* `toolCalls`, and `info` is the CLI's own banner text. Tool results are
|
|
13
|
+
* inlined on the call rather than split into a following record, which this
|
|
14
|
+
* projection drops along with every other result.
|
|
15
|
+
*
|
|
16
|
+
* @module dsh-reference-anything/local-agent/adapters/gemini-cli
|
|
17
|
+
*/
|
|
18
|
+
import type { TranscriptAdapter } from '../types.ts';
|
|
19
|
+
/** Reads Gemini CLI chat files. */
|
|
20
|
+
export declare const geminiCliAdapter: TranscriptAdapter;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gemini CLI chats: `~/.gemini/history/<slot>/chats/session-*.json`.
|
|
3
|
+
*
|
|
4
|
+
* The only supported format that is not line-delimited. One file is one JSON
|
|
5
|
+
* object — `{sessionId, projectHash, startTime, directories, messages}` — so
|
|
6
|
+
* half of it is not a shorter conversation, and the adapter declares itself
|
|
7
|
+
* {@link TranscriptAdapter.document} to say so: records are held verbatim
|
|
8
|
+
* until the file ends, then parsed once.
|
|
9
|
+
*
|
|
10
|
+
* The messages themselves are the simplest of any agent: `user` carries a
|
|
11
|
+
* parts array, `gemini` carries a plain string plus its `thoughts` and
|
|
12
|
+
* `toolCalls`, and `info` is the CLI's own banner text. Tool results are
|
|
13
|
+
* inlined on the call rather than split into a following record, which this
|
|
14
|
+
* projection drops along with every other result.
|
|
15
|
+
*
|
|
16
|
+
* @module dsh-reference-anything/local-agent/adapters/gemini-cli
|
|
17
|
+
*/
|
|
18
|
+
import { joinLocalPath } from "../path.js";
|
|
19
|
+
import { blankToUndefined, createSharedState, emitUser, flushAssistant, holdLine, normalizeTitle, parseDocument, parseTimestamp, pushAssistant, renderToolCall, renderToolResult, takeHeld, } from "./shared.js";
|
|
20
|
+
/** Reads Gemini CLI chat files. */
|
|
21
|
+
export const geminiCliAdapter = {
|
|
22
|
+
kind: 'gemini-cli',
|
|
23
|
+
displayName: 'Gemini CLI',
|
|
24
|
+
document: true,
|
|
25
|
+
defaultRoots(home) {
|
|
26
|
+
return [joinLocalPath(home, '.gemini', 'history')];
|
|
27
|
+
},
|
|
28
|
+
matches(relativePath) {
|
|
29
|
+
const path = relativePath.toLowerCase();
|
|
30
|
+
return path.includes('/chats/') && path.endsWith('.json');
|
|
31
|
+
},
|
|
32
|
+
createState() {
|
|
33
|
+
return createSharedState();
|
|
34
|
+
},
|
|
35
|
+
step(line, state, _options) {
|
|
36
|
+
return holdLine(state, line);
|
|
37
|
+
},
|
|
38
|
+
flush(state, options) {
|
|
39
|
+
const chat = asRecord(parseDocument(takeHeld(state)));
|
|
40
|
+
const messages = chat?.['messages'];
|
|
41
|
+
if (!Array.isArray(messages))
|
|
42
|
+
return [];
|
|
43
|
+
const turns = [];
|
|
44
|
+
for (const message of messages) {
|
|
45
|
+
const entry = asRecord(message);
|
|
46
|
+
if (entry === undefined)
|
|
47
|
+
continue;
|
|
48
|
+
if (entry['type'] === 'user') {
|
|
49
|
+
const text = blankToUndefined(partsText(entry['content']));
|
|
50
|
+
if (text !== undefined)
|
|
51
|
+
turns.push(...emitUser(state, text));
|
|
52
|
+
}
|
|
53
|
+
else if (entry['type'] === 'gemini') {
|
|
54
|
+
pushGemini(entry, state, options);
|
|
55
|
+
}
|
|
56
|
+
// `info` is a CLI banner — an error notice, a cancellation — not speech.
|
|
57
|
+
}
|
|
58
|
+
turns.push(...flushAssistant(state));
|
|
59
|
+
return turns;
|
|
60
|
+
},
|
|
61
|
+
head(headLines, tailLines) {
|
|
62
|
+
// A probe that captured the whole small file parses; one that captured a
|
|
63
|
+
// prefix of a large file does not, and the entry degrades to its filename
|
|
64
|
+
// rather than to a guess at the missing half.
|
|
65
|
+
const chat = asRecord(parseDocument(headLines.join('\n')))
|
|
66
|
+
?? asRecord(parseDocument([...headLines, ...tailLines].join('\n')));
|
|
67
|
+
if (chat === undefined)
|
|
68
|
+
return {};
|
|
69
|
+
const messages = Array.isArray(chat['messages']) ? chat['messages'] : [];
|
|
70
|
+
let firstPrompt;
|
|
71
|
+
for (const message of messages) {
|
|
72
|
+
const entry = asRecord(message);
|
|
73
|
+
if (entry?.['type'] !== 'user')
|
|
74
|
+
continue;
|
|
75
|
+
firstPrompt = blankToUndefined(partsText(entry['content']));
|
|
76
|
+
if (firstPrompt !== undefined)
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
const directories = chat['directories'];
|
|
80
|
+
const cwd = Array.isArray(directories) && typeof directories[0] === 'string' ? directories[0] : undefined;
|
|
81
|
+
const createdAt = parseTimestamp(chat['startTime']);
|
|
82
|
+
const title = normalizeTitle(firstPrompt ?? '');
|
|
83
|
+
return {
|
|
84
|
+
...title === '' ? {} : { title },
|
|
85
|
+
...cwd === undefined ? {} : { cwd },
|
|
86
|
+
...firstPrompt === undefined ? {} : { firstPrompt },
|
|
87
|
+
...createdAt === undefined ? {} : { createdAt },
|
|
88
|
+
};
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
/** Fold one `gemini` message into the open assistant run. */
|
|
92
|
+
function pushGemini(entry, state, options) {
|
|
93
|
+
if (typeof entry['content'] === 'string')
|
|
94
|
+
pushAssistant(state, entry['content']);
|
|
95
|
+
if (options.includeThinking && Array.isArray(entry['thoughts'])) {
|
|
96
|
+
for (const thought of entry['thoughts']) {
|
|
97
|
+
const it = asRecord(thought);
|
|
98
|
+
if (it === undefined)
|
|
99
|
+
continue;
|
|
100
|
+
const parts = [it['subject'], it['description']].filter((v) => typeof v === 'string' && v !== '');
|
|
101
|
+
if (parts.length > 0)
|
|
102
|
+
pushAssistant(state, parts.join(':'));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (!Array.isArray(entry['toolCalls']))
|
|
106
|
+
return;
|
|
107
|
+
for (const call of entry['toolCalls']) {
|
|
108
|
+
const it = asRecord(call);
|
|
109
|
+
if (it === undefined)
|
|
110
|
+
continue;
|
|
111
|
+
pushAssistant(state, renderToolCall(it['name'], it['args'], options.toolCalls, options.toolSummaryChars));
|
|
112
|
+
const output = it['result'] ?? it['output'] ?? it['response'];
|
|
113
|
+
if (output !== undefined)
|
|
114
|
+
pushAssistant(state, renderToolResult(output, options.toolResults, options.toolSummaryChars));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Flatten a `parts`-style content value to text.
|
|
119
|
+
* @param content - the message's `content`, of unknown shape.
|
|
120
|
+
* @returns the flattened text; empty when nothing readable was found.
|
|
121
|
+
*/
|
|
122
|
+
function partsText(content) {
|
|
123
|
+
if (typeof content === 'string')
|
|
124
|
+
return content;
|
|
125
|
+
if (!Array.isArray(content))
|
|
126
|
+
return '';
|
|
127
|
+
const parts = [];
|
|
128
|
+
for (const part of content) {
|
|
129
|
+
const entry = asRecord(part);
|
|
130
|
+
const text = entry?.['text'];
|
|
131
|
+
if (typeof text === 'string' && text.trim() !== '')
|
|
132
|
+
parts.push(text.trim());
|
|
133
|
+
}
|
|
134
|
+
return parts.join('\n');
|
|
135
|
+
}
|
|
136
|
+
/** Narrow an unknown to a plain object, which is the only shape this format uses. */
|
|
137
|
+
function asRecord(value) {
|
|
138
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
139
|
+
return undefined;
|
|
140
|
+
return value;
|
|
141
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Grok Build sessions: `~/.grok/sessions/<slug>/<id>/chat_history.jsonl`, and
|
|
3
|
+
* the same layout under `~/.grok/archived_sessions/`.
|
|
4
|
+
*
|
|
5
|
+
* A session is a *directory*, not a file: `chat_history.jsonl` holds the
|
|
6
|
+
* conversation while a sibling `summary.json` holds the generated title, the
|
|
7
|
+
* `cwd`, and the timestamps. A pure adapter reads one file, so the title falls
|
|
8
|
+
* back to the opening prompt and Grok sessions carry no recorded `cwd` — which
|
|
9
|
+
* means they list under `scope: 'all'` rather than being matched to a
|
|
10
|
+
* workspace. That is a real gap, not an oversight; closing it would mean
|
|
11
|
+
* giving adapters I/O, which is the one thing the interface exists to prevent.
|
|
12
|
+
*
|
|
13
|
+
* Records are `{type, content, timestamp}` with `content` either a string or a
|
|
14
|
+
* Messages-API block array. Two of the five types are dropped: `system` is
|
|
15
|
+
* harness-injected, and `reasoning` carries Grok's encrypted internal state,
|
|
16
|
+
* which renders as a wall of base64 rather than as thought.
|
|
17
|
+
*
|
|
18
|
+
* @module dsh-reference-anything/local-agent/adapters/grokbuild
|
|
19
|
+
*/
|
|
20
|
+
import type { TranscriptAdapter } from '../types.ts';
|
|
21
|
+
/** Reads Grok Build chat histories. */
|
|
22
|
+
export declare const grokbuildAdapter: TranscriptAdapter;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Grok Build sessions: `~/.grok/sessions/<slug>/<id>/chat_history.jsonl`, and
|
|
3
|
+
* the same layout under `~/.grok/archived_sessions/`.
|
|
4
|
+
*
|
|
5
|
+
* A session is a *directory*, not a file: `chat_history.jsonl` holds the
|
|
6
|
+
* conversation while a sibling `summary.json` holds the generated title, the
|
|
7
|
+
* `cwd`, and the timestamps. A pure adapter reads one file, so the title falls
|
|
8
|
+
* back to the opening prompt and Grok sessions carry no recorded `cwd` — which
|
|
9
|
+
* means they list under `scope: 'all'` rather than being matched to a
|
|
10
|
+
* workspace. That is a real gap, not an oversight; closing it would mean
|
|
11
|
+
* giving adapters I/O, which is the one thing the interface exists to prevent.
|
|
12
|
+
*
|
|
13
|
+
* Records are `{type, content, timestamp}` with `content` either a string or a
|
|
14
|
+
* Messages-API block array. Two of the five types are dropped: `system` is
|
|
15
|
+
* harness-injected, and `reasoning` carries Grok's encrypted internal state,
|
|
16
|
+
* which renders as a wall of base64 rather than as thought.
|
|
17
|
+
*
|
|
18
|
+
* @module dsh-reference-anything/local-agent/adapters/grokbuild
|
|
19
|
+
*/
|
|
20
|
+
import { joinLocalPath } from "../path.js";
|
|
21
|
+
import { arrayField, contentBlockUserText, createSharedState, emitUser, flushAssistant, normalizeTitle, parseRecord, parseTimestamp, pushAssistant, pushContentBlocks, pushToolResults, } from "./shared.js";
|
|
22
|
+
/** Reads Grok Build chat histories. */
|
|
23
|
+
export const grokbuildAdapter = {
|
|
24
|
+
kind: 'grokbuild',
|
|
25
|
+
displayName: 'Grok Build',
|
|
26
|
+
defaultRoots(home) {
|
|
27
|
+
return [joinLocalPath(home, '.grok', 'sessions'), joinLocalPath(home, '.grok', 'archived_sessions')];
|
|
28
|
+
},
|
|
29
|
+
matches(relativePath) {
|
|
30
|
+
const path = relativePath.toLowerCase();
|
|
31
|
+
return path.endsWith('/chat_history.jsonl') || path === 'chat_history.jsonl';
|
|
32
|
+
},
|
|
33
|
+
createState() {
|
|
34
|
+
return createSharedState();
|
|
35
|
+
},
|
|
36
|
+
step(line, state, options) {
|
|
37
|
+
const record = parseRecord(line);
|
|
38
|
+
if (record === undefined)
|
|
39
|
+
return [];
|
|
40
|
+
if (record['type'] === 'user') {
|
|
41
|
+
pushToolResults(state, record['content'], options);
|
|
42
|
+
const text = contentBlockUserText(record['content']);
|
|
43
|
+
return text === undefined ? [] : emitUser(state, text);
|
|
44
|
+
}
|
|
45
|
+
if (record['type'] !== 'assistant')
|
|
46
|
+
return [];
|
|
47
|
+
const content = record['content'];
|
|
48
|
+
if (typeof content === 'string')
|
|
49
|
+
pushAssistant(state, content);
|
|
50
|
+
else
|
|
51
|
+
pushContentBlocks(state, arrayField(record, 'content'), options);
|
|
52
|
+
return [];
|
|
53
|
+
},
|
|
54
|
+
flush(state, _options) {
|
|
55
|
+
return flushAssistant(state);
|
|
56
|
+
},
|
|
57
|
+
head(headLines, _tailLines) {
|
|
58
|
+
let createdAt;
|
|
59
|
+
let firstPrompt;
|
|
60
|
+
for (const line of headLines) {
|
|
61
|
+
const record = parseRecord(line);
|
|
62
|
+
if (record === undefined)
|
|
63
|
+
continue;
|
|
64
|
+
createdAt ??= parseTimestamp(record['timestamp']);
|
|
65
|
+
if (firstPrompt === undefined && record['type'] === 'user') {
|
|
66
|
+
firstPrompt = contentBlockUserText(record['content']);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const title = normalizeTitle(firstPrompt ?? '');
|
|
70
|
+
return {
|
|
71
|
+
...title === '' ? {} : { title },
|
|
72
|
+
...firstPrompt === undefined ? {} : { firstPrompt },
|
|
73
|
+
...createdAt === undefined ? {} : { createdAt },
|
|
74
|
+
};
|
|
75
|
+
},
|
|
76
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hermes sessions: `~/.hermes/sessions/<id>.jsonl`.
|
|
3
|
+
*
|
|
4
|
+
* Hermes writes two record shapes and neither generation is retired, so both
|
|
5
|
+
* are accepted per line: a flat `{role, content, ts}` and a nested
|
|
6
|
+
* `{type: 'session' | 'message', message: {role, content}, timestamp}`. The
|
|
7
|
+
* discriminator is whether a `message` object is present, which costs one
|
|
8
|
+
* lookup and removes the need to sniff the file first.
|
|
9
|
+
*
|
|
10
|
+
* Hermes also keeps its canonical history in a SQLite `state.db` and treats
|
|
11
|
+
* the JSONL files as an export. Reading the database needs a query interface
|
|
12
|
+
* rather than a line fold, so it is out of this adapter's scope; a session
|
|
13
|
+
* that exists only in `state.db` will not appear in the menu.
|
|
14
|
+
*
|
|
15
|
+
* @module dsh-reference-anything/local-agent/adapters/hermes
|
|
16
|
+
*/
|
|
17
|
+
import type { TranscriptAdapter } from '../types.ts';
|
|
18
|
+
/** Reads Hermes session exports. */
|
|
19
|
+
export declare const hermesAdapter: TranscriptAdapter;
|