dsh-reference-anything 0.3.1 → 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.
Files changed (107) hide show
  1. package/LICENSE +22 -22
  2. package/NOTICE.md +52 -25
  3. package/README.md +334 -285
  4. package/cordis.patch.yml +89 -18
  5. package/lib/client.js +13530 -517
  6. package/lib/contract.d.ts +155 -3
  7. package/lib/contract.js +68 -4
  8. package/lib/download-directory.d.ts +18 -0
  9. package/lib/download-directory.js +55 -0
  10. package/lib/errors.d.ts +4 -0
  11. package/lib/host.d.ts +96 -3
  12. package/lib/host.js +88 -5
  13. package/lib/index.d.ts +22 -2
  14. package/lib/index.js +57 -7
  15. package/lib/opencli.d.ts +5 -3
  16. package/lib/opencli.js +13 -6
  17. package/lib/openlist/host.d.ts +157 -0
  18. package/lib/openlist/host.js +873 -0
  19. package/lib/openlist/index.d.ts +84 -0
  20. package/lib/openlist/index.js +507 -0
  21. package/lib/render.d.ts +12 -7
  22. package/lib/render.js +27 -21
  23. package/lib/sources/cloud-drive/cache.d.ts +60 -0
  24. package/lib/sources/cloud-drive/cache.js +107 -0
  25. package/lib/sources/cloud-drive/index.d.ts +190 -0
  26. package/lib/sources/cloud-drive/index.js +526 -0
  27. package/lib/sources/cloud-drive/providers/http.d.ts +27 -0
  28. package/lib/sources/cloud-drive/providers/http.js +73 -0
  29. package/lib/sources/cloud-drive/providers/openlist.d.ts +59 -0
  30. package/lib/sources/cloud-drive/providers/openlist.js +535 -0
  31. package/lib/sources/cloud-drive/registry.d.ts +53 -0
  32. package/lib/sources/cloud-drive/registry.js +101 -0
  33. package/lib/sources/cloud-drive/types.d.ts +169 -0
  34. package/lib/sources/cloud-drive/types.js +17 -0
  35. package/lib/sources/file.d.ts +2 -0
  36. package/lib/sources/file.js +2 -0
  37. package/lib/sources/local-agent/adapters/claude-code.d.ts +16 -0
  38. package/lib/sources/local-agent/adapters/claude-code.js +171 -0
  39. package/lib/sources/local-agent/adapters/codex.d.ts +20 -0
  40. package/lib/sources/local-agent/adapters/codex.js +204 -0
  41. package/lib/sources/local-agent/adapters/cursor.d.ts +23 -0
  42. package/lib/sources/local-agent/adapters/cursor.js +121 -0
  43. package/lib/sources/local-agent/adapters/gemini-cli.d.ts +20 -0
  44. package/lib/sources/local-agent/adapters/gemini-cli.js +141 -0
  45. package/lib/sources/local-agent/adapters/grokbuild.d.ts +22 -0
  46. package/lib/sources/local-agent/adapters/grokbuild.js +76 -0
  47. package/lib/sources/local-agent/adapters/hermes.d.ts +19 -0
  48. package/lib/sources/local-agent/adapters/hermes.js +85 -0
  49. package/lib/sources/local-agent/adapters/kimi.d.ts +28 -0
  50. package/lib/sources/local-agent/adapters/kimi.js +261 -0
  51. package/lib/sources/local-agent/adapters/openclaw.d.ts +21 -0
  52. package/lib/sources/local-agent/adapters/openclaw.js +120 -0
  53. package/lib/sources/local-agent/adapters/opencode.d.ts +44 -0
  54. package/lib/sources/local-agent/adapters/opencode.js +236 -0
  55. package/lib/sources/local-agent/adapters/pi.d.ts +26 -0
  56. package/lib/sources/local-agent/adapters/pi.js +242 -0
  57. package/lib/sources/local-agent/adapters/qoder.d.ts +19 -0
  58. package/lib/sources/local-agent/adapters/qoder.js +93 -0
  59. package/lib/sources/local-agent/adapters/reasonix.d.ts +23 -0
  60. package/lib/sources/local-agent/adapters/reasonix.js +90 -0
  61. package/lib/sources/local-agent/adapters/shared.d.ts +163 -0
  62. package/lib/sources/local-agent/adapters/shared.js +415 -0
  63. package/lib/sources/local-agent/adapters/sqlite-shared.d.ts +92 -0
  64. package/lib/sources/local-agent/adapters/sqlite-shared.js +215 -0
  65. package/lib/sources/local-agent/adapters/zcode.d.ts +41 -0
  66. package/lib/sources/local-agent/adapters/zcode.js +159 -0
  67. package/lib/sources/local-agent/index.d.ts +222 -0
  68. package/lib/sources/local-agent/index.js +656 -0
  69. package/lib/sources/local-agent/page.d.ts +55 -0
  70. package/lib/sources/local-agent/page.js +83 -0
  71. package/lib/sources/local-agent/path.d.ts +8 -0
  72. package/lib/sources/local-agent/path.js +43 -0
  73. package/lib/sources/local-agent/registry.d.ts +68 -0
  74. package/lib/sources/local-agent/registry.js +116 -0
  75. package/lib/sources/local-agent/scan.d.ts +175 -0
  76. package/lib/sources/local-agent/scan.js +472 -0
  77. package/lib/sources/local-agent/sqlite.d.ts +45 -0
  78. package/lib/sources/local-agent/sqlite.js +113 -0
  79. package/lib/sources/local-agent/store.d.ts +93 -0
  80. package/lib/sources/local-agent/store.js +113 -0
  81. package/lib/sources/local-agent/types.d.ts +338 -0
  82. package/lib/sources/local-agent/types.js +20 -0
  83. package/lib/sources/web-chat/index.d.ts +5 -1
  84. package/lib/sources/web-chat/index.js +47 -8
  85. package/lib/store/spec.d.ts +50 -0
  86. package/lib/store/spec.js +2 -2
  87. package/lib/sync/index.d.ts +1 -0
  88. package/lib/sync/index.js +28 -8
  89. package/lib/tool.d.ts +5 -0
  90. package/lib/tool.js +418 -45
  91. package/lib/typert.js +13 -1
  92. package/lib/types.d.ts +28 -2
  93. package/lib/update.d.ts +6 -0
  94. package/lib/update.js +29 -1
  95. package/lib/web.js +5 -1
  96. package/lib/wire.d.ts +80 -0
  97. package/lib/wire.js +30 -1
  98. package/opencli-plugin/chatgpt.js +30 -27
  99. package/opencli-plugin/claude.js +12 -8
  100. package/opencli-plugin/common.js +266 -187
  101. package/opencli-plugin/deepseek.js +8 -5
  102. package/opencli-plugin/gemini.js +42 -38
  103. package/opencli-plugin/grok.js +22 -21
  104. package/opencli-plugin/kimi.js +34 -31
  105. package/opencli-plugin/opencli-plugin.json +6 -6
  106. package/opencli-plugin/package.json +8 -8
  107. package/package.json +200 -185
@@ -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;