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.
- package/LICENSE +22 -22
- package/NOTICE.md +52 -25
- package/README.md +342 -284
- package/cordis.patch.yml +89 -18
- package/lib/client.js +13534 -521
- package/lib/contract.d.ts +155 -3
- package/lib/contract.js +68 -4
- 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 -3
- package/lib/host.js +88 -5
- 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 -0
- 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 -1
- 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 -0
- package/lib/wire.js +30 -1
- 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 +200 -185
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the three database-backed formats have in common.
|
|
3
|
+
*
|
|
4
|
+
* opencode, its fork mimocode, and zcode all store a conversation the same
|
|
5
|
+
* way — a `session` row, `message` rows under it, and `part` rows under those,
|
|
6
|
+
* each part a JSON blob naming its own type. The queries differ (zcode filters
|
|
7
|
+
* to main sessions, and each probes its own optional columns) and the message-level
|
|
8
|
+
* rules differ (zcode drops system prompts, opencode honours a `tail_start_id`),
|
|
9
|
+
* but the part-level projection is identical, and it is where all the fiddly
|
|
10
|
+
* work is. It lives here so one change to how a tool call renders reaches every
|
|
11
|
+
* database format at once.
|
|
12
|
+
*
|
|
13
|
+
* Everything below is pure: it takes a {@link SqliteReader}, never a driver.
|
|
14
|
+
*
|
|
15
|
+
* @module dsh-reference-anything/local-agent/adapters/sqlite-shared
|
|
16
|
+
*/
|
|
17
|
+
import type { AdapterState, ConvertOptions, ParsedTurn, SqliteReader } from '../types.ts';
|
|
18
|
+
/** One message row with the parts that belong to it, in order. */
|
|
19
|
+
export interface SessionMessage {
|
|
20
|
+
readonly id: string;
|
|
21
|
+
/** The message's own JSON payload: `role`, `path.cwd`, `agent`, `summary`… */
|
|
22
|
+
readonly data: Record<string, unknown>;
|
|
23
|
+
/** Its parts' JSON payloads, oldest first. */
|
|
24
|
+
readonly parts: readonly Record<string, unknown>[];
|
|
25
|
+
}
|
|
26
|
+
/** A session's messages, and whether older ones were left behind. */
|
|
27
|
+
export interface SessionMessages {
|
|
28
|
+
readonly messages: readonly SessionMessage[];
|
|
29
|
+
/** The record cap was reached, so the oldest messages were not read. */
|
|
30
|
+
readonly truncated: boolean;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Read one session's messages, newest-anchored and bounded.
|
|
34
|
+
*
|
|
35
|
+
* Bounded from the *end* rather than the start, for the same reason the
|
|
36
|
+
* line-local reader anchors an oversized file to its tail: the newest turns are
|
|
37
|
+
* the ones a reader asking about a conversation almost always wants, and a
|
|
38
|
+
* bound that kept the oldest would return the setup and drop the answer.
|
|
39
|
+
* @param db - a read-only reader over the database.
|
|
40
|
+
* @param sessionId - the conversation to read.
|
|
41
|
+
* @param maxRecords - most message rows to materialize.
|
|
42
|
+
* @returns the messages oldest-first, and whether the cap cut any off.
|
|
43
|
+
*/
|
|
44
|
+
export declare function readSessionMessages(db: SqliteReader, sessionId: string, maxRecords: number): SessionMessages;
|
|
45
|
+
/**
|
|
46
|
+
* The text a user typed, joined from the message's text parts.
|
|
47
|
+
*
|
|
48
|
+
* These agents inject their own material through the same `user` role a person
|
|
49
|
+
* types into — a `<system-reminder>` appended to a real question, a whole
|
|
50
|
+
* `AGENTS.md` wrapped in `<INSTRUCTIONS>` — so the same stripping the JSONL
|
|
51
|
+
* adapters apply is applied here. A message that was nothing but machinery
|
|
52
|
+
* collapses to an empty string and the caller emits no turn for it.
|
|
53
|
+
* @param message - one message row.
|
|
54
|
+
* @param strip - whether to remove harness-injected wrappers.
|
|
55
|
+
* @returns the prompt, or an empty string when the message carried no words.
|
|
56
|
+
*/
|
|
57
|
+
export declare function userPromptText(message: SessionMessage, strip?: boolean): string;
|
|
58
|
+
/**
|
|
59
|
+
* Fold one assistant message's parts into the open assistant run.
|
|
60
|
+
*
|
|
61
|
+
* Structural parts — `step-start`, `step-finish`, `compaction`, `timeline`, and
|
|
62
|
+
* whatever a later build adds — carry nothing a reader can use and are skipped,
|
|
63
|
+
* which is what keeps a new part type from breaking an old adapter.
|
|
64
|
+
* @param state - fold state, mutated in place.
|
|
65
|
+
* @param message - the assistant message whose parts to project.
|
|
66
|
+
* @param options - projection settings.
|
|
67
|
+
*/
|
|
68
|
+
export declare function pushSessionParts(state: AdapterState, message: SessionMessage, options: ConvertOptions): void;
|
|
69
|
+
/**
|
|
70
|
+
* Fold one message into the running conversation.
|
|
71
|
+
* @param state - fold state, mutated in place.
|
|
72
|
+
* @param message - the message to fold.
|
|
73
|
+
* @param options - projection settings.
|
|
74
|
+
* @returns turns this message completed, in order.
|
|
75
|
+
*/
|
|
76
|
+
export declare function stepSessionMessage(state: AdapterState, message: SessionMessage, options: ConvertOptions): readonly ParsedTurn[];
|
|
77
|
+
/**
|
|
78
|
+
* Parse a `data` column, which every one of these schemas stores as JSON text.
|
|
79
|
+
* @param value - the raw column value.
|
|
80
|
+
* @returns the object, or undefined when the column is absent or not a JSON object.
|
|
81
|
+
*/
|
|
82
|
+
export declare function parseBlob(value: unknown): Record<string, unknown> | undefined;
|
|
83
|
+
/** Read a nested string, treating a blank one as absent. */
|
|
84
|
+
export declare function asText(value: unknown): string | undefined;
|
|
85
|
+
/**
|
|
86
|
+
* Read a string field from a nested object, for the `path.cwd` shape both forks use.
|
|
87
|
+
* @param data - the message payload.
|
|
88
|
+
* @param outer - the containing object's key.
|
|
89
|
+
* @param inner - the key inside it.
|
|
90
|
+
* @returns the value, or undefined when either level is missing.
|
|
91
|
+
*/
|
|
92
|
+
export declare function nestedText(data: Record<string, unknown>, outer: string, inner: string): string | undefined;
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the three database-backed formats have in common.
|
|
3
|
+
*
|
|
4
|
+
* opencode, its fork mimocode, and zcode all store a conversation the same
|
|
5
|
+
* way — a `session` row, `message` rows under it, and `part` rows under those,
|
|
6
|
+
* each part a JSON blob naming its own type. The queries differ (zcode filters
|
|
7
|
+
* to main sessions, and each probes its own optional columns) and the message-level
|
|
8
|
+
* rules differ (zcode drops system prompts, opencode honours a `tail_start_id`),
|
|
9
|
+
* but the part-level projection is identical, and it is where all the fiddly
|
|
10
|
+
* work is. It lives here so one change to how a tool call renders reaches every
|
|
11
|
+
* database format at once.
|
|
12
|
+
*
|
|
13
|
+
* Everything below is pure: it takes a {@link SqliteReader}, never a driver.
|
|
14
|
+
*
|
|
15
|
+
* @module dsh-reference-anything/local-agent/adapters/sqlite-shared
|
|
16
|
+
*/
|
|
17
|
+
import { cleanUserText, emitUser, pushAssistant, renderToolCall, renderToolResult } from "./shared.js";
|
|
18
|
+
/**
|
|
19
|
+
* Message ids bound into one `IN (…)` clause.
|
|
20
|
+
*
|
|
21
|
+
* SQLite accepts far more parameters than this, but a bounded chunk keeps the
|
|
22
|
+
* statement cache from being churned by a different arity per session.
|
|
23
|
+
*/
|
|
24
|
+
const ID_CHUNK = 400;
|
|
25
|
+
/**
|
|
26
|
+
* Read one session's messages, newest-anchored and bounded.
|
|
27
|
+
*
|
|
28
|
+
* Bounded from the *end* rather than the start, for the same reason the
|
|
29
|
+
* line-local reader anchors an oversized file to its tail: the newest turns are
|
|
30
|
+
* the ones a reader asking about a conversation almost always wants, and a
|
|
31
|
+
* bound that kept the oldest would return the setup and drop the answer.
|
|
32
|
+
* @param db - a read-only reader over the database.
|
|
33
|
+
* @param sessionId - the conversation to read.
|
|
34
|
+
* @param maxRecords - most message rows to materialize.
|
|
35
|
+
* @returns the messages oldest-first, and whether the cap cut any off.
|
|
36
|
+
*/
|
|
37
|
+
export function readSessionMessages(db, sessionId, maxRecords) {
|
|
38
|
+
const cap = Math.max(1, Math.trunc(maxRecords));
|
|
39
|
+
const rows = db.all('SELECT id, time_created, data FROM message WHERE session_id = ? ORDER BY time_created DESC, id DESC LIMIT ?', sessionId, cap);
|
|
40
|
+
const ordered = [...rows].reverse();
|
|
41
|
+
const ids = ordered.map(row => String(row['id']));
|
|
42
|
+
const parts = readParts(db, ids);
|
|
43
|
+
const messages = [];
|
|
44
|
+
for (const row of ordered) {
|
|
45
|
+
const data = parseBlob(row['data']);
|
|
46
|
+
// A message whose payload is not JSON is dirty data, not a reason to fail
|
|
47
|
+
// the whole read; the conversation around it is still worth returning.
|
|
48
|
+
if (data === undefined)
|
|
49
|
+
continue;
|
|
50
|
+
const id = String(row['id']);
|
|
51
|
+
messages.push({ id, data, parts: parts.get(id) ?? [] });
|
|
52
|
+
}
|
|
53
|
+
return { messages, truncated: rows.length >= cap };
|
|
54
|
+
}
|
|
55
|
+
/** Fetch the parts of exactly the messages that were kept. */
|
|
56
|
+
function readParts(db, ids) {
|
|
57
|
+
const byMessage = new Map();
|
|
58
|
+
for (let at = 0; at < ids.length; at += ID_CHUNK) {
|
|
59
|
+
const chunk = ids.slice(at, at + ID_CHUNK);
|
|
60
|
+
if (chunk.length === 0)
|
|
61
|
+
continue;
|
|
62
|
+
const holes = chunk.map(() => '?').join(',');
|
|
63
|
+
const rows = db.all(`SELECT message_id, data FROM part WHERE message_id IN (${holes}) ORDER BY time_created, id`, ...chunk);
|
|
64
|
+
for (const row of rows) {
|
|
65
|
+
const data = parseBlob(row['data']);
|
|
66
|
+
if (data === undefined)
|
|
67
|
+
continue;
|
|
68
|
+
const key = String(row['message_id']);
|
|
69
|
+
const bucket = byMessage.get(key);
|
|
70
|
+
if (bucket === undefined)
|
|
71
|
+
byMessage.set(key, [data]);
|
|
72
|
+
else
|
|
73
|
+
bucket.push(data);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return byMessage;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* The text a user typed, joined from the message's text parts.
|
|
80
|
+
*
|
|
81
|
+
* These agents inject their own material through the same `user` role a person
|
|
82
|
+
* types into — a `<system-reminder>` appended to a real question, a whole
|
|
83
|
+
* `AGENTS.md` wrapped in `<INSTRUCTIONS>` — so the same stripping the JSONL
|
|
84
|
+
* adapters apply is applied here. A message that was nothing but machinery
|
|
85
|
+
* collapses to an empty string and the caller emits no turn for it.
|
|
86
|
+
* @param message - one message row.
|
|
87
|
+
* @param strip - whether to remove harness-injected wrappers.
|
|
88
|
+
* @returns the prompt, or an empty string when the message carried no words.
|
|
89
|
+
*/
|
|
90
|
+
export function userPromptText(message, strip = true) {
|
|
91
|
+
const texts = [];
|
|
92
|
+
for (const part of message.parts) {
|
|
93
|
+
if (part['type'] !== 'text')
|
|
94
|
+
continue;
|
|
95
|
+
const text = part['text'];
|
|
96
|
+
if (typeof text === 'string' && text.trim() !== '')
|
|
97
|
+
texts.push(text.trim());
|
|
98
|
+
}
|
|
99
|
+
const joined = texts.join('\n');
|
|
100
|
+
return strip ? cleanUserText(joined) : joined;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Fold one assistant message's parts into the open assistant run.
|
|
104
|
+
*
|
|
105
|
+
* Structural parts — `step-start`, `step-finish`, `compaction`, `timeline`, and
|
|
106
|
+
* whatever a later build adds — carry nothing a reader can use and are skipped,
|
|
107
|
+
* which is what keeps a new part type from breaking an old adapter.
|
|
108
|
+
* @param state - fold state, mutated in place.
|
|
109
|
+
* @param message - the assistant message whose parts to project.
|
|
110
|
+
* @param options - projection settings.
|
|
111
|
+
*/
|
|
112
|
+
export function pushSessionParts(state, message, options) {
|
|
113
|
+
for (const part of message.parts) {
|
|
114
|
+
switch (part['type']) {
|
|
115
|
+
case 'text':
|
|
116
|
+
pushAssistant(state, asText(part['text']));
|
|
117
|
+
break;
|
|
118
|
+
case 'reasoning':
|
|
119
|
+
if (options.includeThinking)
|
|
120
|
+
pushAssistant(state, asText(part['text']));
|
|
121
|
+
break;
|
|
122
|
+
case 'tool': {
|
|
123
|
+
const state_ = part['state'];
|
|
124
|
+
const input = typeof state_ === 'object' && state_ !== null
|
|
125
|
+
? state_['input']
|
|
126
|
+
: undefined;
|
|
127
|
+
pushAssistant(state, renderToolCall(part['tool'], input, options.toolCalls, options.toolSummaryChars));
|
|
128
|
+
if (typeof state_ === 'object' && state_ !== null) {
|
|
129
|
+
const result = state_;
|
|
130
|
+
const output = result['output'] ?? result['result'];
|
|
131
|
+
if (output !== undefined)
|
|
132
|
+
pushAssistant(state, renderToolResult(output, options.toolResults, options.toolSummaryChars));
|
|
133
|
+
}
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
case 'file':
|
|
137
|
+
// No attachment handle is emitted: `reference_attachment_read` only
|
|
138
|
+
// serves the web-chat source, so a handle here would be a dead end.
|
|
139
|
+
pushAssistant(state, `[attachment: ${asText(part['filename']) ?? 'unknown'}]`);
|
|
140
|
+
break;
|
|
141
|
+
case 'patch': {
|
|
142
|
+
const files = part['files'];
|
|
143
|
+
pushAssistant(state, `[patch: ${Array.isArray(files) ? files.length : 0} files]`);
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
case 'subtask': {
|
|
147
|
+
const command = asText(part['command']) ?? '';
|
|
148
|
+
const description = asText(part['description']) ?? '';
|
|
149
|
+
const detail = [command, description].filter(entry => entry !== '').join(' — ');
|
|
150
|
+
pushAssistant(state, `[subtask${detail === '' ? '' : `: ${detail}`}]`);
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
default:
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Fold one message into the running conversation.
|
|
160
|
+
* @param state - fold state, mutated in place.
|
|
161
|
+
* @param message - the message to fold.
|
|
162
|
+
* @param options - projection settings.
|
|
163
|
+
* @returns turns this message completed, in order.
|
|
164
|
+
*/
|
|
165
|
+
export function stepSessionMessage(state, message, options) {
|
|
166
|
+
const role = message.data['role'];
|
|
167
|
+
if (role === 'user') {
|
|
168
|
+
const prompt = userPromptText(message, options.stripEnvironmentPreamble);
|
|
169
|
+
return prompt === '' ? [] : emitUser(state, prompt);
|
|
170
|
+
}
|
|
171
|
+
if (role !== 'assistant')
|
|
172
|
+
return [];
|
|
173
|
+
pushSessionParts(state, message, options);
|
|
174
|
+
return [];
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Parse a `data` column, which every one of these schemas stores as JSON text.
|
|
178
|
+
* @param value - the raw column value.
|
|
179
|
+
* @returns the object, or undefined when the column is absent or not a JSON object.
|
|
180
|
+
*/
|
|
181
|
+
export function parseBlob(value) {
|
|
182
|
+
const text = typeof value === 'string'
|
|
183
|
+
? value
|
|
184
|
+
: value instanceof Uint8Array
|
|
185
|
+
? Buffer.from(value).toString('utf8')
|
|
186
|
+
: undefined;
|
|
187
|
+
if (text === undefined)
|
|
188
|
+
return undefined;
|
|
189
|
+
try {
|
|
190
|
+
const parsed = JSON.parse(text);
|
|
191
|
+
return typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)
|
|
192
|
+
? parsed
|
|
193
|
+
: undefined;
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
return undefined;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
/** Read a nested string, treating a blank one as absent. */
|
|
200
|
+
export function asText(value) {
|
|
201
|
+
return typeof value === 'string' && value.trim() !== '' ? value : undefined;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Read a string field from a nested object, for the `path.cwd` shape both forks use.
|
|
205
|
+
* @param data - the message payload.
|
|
206
|
+
* @param outer - the containing object's key.
|
|
207
|
+
* @param inner - the key inside it.
|
|
208
|
+
* @returns the value, or undefined when either level is missing.
|
|
209
|
+
*/
|
|
210
|
+
export function nestedText(data, outer, inner) {
|
|
211
|
+
const nested = data[outer];
|
|
212
|
+
if (typeof nested !== 'object' || nested === null)
|
|
213
|
+
return undefined;
|
|
214
|
+
return asText(nested[inner]);
|
|
215
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reads zcode's SQLite history.
|
|
3
|
+
*
|
|
4
|
+
* zcode (z.ai's CLI) keeps every session in `~/.zcode/cli/db/db.sqlite` with the
|
|
5
|
+
* same three tables opencode uses, which is why the part-level projection is
|
|
6
|
+
* shared. Three rules are its own:
|
|
7
|
+
*
|
|
8
|
+
* - **Subagent sessions are hidden.** A row with a `parent_id` is work zcode
|
|
9
|
+
* spawned for itself; listing it would put a fragment of a conversation in the
|
|
10
|
+
* menu beside the conversation it belongs to.
|
|
11
|
+
* - **System prompts are dropped.** zcode stores them as `role: 'system'`
|
|
12
|
+
* messages in the same table as the conversation.
|
|
13
|
+
* - **Compaction is recorded twice.** A `compaction` part carries the summary,
|
|
14
|
+
* and older builds hang it on the message instead. Either way the carrier is a
|
|
15
|
+
* marker rather than something the user said, so the message is dropped and
|
|
16
|
+
* the summary re-emitted once, ahead of the turns that survived.
|
|
17
|
+
*
|
|
18
|
+
* @module dsh-reference-anything/local-agent/adapters/zcode
|
|
19
|
+
*/
|
|
20
|
+
import type { QueryAdapter } from '../types.ts';
|
|
21
|
+
import type { SessionMessage } from './sqlite-shared.ts';
|
|
22
|
+
/** Reads zcode's session database. */
|
|
23
|
+
export declare const zcodeAdapter: QueryAdapter;
|
|
24
|
+
/**
|
|
25
|
+
* The summary carried by a `compaction` part, if this message has one.
|
|
26
|
+
*
|
|
27
|
+
* The part itself never reaches the reader — `pushSessionParts` skips every
|
|
28
|
+
* structural type — so this is the only thing that recovers it.
|
|
29
|
+
* @param message - one message row with its parts.
|
|
30
|
+
* @returns the summary text, or undefined when no part carried one.
|
|
31
|
+
*/
|
|
32
|
+
export declare function partSummary(message: SessionMessage): string | undefined;
|
|
33
|
+
/**
|
|
34
|
+
* The summary hung on the message itself, which is where older builds put it.
|
|
35
|
+
*
|
|
36
|
+
* A message shaped this way is a marker rather than something the user said:
|
|
37
|
+
* its own body is a lead-in to the summary. The caller drops it.
|
|
38
|
+
* @param message - one message row.
|
|
39
|
+
* @returns the summary text, or undefined when this is ordinary conversation.
|
|
40
|
+
*/
|
|
41
|
+
export declare function messageSummary(message: SessionMessage): string | undefined;
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reads zcode's SQLite history.
|
|
3
|
+
*
|
|
4
|
+
* zcode (z.ai's CLI) keeps every session in `~/.zcode/cli/db/db.sqlite` with the
|
|
5
|
+
* same three tables opencode uses, which is why the part-level projection is
|
|
6
|
+
* shared. Three rules are its own:
|
|
7
|
+
*
|
|
8
|
+
* - **Subagent sessions are hidden.** A row with a `parent_id` is work zcode
|
|
9
|
+
* spawned for itself; listing it would put a fragment of a conversation in the
|
|
10
|
+
* menu beside the conversation it belongs to.
|
|
11
|
+
* - **System prompts are dropped.** zcode stores them as `role: 'system'`
|
|
12
|
+
* messages in the same table as the conversation.
|
|
13
|
+
* - **Compaction is recorded twice.** A `compaction` part carries the summary,
|
|
14
|
+
* and older builds hang it on the message instead. Either way the carrier is a
|
|
15
|
+
* marker rather than something the user said, so the message is dropped and
|
|
16
|
+
* the summary re-emitted once, ahead of the turns that survived.
|
|
17
|
+
*
|
|
18
|
+
* @module dsh-reference-anything/local-agent/adapters/zcode
|
|
19
|
+
*/
|
|
20
|
+
import { joinLocalPath } from "../path.js";
|
|
21
|
+
import { COMPACTION_MARKER, createSharedState, flushAssistant, normalizeTitle, parseTimestamp } from "./shared.js";
|
|
22
|
+
import { readSessionMessages, stepSessionMessage } from "./sqlite-shared.js";
|
|
23
|
+
/**
|
|
24
|
+
* The select list both queries share, with `$recency` standing for whichever
|
|
25
|
+
* timestamp column this build has. Aliased so the projection below reads one
|
|
26
|
+
* column name regardless.
|
|
27
|
+
*/
|
|
28
|
+
const SESSION_FIELDS = 'id, title, directory, $recency AS recency';
|
|
29
|
+
/** Reads zcode's session database. */
|
|
30
|
+
export const zcodeAdapter = {
|
|
31
|
+
kind: 'zcode',
|
|
32
|
+
displayName: 'zcode',
|
|
33
|
+
query: true,
|
|
34
|
+
defaultRoots(home) {
|
|
35
|
+
return [joinLocalPath(home, '.zcode', 'cli', 'db')];
|
|
36
|
+
},
|
|
37
|
+
matches(relativePath) {
|
|
38
|
+
// Exact, so SQLite's `-wal` and `-shm` sidecars are not opened as databases.
|
|
39
|
+
return relativePath.split(/[\\/]/u).at(-1) === 'db.sqlite';
|
|
40
|
+
},
|
|
41
|
+
sessions(db, limit) {
|
|
42
|
+
const columns = db.columns('session');
|
|
43
|
+
if (columns.size === 0)
|
|
44
|
+
return [];
|
|
45
|
+
// A build that predates `parent_id` has no subagent rows to exclude, so the
|
|
46
|
+
// predicate is dropped rather than made into a query error.
|
|
47
|
+
const scope = columns.has('parent_id') ? "WHERE parent_id IS NULL OR parent_id = ''" : '';
|
|
48
|
+
const recency = columns.has('time_updated') ? 'time_updated' : 'time_created';
|
|
49
|
+
const rows = db.all(`SELECT ${SESSION_FIELDS.replace('$recency', recency)} FROM session ${scope} ORDER BY recency DESC, id DESC LIMIT ?`, Math.max(1, Math.trunc(limit)));
|
|
50
|
+
const sessions = [];
|
|
51
|
+
for (const row of rows) {
|
|
52
|
+
const session = toSession(row);
|
|
53
|
+
if (session !== undefined)
|
|
54
|
+
sessions.push(session);
|
|
55
|
+
}
|
|
56
|
+
return sessions;
|
|
57
|
+
},
|
|
58
|
+
session(db, sessionId) {
|
|
59
|
+
const columns = db.columns('session');
|
|
60
|
+
if (columns.size === 0)
|
|
61
|
+
return undefined;
|
|
62
|
+
const recency = columns.has('time_updated') ? 'time_updated' : 'time_created';
|
|
63
|
+
// No `parent_id` predicate here: the subagent filter is about what the menu
|
|
64
|
+
// offers, and a caller holding an id has already got past the menu.
|
|
65
|
+
const [row] = db.all(`SELECT ${SESSION_FIELDS.replace('$recency', recency)} FROM session WHERE id = ? LIMIT 1`, sessionId);
|
|
66
|
+
return row === undefined ? undefined : toSession(row);
|
|
67
|
+
},
|
|
68
|
+
turns(db, sessionId, options, maxRecords) {
|
|
69
|
+
const { messages, truncated } = readSessionMessages(db, sessionId, maxRecords);
|
|
70
|
+
const state = createSharedState();
|
|
71
|
+
const items = [];
|
|
72
|
+
let summary;
|
|
73
|
+
const conversation = [];
|
|
74
|
+
for (const message of messages) {
|
|
75
|
+
if (message.data['role'] === 'system')
|
|
76
|
+
continue;
|
|
77
|
+
// Last compaction wins: an older summary describes history a newer one has
|
|
78
|
+
// already folded into itself.
|
|
79
|
+
const onPart = partSummary(message);
|
|
80
|
+
if (onPart !== undefined)
|
|
81
|
+
summary = onPart;
|
|
82
|
+
const onMessage = messageSummary(message);
|
|
83
|
+
if (onMessage !== undefined) {
|
|
84
|
+
// The older shape puts the summary on a message whose own body is just
|
|
85
|
+
// a lead-in, so the whole message goes; the part shape marks up a
|
|
86
|
+
// message that still carries conversation, and only the part goes.
|
|
87
|
+
summary = onMessage;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
conversation.push(message);
|
|
91
|
+
}
|
|
92
|
+
if (summary !== undefined)
|
|
93
|
+
items.push({ role: 'assistant', text: `${COMPACTION_MARKER}\n\n${summary}` });
|
|
94
|
+
for (const message of conversation)
|
|
95
|
+
items.push(...stepSessionMessage(state, message, options));
|
|
96
|
+
items.push(...flushAssistant(state));
|
|
97
|
+
return { items, truncated, compacted: summary !== undefined };
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
/**
|
|
101
|
+
* Project one `session` row.
|
|
102
|
+
*
|
|
103
|
+
* zcode records a single timestamp per session in the column this build has, so
|
|
104
|
+
* a conversation's start and its last activity are the same instant here.
|
|
105
|
+
* @param row - the row as read, with its timestamp aliased to `recency`.
|
|
106
|
+
* @returns the session, or undefined when the row has no usable id.
|
|
107
|
+
*/
|
|
108
|
+
function toSession(row) {
|
|
109
|
+
const id = typeof row['id'] === 'string' ? row['id'] : String(row['id'] ?? '');
|
|
110
|
+
if (id === '')
|
|
111
|
+
return undefined;
|
|
112
|
+
const at = parseTimestamp(row['recency']) ?? 0;
|
|
113
|
+
return {
|
|
114
|
+
id,
|
|
115
|
+
title: normalizeTitle(row['title']),
|
|
116
|
+
cwd: typeof row['directory'] === 'string' ? row['directory'] : '',
|
|
117
|
+
createdAt: at,
|
|
118
|
+
updatedAt: at,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* The summary carried by a `compaction` part, if this message has one.
|
|
123
|
+
*
|
|
124
|
+
* The part itself never reaches the reader — `pushSessionParts` skips every
|
|
125
|
+
* structural type — so this is the only thing that recovers it.
|
|
126
|
+
* @param message - one message row with its parts.
|
|
127
|
+
* @returns the summary text, or undefined when no part carried one.
|
|
128
|
+
*/
|
|
129
|
+
export function partSummary(message) {
|
|
130
|
+
for (const part of message.parts) {
|
|
131
|
+
if (part['type'] !== 'compaction')
|
|
132
|
+
continue;
|
|
133
|
+
const body = summaryBody(part['summary']);
|
|
134
|
+
if (body !== undefined)
|
|
135
|
+
return body;
|
|
136
|
+
}
|
|
137
|
+
return undefined;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* The summary hung on the message itself, which is where older builds put it.
|
|
141
|
+
*
|
|
142
|
+
* A message shaped this way is a marker rather than something the user said:
|
|
143
|
+
* its own body is a lead-in to the summary. The caller drops it.
|
|
144
|
+
* @param message - one message row.
|
|
145
|
+
* @returns the summary text, or undefined when this is ordinary conversation.
|
|
146
|
+
*/
|
|
147
|
+
export function messageSummary(message) {
|
|
148
|
+
return summaryBody(message.data['summary']);
|
|
149
|
+
}
|
|
150
|
+
/** Read `summary.body`, which is where both shapes keep the text. */
|
|
151
|
+
function summaryBody(summary) {
|
|
152
|
+
if (typeof summary !== 'object' || summary === null)
|
|
153
|
+
return undefined;
|
|
154
|
+
const body = summary['body'];
|
|
155
|
+
if (typeof body !== 'string')
|
|
156
|
+
return undefined;
|
|
157
|
+
const trimmed = body.trim();
|
|
158
|
+
return trimmed === '' ? undefined : trimmed;
|
|
159
|
+
}
|