dsh-reference-anything 0.3.1 → 0.3.3

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 +342 -284
  4. package/cordis.patch.yml +89 -18
  5. package/lib/client.js +13534 -521
  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,236 @@
1
+ /**
2
+ * Reads opencode's SQLite history, and mimocode's fork of it.
3
+ *
4
+ * opencode keeps every session it has ever run in one database under
5
+ * `~/.local/share/opencode/`, rather than one file per conversation. That single
6
+ * fact is why this is a {@link QueryAdapter} instead of a
7
+ * {@link TranscriptAdapter}: there is no byte offset that means "the start of
8
+ * this conversation", so a reference has to name the session as well as the
9
+ * file, and a listing is a query rather than a directory walk.
10
+ *
11
+ * mimocode is a fork of it, and older opencode builds differ from newer ones,
12
+ * so the columns that vary — `time_updated` here, `parent_id` in zcode — are
13
+ * probed rather than branched on a version number nobody writes down. What
14
+ * mimocode does not share is its background traffic: MiMo runs
15
+ * memory-consolidation and distillation sessions through the same tables as the
16
+ * user's own work, and those are filtered out of discovery.
17
+ *
18
+ * @module dsh-reference-anything/local-agent/adapters/opencode
19
+ */
20
+ import { joinLocalPath } from "../path.js";
21
+ import { COMPACTION_MARKER, createSharedState, flushAssistant, normalizeTitle, parseTimestamp } from "./shared.js";
22
+ import { nestedText, parseBlob, readSessionMessages, stepSessionMessage } from "./sqlite-shared.js";
23
+ /**
24
+ * Sessions MiMo runs for itself rather than for the user.
25
+ *
26
+ * Measured against a real `mimocode.db`: 822 `checkpoint-writer` sessions
27
+ * against a handful of real ones. Listing them would bury the user's own work
28
+ * under machinery they never started.
29
+ */
30
+ const BACKGROUND_TITLE = /^(checkpoint[-_ ]?writer|auto[-_ ]?(dream|distill))\b/iu;
31
+ /**
32
+ * How far discovery will over-fetch chasing enough non-background sessions.
33
+ *
34
+ * The filter is applied in this process rather than in SQL because the exact
35
+ * predicate is a regular expression and the SQL equivalent — a pile of `LIKE`
36
+ * patterns — would either miss forms or swallow a real session that happened to
37
+ * be titled about checkpoints. Over-fetching is the cost of getting that right,
38
+ * and it is bounded here rather than left to widen without limit.
39
+ */
40
+ const OVERFETCH_STEPS = [1, 4, 16];
41
+ /** Reads opencode's session database. */
42
+ export const opencodeAdapter = variantAdapter({
43
+ kind: 'opencode',
44
+ displayName: 'opencode',
45
+ dataDir: 'opencode',
46
+ dbName: 'opencode.db',
47
+ background: false,
48
+ });
49
+ /** Reads mimocode's session database, minus its own background traffic. */
50
+ export const mimocodeAdapter = variantAdapter({
51
+ kind: 'mimocode',
52
+ displayName: 'mimocode',
53
+ dataDir: 'mimocode',
54
+ dbName: 'mimocode.db',
55
+ background: true,
56
+ });
57
+ /**
58
+ * Whether a session is MiMo's own background work rather than the user's.
59
+ *
60
+ * Exported for the tests, which is the only way to pin a filter whose whole job
61
+ * is to be invisible when it works.
62
+ * @param title - the session's recorded title.
63
+ * @returns whether discovery should hide it.
64
+ */
65
+ export function isBackgroundSession(title) {
66
+ return BACKGROUND_TITLE.test(title.trim());
67
+ }
68
+ function variantAdapter(variant) {
69
+ return {
70
+ kind: variant.kind,
71
+ displayName: variant.displayName,
72
+ query: true,
73
+ defaultRoots(home) {
74
+ // The root is the directory, not the database: discovery walks
75
+ // directories, and pointing it at a file would make the one format that
76
+ // needs no walking the only one that needs a special case.
77
+ return [joinLocalPath(home, '.local', 'share', variant.dataDir)];
78
+ },
79
+ matches(relativePath) {
80
+ // Exact, so SQLite's `-wal` and `-shm` sidecars are not mistaken for
81
+ // databases and a backup copy is not listed as a second corpus.
82
+ return relativePath.split(/[\\/]/u).at(-1) === variant.dbName;
83
+ },
84
+ sessions(db, limit) {
85
+ const query = sessionQuery(db);
86
+ if (query === undefined)
87
+ return [];
88
+ const wanted = Math.max(1, Math.trunc(limit));
89
+ let kept = [];
90
+ for (const step of OVERFETCH_STEPS) {
91
+ const ask = wanted * step;
92
+ const rows = db.all(`SELECT ${query.fields} FROM session ORDER BY ${query.recency} DESC, id DESC LIMIT ?`, ask);
93
+ kept = [];
94
+ for (const row of rows) {
95
+ const session = toSession(row, query.recency);
96
+ if (session === undefined)
97
+ continue;
98
+ if (variant.background && isBackgroundSession(session.title))
99
+ continue;
100
+ kept.push(session);
101
+ if (kept.length >= wanted)
102
+ break;
103
+ }
104
+ // Enough survived the filter, or the database has nothing more to give.
105
+ if (kept.length >= wanted || rows.length < ask)
106
+ break;
107
+ }
108
+ return kept;
109
+ },
110
+ session(db, sessionId) {
111
+ const query = sessionQuery(db);
112
+ if (query === undefined)
113
+ return undefined;
114
+ const [row] = db.all(`SELECT ${query.fields} FROM session WHERE id = ? LIMIT 1`, sessionId);
115
+ return row === undefined ? undefined : toSession(row, query.recency);
116
+ },
117
+ turns(db, sessionId, options, maxRecords) {
118
+ const { messages, truncated } = readSessionMessages(db, sessionId, maxRecords);
119
+ const { kept, summary, compacted } = applyCompaction(messages);
120
+ const state = createSharedState();
121
+ const items = [];
122
+ // The summary stands where the discarded turns were: first, so the reader
123
+ // meets it before the tail it explains.
124
+ if (summary !== undefined)
125
+ items.push({ role: 'assistant', text: `${COMPACTION_MARKER}\n\n${summary}` });
126
+ for (const message of kept)
127
+ items.push(...stepSessionMessage(state, message, options));
128
+ items.push(...flushAssistant(state));
129
+ return { items, truncated, compacted };
130
+ },
131
+ };
132
+ }
133
+ /**
134
+ * Which columns this build has, resolved once per query.
135
+ *
136
+ * `time_updated` is the one that actually varies between the two forks and
137
+ * across opencode's own history, so it is probed rather than assumed; a build
138
+ * without it orders and dates sessions by when they started.
139
+ * @param db - a read-only reader over the database.
140
+ * @returns the select list and recency column, or undefined when there is no `session` table.
141
+ */
142
+ function sessionQuery(db) {
143
+ const columns = db.columns('session');
144
+ if (columns.size === 0)
145
+ return undefined;
146
+ const recency = columns.has('time_updated') ? 'time_updated' : 'time_created';
147
+ const fields = ['id', 'title', 'directory', 'time_created'];
148
+ if (recency === 'time_updated')
149
+ fields.push('time_updated');
150
+ return { fields: fields.join(', '), recency };
151
+ }
152
+ /**
153
+ * Project one `session` row.
154
+ * @param row - the row as read.
155
+ * @param recency - which column carries last activity.
156
+ * @returns the session, or undefined when the row has no usable id.
157
+ */
158
+ function toSession(row, recency) {
159
+ const id = typeof row['id'] === 'string' ? row['id'] : String(row['id'] ?? '');
160
+ if (id === '')
161
+ return undefined;
162
+ const createdAt = parseTimestamp(row['time_created']) ?? 0;
163
+ return {
164
+ id,
165
+ title: normalizeTitle(row['title']),
166
+ cwd: typeof row['directory'] === 'string' ? row['directory'] : '',
167
+ createdAt,
168
+ updatedAt: parseTimestamp(row[recency]) ?? createdAt,
169
+ };
170
+ }
171
+ /**
172
+ * Honour the compaction opencode recorded, rather than unwinding it.
173
+ *
174
+ * A `compaction` part names the message the surviving tail starts at, and the
175
+ * summary message beside it stands in for everything before. Replaying the
176
+ * discarded messages would contradict what the agent itself decided to forget;
177
+ * what is kept is the tail plus the summary, and the caller marks the read
178
+ * partial so the loss is visible.
179
+ * @param messages - the session's messages, oldest first.
180
+ * @returns the messages to fold, the summary text, and whether a boundary was crossed.
181
+ */
182
+ function applyCompaction(messages) {
183
+ let tailStart;
184
+ let summary;
185
+ for (const message of messages) {
186
+ for (const part of message.parts) {
187
+ if (part['type'] === 'compaction' && typeof part['tail_start_id'] === 'string') {
188
+ tailStart = part['tail_start_id'];
189
+ }
190
+ }
191
+ if (!isSummaryMessage(message))
192
+ continue;
193
+ const text = message.parts
194
+ .filter(part => part['type'] === 'text' && typeof part['text'] === 'string')
195
+ .map(part => String(part['text']).trim())
196
+ .filter(text => text !== '')
197
+ .join('\n')
198
+ .trim();
199
+ if (text !== '')
200
+ summary = text;
201
+ }
202
+ if (tailStart === undefined)
203
+ return { kept: messages, summary: undefined, compacted: false };
204
+ const at = messages.findIndex(message => message.id === tailStart);
205
+ // A `tail_start_id` pointing at a message the record cap already dropped is
206
+ // not a reason to discard the tail we do have.
207
+ if (at < 0)
208
+ return { kept: messages, summary, compacted: true };
209
+ return { kept: messages.slice(at).filter(message => !isSummaryMessage(message)), summary, compacted: true };
210
+ }
211
+ /** Whether a message is a compaction summary rather than part of the conversation. */
212
+ function isSummaryMessage(message) {
213
+ return message.data['mode'] === 'compaction' || message.data['summary'] === true;
214
+ }
215
+ /**
216
+ * The working directory a session ran in, when the session row did not record one.
217
+ *
218
+ * Older opencode databases leave `session.directory` empty and put the path on
219
+ * each message instead, so this is the fallback discovery uses before deciding
220
+ * a session belongs to no workspace.
221
+ * @param db - a read-only reader over the database.
222
+ * @param sessionId - the conversation to inspect.
223
+ * @returns the recorded cwd, or undefined when no message carries one.
224
+ */
225
+ export function sessionCwd(db, sessionId) {
226
+ const rows = db.all('SELECT data FROM message WHERE session_id = ? ORDER BY time_created, id LIMIT 8', sessionId);
227
+ for (const row of rows) {
228
+ const data = parseBlob(row['data']);
229
+ if (data === undefined)
230
+ continue;
231
+ const cwd = nestedText(data, 'path', 'cwd');
232
+ if (cwd !== undefined)
233
+ return cwd;
234
+ }
235
+ return undefined;
236
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Pi Coding Agent sessions:
3
+ * `~/.pi/agent/sessions/--<cwd>--/<stamp>_<uuid>.jsonl`.
4
+ *
5
+ * Line-delimited, but not linear. Every entry after the `session` header
6
+ * carries an `id` and a `parentId`, and the file accumulates *all* branches a
7
+ * session explored — so reading it front to back would interleave abandoned
8
+ * attempts with the one that was kept. The conversation is the path from the
9
+ * last entry back to the root, which is only knowable once the file has ended;
10
+ * hence {@link TranscriptAdapter.document}. Older v1 files have no ids at all
11
+ * and are treated as a chain in write order.
12
+ *
13
+ * Compaction is respected rather than unwound: from the newest `compaction` on
14
+ * the active path, the conversation is its summary, then whatever it retained,
15
+ * then everything after it. The entries it replaced are not in the file to
16
+ * read, so the summary is the only surviving account of them.
17
+ *
18
+ * Pi also names its blocks differently from the Messages API — a tool call's
19
+ * argument bag is `arguments`, not `input` — which the shared block renderer
20
+ * already accepts.
21
+ *
22
+ * @module dsh-reference-anything/local-agent/adapters/pi
23
+ */
24
+ import type { TranscriptAdapter } from '../types.ts';
25
+ /** Reads Pi Coding Agent sessions. */
26
+ export declare const piAdapter: TranscriptAdapter;
@@ -0,0 +1,242 @@
1
+ /**
2
+ * Pi Coding Agent sessions:
3
+ * `~/.pi/agent/sessions/--<cwd>--/<stamp>_<uuid>.jsonl`.
4
+ *
5
+ * Line-delimited, but not linear. Every entry after the `session` header
6
+ * carries an `id` and a `parentId`, and the file accumulates *all* branches a
7
+ * session explored — so reading it front to back would interleave abandoned
8
+ * attempts with the one that was kept. The conversation is the path from the
9
+ * last entry back to the root, which is only knowable once the file has ended;
10
+ * hence {@link TranscriptAdapter.document}. Older v1 files have no ids at all
11
+ * and are treated as a chain in write order.
12
+ *
13
+ * Compaction is respected rather than unwound: from the newest `compaction` on
14
+ * the active path, the conversation is its summary, then whatever it retained,
15
+ * then everything after it. The entries it replaced are not in the file to
16
+ * read, so the summary is the only surviving account of them.
17
+ *
18
+ * Pi also names its blocks differently from the Messages API — a tool call's
19
+ * argument bag is `arguments`, not `input` — which the shared block renderer
20
+ * already accepts.
21
+ *
22
+ * @module dsh-reference-anything/local-agent/adapters/pi
23
+ */
24
+ import { joinLocalPath } from "../path.js";
25
+ import { COMPACTION_MARKER, arrayField, blankToUndefined, createSharedState, emitUser, flushAssistant, holdLine, normalizeTitle, objectField, parseRecord, parseTimestamp, pushAssistant, pushContentBlocks, renderToolResult, stringField, takeHeld, } from "./shared.js";
26
+ /** Precedes the text summarizing a branch the session came back from. */
27
+ const BRANCH_MARKER = '[summary of an earlier branch]';
28
+ /** Reads Pi Coding Agent sessions. */
29
+ export const piAdapter = {
30
+ kind: 'pi',
31
+ displayName: 'Pi',
32
+ document: true,
33
+ defaultRoots(home) {
34
+ return [joinLocalPath(home, '.pi', 'agent', 'sessions')];
35
+ },
36
+ matches(relativePath) {
37
+ return relativePath.toLowerCase().endsWith('.jsonl');
38
+ },
39
+ createState() {
40
+ return createSharedState();
41
+ },
42
+ step(line, state, _options) {
43
+ return holdLine(state, line);
44
+ },
45
+ flush(state, options) {
46
+ const document = takeHeld(state);
47
+ if (document === undefined)
48
+ return flushAssistant(state);
49
+ const entries = [];
50
+ for (const line of document.split('\n')) {
51
+ const record = parseRecord(line);
52
+ if (record === undefined || record['type'] === 'session')
53
+ continue;
54
+ const id = stringField(record, 'id') ?? `e${entries.length}`;
55
+ // v1 wrote no ids, so an entry with no parent continues the previous
56
+ // one. That makes the "tree" a chain, which is what v1 sessions are.
57
+ const parentId = typeof record['parentId'] === 'string'
58
+ ? record['parentId']
59
+ : entries[entries.length - 1]?.id;
60
+ entries.push({ id, parentId, record });
61
+ }
62
+ const turns = [];
63
+ for (const item of compacted(activeBranch(entries), state)) {
64
+ turns.push(...foldEntry(item, state, options));
65
+ }
66
+ turns.push(...flushAssistant(state));
67
+ return turns;
68
+ },
69
+ head(headLines, tailLines) {
70
+ let cwd;
71
+ let createdAt;
72
+ let named;
73
+ let firstPrompt;
74
+ // Pi's header is the first line, so a probe of the start always reaches
75
+ // it — the branch tree only matters once turns are being read.
76
+ for (const line of [...headLines, ...tailLines]) {
77
+ const record = parseRecord(line);
78
+ if (record === undefined)
79
+ continue;
80
+ if (record['type'] === 'session') {
81
+ cwd ??= stringField(record, 'cwd');
82
+ createdAt ??= parseTimestamp(record['timestamp']);
83
+ continue;
84
+ }
85
+ // A rename writes another `session_info`, so the newest one wins.
86
+ if (record['type'] === 'session_info')
87
+ named = stringField(record, 'name') ?? named;
88
+ }
89
+ for (const line of headLines) {
90
+ const record = parseRecord(line);
91
+ if (record?.['type'] !== 'message')
92
+ continue;
93
+ const message = objectField(record, 'message');
94
+ if (message?.['role'] !== 'user')
95
+ continue;
96
+ firstPrompt = blankToUndefined(userText(message['content']));
97
+ if (firstPrompt !== undefined)
98
+ break;
99
+ }
100
+ const title = normalizeTitle(named ?? firstPrompt ?? '');
101
+ return {
102
+ ...title === '' ? {} : { title },
103
+ ...cwd === undefined ? {} : { cwd },
104
+ ...firstPrompt === undefined ? {} : { firstPrompt },
105
+ ...createdAt === undefined ? {} : { createdAt },
106
+ };
107
+ },
108
+ };
109
+ /**
110
+ * The path from the newest entry back to the root, in write order.
111
+ *
112
+ * The last entry is the leaf of whichever branch the session ended on, so
113
+ * walking its parents recovers exactly the conversation a person would say
114
+ * happened and discards the attempts they backed out of.
115
+ * @param entries - every entry in the file, in write order.
116
+ * @returns the active path, oldest first.
117
+ */
118
+ function activeBranch(entries) {
119
+ const byId = new Map(entries.map(entry => [entry.id, entry]));
120
+ const path = [];
121
+ const seen = new Set();
122
+ let cursor = entries[entries.length - 1];
123
+ while (cursor !== undefined && !seen.has(cursor.id)) {
124
+ seen.add(cursor.id);
125
+ path.push(cursor);
126
+ cursor = cursor.parentId === undefined ? undefined : byId.get(cursor.parentId);
127
+ }
128
+ return path.reverse();
129
+ }
130
+ /**
131
+ * Trim the path to what survived the newest compaction on it.
132
+ *
133
+ * A `compaction` entry states that everything before it is gone and carries
134
+ * both a summary and, on modern files, the tail it kept verbatim. Older files
135
+ * name the first surviving entry instead.
136
+ * @param path - the active branch, oldest first.
137
+ * @param state - fold state; marked compacted when a boundary is crossed.
138
+ * @returns the entries to fold, oldest first.
139
+ */
140
+ function compacted(path, state) {
141
+ let at = -1;
142
+ for (let index = path.length - 1; index >= 0; index -= 1) {
143
+ if (path[index]?.record['type'] === 'compaction') {
144
+ at = index;
145
+ break;
146
+ }
147
+ }
148
+ const boundary = at < 0 ? undefined : path[at];
149
+ if (boundary === undefined)
150
+ return path;
151
+ state.compacted = true;
152
+ const kept = [boundary];
153
+ const retained = arrayField(boundary.record, 'retainedTail');
154
+ if (retained !== undefined) {
155
+ for (const message of retained) {
156
+ kept.push({ id: '', parentId: undefined, record: { type: 'message', message } });
157
+ }
158
+ }
159
+ else {
160
+ const from = stringField(boundary.record, 'firstKeptEntryId');
161
+ const start = from === undefined ? -1 : path.findIndex(entry => entry.id === from);
162
+ if (start >= 0)
163
+ kept.push(...path.slice(start, at));
164
+ }
165
+ kept.push(...path.slice(at + 1));
166
+ return kept;
167
+ }
168
+ /** Fold one entry on the active path. */
169
+ function foldEntry(entry, state, options) {
170
+ const record = entry.record;
171
+ switch (record['type']) {
172
+ case 'message':
173
+ return foldMessage(objectField(record, 'message'), state, options);
174
+ case 'compaction':
175
+ pushAssistant(state, marked(COMPACTION_MARKER, stringField(record, 'summary')));
176
+ return [];
177
+ case 'branch_summary':
178
+ pushAssistant(state, marked(BRANCH_MARKER, stringField(record, 'summary')));
179
+ return [];
180
+ default:
181
+ // `model_change`, `thinking_level_change`, `label`, `session_info`, and
182
+ // the extension-injected `custom` entries are session state.
183
+ return [];
184
+ }
185
+ }
186
+ /** Fold one embedded `AgentMessage`, whose role vocabulary is Pi's own. */
187
+ function foldMessage(message, state, options) {
188
+ if (message === undefined)
189
+ return [];
190
+ switch (message['role']) {
191
+ case 'user': {
192
+ const text = blankToUndefined(userText(message['content']));
193
+ return text === undefined ? [] : emitUser(state, text);
194
+ }
195
+ case 'assistant':
196
+ pushContentBlocks(state, arrayField(message, 'content'), options);
197
+ return [];
198
+ case 'toolResult':
199
+ case 'bashExecution':
200
+ pushAssistant(state, renderToolResult(message['content'], options.toolResults, options.toolSummaryChars));
201
+ return [];
202
+ case 'compactionSummary':
203
+ pushAssistant(state, marked(COMPACTION_MARKER, stringField(message, 'summary')));
204
+ return [];
205
+ case 'branchSummary':
206
+ pushAssistant(state, marked(BRANCH_MARKER, stringField(message, 'summary')));
207
+ return [];
208
+ default:
209
+ // `custom` is an extension's injected context.
210
+ return [];
211
+ }
212
+ }
213
+ /**
214
+ * Flatten a Pi content value to the words a person typed.
215
+ * @param content - the message's `content`, a string or a block array.
216
+ * @returns the flattened text; empty when nothing readable was found.
217
+ */
218
+ function userText(content) {
219
+ if (typeof content === 'string')
220
+ return content;
221
+ if (!Array.isArray(content))
222
+ return '';
223
+ const parts = [];
224
+ for (const block of content) {
225
+ if (typeof block !== 'object' || block === null)
226
+ continue;
227
+ const entry = block;
228
+ if (entry['type'] === 'text' && typeof entry['text'] === 'string' && entry['text'].trim() !== '') {
229
+ parts.push(entry['text'].trim());
230
+ }
231
+ else if (entry['type'] === 'image') {
232
+ const mime = typeof entry['mimeType'] === 'string' ? entry['mimeType'] : 'image';
233
+ parts.push(`[image: ${mime}]`);
234
+ }
235
+ }
236
+ return parts.join('\n');
237
+ }
238
+ /** Label a summary so it does not read as something the assistant just said. */
239
+ function marked(marker, summary) {
240
+ const text = summary?.trim() ?? '';
241
+ return text === '' ? undefined : `${marker}\n\n${text}`;
242
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Qoder CLI sessions: `~/.qoder/projects/<encoded-project>/<sessionId>.jsonl`.
3
+ *
4
+ * Qoder adopted Claude Code's record layout nearly verbatim — the same `user`
5
+ * and `assistant` envelopes around a Messages-API `message`, the same
6
+ * `ai-title` and `last-prompt` state records, the same `cwd` on every entry —
7
+ * so the fold is the same one, and this adapter exists mainly to own the
8
+ * different roots and the subagent rule.
9
+ *
10
+ * That rule is the one thing worth stating: Qoder writes a spawned subagent's
11
+ * transcript to `<sessionId>/subagents/*.jsonl` while stamping it with the
12
+ * *parent's* `sessionId`. Listing those as sessions of their own would give
13
+ * two entries the same identity, so they are rejected by path.
14
+ *
15
+ * @module dsh-reference-anything/local-agent/adapters/qoder
16
+ */
17
+ import type { TranscriptAdapter } from '../types.ts';
18
+ /** Reads Qoder CLI session transcripts. */
19
+ export declare const qoderAdapter: TranscriptAdapter;
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Qoder CLI sessions: `~/.qoder/projects/<encoded-project>/<sessionId>.jsonl`.
3
+ *
4
+ * Qoder adopted Claude Code's record layout nearly verbatim — the same `user`
5
+ * and `assistant` envelopes around a Messages-API `message`, the same
6
+ * `ai-title` and `last-prompt` state records, the same `cwd` on every entry —
7
+ * so the fold is the same one, and this adapter exists mainly to own the
8
+ * different roots and the subagent rule.
9
+ *
10
+ * That rule is the one thing worth stating: Qoder writes a spawned subagent's
11
+ * transcript to `<sessionId>/subagents/*.jsonl` while stamping it with the
12
+ * *parent's* `sessionId`. Listing those as sessions of their own would give
13
+ * two entries the same identity, so they are rejected by path.
14
+ *
15
+ * @module dsh-reference-anything/local-agent/adapters/qoder
16
+ */
17
+ import { joinLocalPath } from "../path.js";
18
+ import { arrayField, contentBlockUserText, createSharedState, emitUser, flushAssistant, normalizeTitle, objectField, parseRecord, parseTimestamp, pushAssistant, pushContentBlocks, pushToolResults, stringField, } from "./shared.js";
19
+ /** Reads Qoder CLI session transcripts. */
20
+ export const qoderAdapter = {
21
+ kind: 'qoder',
22
+ displayName: 'Qoder',
23
+ defaultRoots(home) {
24
+ return [joinLocalPath(home, '.qoder', 'projects')];
25
+ },
26
+ matches(relativePath) {
27
+ const path = relativePath.toLowerCase();
28
+ return path.endsWith('.jsonl') && !path.includes('/subagents/');
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 message = objectField(record, 'message');
38
+ if (record['type'] === 'user') {
39
+ pushToolResults(state, message?.['content'], options);
40
+ const text = contentBlockUserText(message?.['content']);
41
+ return text === undefined ? [] : emitUser(state, text);
42
+ }
43
+ if (record['type'] === 'assistant') {
44
+ const content = message?.['content'];
45
+ if (typeof content === 'string')
46
+ pushAssistant(state, content);
47
+ else
48
+ pushContentBlocks(state, arrayField(message, 'content'), options);
49
+ return [];
50
+ }
51
+ // `ai-title`, `last-prompt`, `mode`, and the subagent metadata records are
52
+ // CLI state; head() reads the two that name the session.
53
+ return [];
54
+ },
55
+ flush(state, _options) {
56
+ return flushAssistant(state);
57
+ },
58
+ head(headLines, tailLines) {
59
+ let aiTitle;
60
+ let lastPrompt;
61
+ let cwd;
62
+ let createdAt;
63
+ let firstPrompt;
64
+ // Later wins for the two title records: Qoder rewrites `ai-title` as the
65
+ // session is renamed, so the newest one in the tail is the current name.
66
+ for (const line of [...headLines, ...tailLines]) {
67
+ const record = parseRecord(line);
68
+ if (record === undefined)
69
+ continue;
70
+ if (record['type'] === 'ai-title')
71
+ aiTitle = stringField(record, 'aiTitle') ?? aiTitle;
72
+ else if (record['type'] === 'last-prompt')
73
+ lastPrompt = stringField(record, 'lastPrompt') ?? lastPrompt;
74
+ cwd ??= stringField(record, 'cwd');
75
+ createdAt ??= parseTimestamp(record['timestamp']);
76
+ }
77
+ for (const line of headLines) {
78
+ const record = parseRecord(line);
79
+ if (record?.['type'] !== 'user')
80
+ continue;
81
+ firstPrompt = contentBlockUserText(objectField(record, 'message')?.['content']);
82
+ if (firstPrompt !== undefined)
83
+ break;
84
+ }
85
+ const title = normalizeTitle(aiTitle ?? lastPrompt ?? firstPrompt ?? '');
86
+ return {
87
+ ...title === '' ? {} : { title },
88
+ ...cwd === undefined ? {} : { cwd },
89
+ ...firstPrompt === undefined ? {} : { firstPrompt },
90
+ ...createdAt === undefined ? {} : { createdAt },
91
+ };
92
+ },
93
+ };
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Reasonix sessions: `~/.reasonix/sessions/<stem>.jsonl`.
3
+ *
4
+ * Records are OpenAI chat messages written straight to disk with no envelope
5
+ * — `{role, content}` and nothing wrapping it — which makes this the one
6
+ * supported format whose lines would be valid request payloads as they stand.
7
+ *
8
+ * Two versions are in the wild and both are accepted: v1 nests a tool call
9
+ * under `function: {name, arguments}`, v2 flattens it to `{name, arguments}`.
10
+ * `arguments` is a JSON *string* in both, which the shared renderer handles.
11
+ *
12
+ * Sibling files share the directory — `<stem>.meta.json` holds the workspace
13
+ * and summary, `<stem>.events.jsonl` is a write-ahead log replayed over the
14
+ * checkpoint — and neither is a transcript, so both are rejected by path. The
15
+ * WAL means a session's newest turns can be missing from the file this reads;
16
+ * that is a staleness bound, not a correctness one, and it resolves itself the
17
+ * next time Reasonix checkpoints.
18
+ *
19
+ * @module dsh-reference-anything/local-agent/adapters/reasonix
20
+ */
21
+ import type { TranscriptAdapter } from '../types.ts';
22
+ /** Reads Reasonix session transcripts. */
23
+ export declare const reasonixAdapter: TranscriptAdapter;