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,85 @@
|
|
|
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 { joinLocalPath } from "../path.js";
|
|
18
|
+
import { arrayField, contentBlockUserText, createSharedState, emitUser, flushAssistant, normalizeTitle, objectField, parseRecord, parseTimestamp, pushAssistant, pushContentBlocks, pushToolResults, stringField, } from "./shared.js";
|
|
19
|
+
/** Reads Hermes session exports. */
|
|
20
|
+
export const hermesAdapter = {
|
|
21
|
+
kind: 'hermes',
|
|
22
|
+
displayName: 'Hermes',
|
|
23
|
+
defaultRoots(home) {
|
|
24
|
+
return [joinLocalPath(home, '.hermes', 'sessions')];
|
|
25
|
+
},
|
|
26
|
+
matches(relativePath) {
|
|
27
|
+
return relativePath.toLowerCase().endsWith('.jsonl');
|
|
28
|
+
},
|
|
29
|
+
createState() {
|
|
30
|
+
return createSharedState();
|
|
31
|
+
},
|
|
32
|
+
step(line, state, options) {
|
|
33
|
+
const record = parseRecord(line);
|
|
34
|
+
if (record === undefined)
|
|
35
|
+
return [];
|
|
36
|
+
// A `session` record is metadata; head() reads it and the fold ignores it.
|
|
37
|
+
if (record['type'] === 'session' || record['type'] === 'init')
|
|
38
|
+
return [];
|
|
39
|
+
const message = objectField(record, 'message') ?? record;
|
|
40
|
+
if (message['role'] === 'user') {
|
|
41
|
+
pushToolResults(state, message['content'], options);
|
|
42
|
+
const text = contentBlockUserText(message['content']);
|
|
43
|
+
return text === undefined ? [] : emitUser(state, text);
|
|
44
|
+
}
|
|
45
|
+
if (message['role'] !== 'assistant')
|
|
46
|
+
return [];
|
|
47
|
+
const content = message['content'];
|
|
48
|
+
if (typeof content === 'string')
|
|
49
|
+
pushAssistant(state, content);
|
|
50
|
+
else
|
|
51
|
+
pushContentBlocks(state, arrayField(message, 'content'), options);
|
|
52
|
+
return [];
|
|
53
|
+
},
|
|
54
|
+
flush(state, _options) {
|
|
55
|
+
return flushAssistant(state);
|
|
56
|
+
},
|
|
57
|
+
head(headLines, _tailLines) {
|
|
58
|
+
let recorded;
|
|
59
|
+
let cwd;
|
|
60
|
+
let createdAt;
|
|
61
|
+
let firstPrompt;
|
|
62
|
+
for (const line of headLines) {
|
|
63
|
+
const record = parseRecord(line);
|
|
64
|
+
if (record === undefined)
|
|
65
|
+
continue;
|
|
66
|
+
// First non-empty wins for metadata: Hermes may repeat the session
|
|
67
|
+
// record on resume, and the original values are the session's.
|
|
68
|
+
recorded ??= stringField(record, 'title');
|
|
69
|
+
cwd ??= stringField(record, 'cwd');
|
|
70
|
+
createdAt ??= parseTimestamp(record['timestamp'] ?? record['ts']);
|
|
71
|
+
if (firstPrompt !== undefined)
|
|
72
|
+
continue;
|
|
73
|
+
const message = objectField(record, 'message') ?? record;
|
|
74
|
+
if (message['role'] === 'user')
|
|
75
|
+
firstPrompt = contentBlockUserText(message['content']);
|
|
76
|
+
}
|
|
77
|
+
const title = normalizeTitle(recorded ?? firstPrompt ?? '');
|
|
78
|
+
return {
|
|
79
|
+
...title === '' ? {} : { title },
|
|
80
|
+
...cwd === undefined ? {} : { cwd },
|
|
81
|
+
...firstPrompt === undefined ? {} : { firstPrompt },
|
|
82
|
+
...createdAt === undefined ? {} : { createdAt },
|
|
83
|
+
};
|
|
84
|
+
},
|
|
85
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kimi CLI and Kimi Code: `wire.jsonl` under
|
|
3
|
+
* `~/.kimi/sessions/<workdir-md5>/<session>/` (older) or
|
|
4
|
+
* `~/.kimi-code/sessions/<workspace>/<session>/agents/main/` (newer).
|
|
5
|
+
*
|
|
6
|
+
* Both generations are read here because they are the same product and a user
|
|
7
|
+
* upgrading mid-project leaves both on disk. They are told apart per record:
|
|
8
|
+
* the old wire wraps everything as `{timestamp, message: {type, payload}}`
|
|
9
|
+
* with PascalCase event names, the new one writes `{type, time, …}` flat with
|
|
10
|
+
* dotted ones.
|
|
11
|
+
*
|
|
12
|
+
* What makes Kimi different from every other supported format is that its
|
|
13
|
+
* assistant content is a *stream*: `TextPart` and `content.part` records each
|
|
14
|
+
* carry a fragment of one sentence, split wherever the model's tokens landed.
|
|
15
|
+
* They must be concatenated, not joined — the blank line the shared buffer
|
|
16
|
+
* puts between fragments would fall in the middle of a word. Hence the
|
|
17
|
+
* chunk-accumulating helpers below, which are this adapter's only real logic.
|
|
18
|
+
*
|
|
19
|
+
* A spawned subagent gets its own `wire.jsonl` under `agents/<name>/`, and the
|
|
20
|
+
* main thread additionally mirrors its events as `SubagentEvent`. Listing both
|
|
21
|
+
* would duplicate the work, so only `agents/main/` is accepted from the newer
|
|
22
|
+
* layout and the mirrored events are skipped.
|
|
23
|
+
*
|
|
24
|
+
* @module dsh-reference-anything/local-agent/adapters/kimi
|
|
25
|
+
*/
|
|
26
|
+
import type { TranscriptAdapter } from '../types.ts';
|
|
27
|
+
/** Reads Kimi CLI and Kimi Code wire logs. */
|
|
28
|
+
export declare const kimiAdapter: TranscriptAdapter;
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kimi CLI and Kimi Code: `wire.jsonl` under
|
|
3
|
+
* `~/.kimi/sessions/<workdir-md5>/<session>/` (older) or
|
|
4
|
+
* `~/.kimi-code/sessions/<workspace>/<session>/agents/main/` (newer).
|
|
5
|
+
*
|
|
6
|
+
* Both generations are read here because they are the same product and a user
|
|
7
|
+
* upgrading mid-project leaves both on disk. They are told apart per record:
|
|
8
|
+
* the old wire wraps everything as `{timestamp, message: {type, payload}}`
|
|
9
|
+
* with PascalCase event names, the new one writes `{type, time, …}` flat with
|
|
10
|
+
* dotted ones.
|
|
11
|
+
*
|
|
12
|
+
* What makes Kimi different from every other supported format is that its
|
|
13
|
+
* assistant content is a *stream*: `TextPart` and `content.part` records each
|
|
14
|
+
* carry a fragment of one sentence, split wherever the model's tokens landed.
|
|
15
|
+
* They must be concatenated, not joined — the blank line the shared buffer
|
|
16
|
+
* puts between fragments would fall in the middle of a word. Hence the
|
|
17
|
+
* chunk-accumulating helpers below, which are this adapter's only real logic.
|
|
18
|
+
*
|
|
19
|
+
* A spawned subagent gets its own `wire.jsonl` under `agents/<name>/`, and the
|
|
20
|
+
* main thread additionally mirrors its events as `SubagentEvent`. Listing both
|
|
21
|
+
* would duplicate the work, so only `agents/main/` is accepted from the newer
|
|
22
|
+
* layout and the mirrored events are skipped.
|
|
23
|
+
*
|
|
24
|
+
* @module dsh-reference-anything/local-agent/adapters/kimi
|
|
25
|
+
*/
|
|
26
|
+
import { joinLocalPath } from "../path.js";
|
|
27
|
+
import { blankToUndefined, createSharedState, emitUser, flushAssistant, normalizeTitle, objectField, parseRecord, parseTimestamp, pushAssistant, renderToolCall, renderToolResult, } from "./shared.js";
|
|
28
|
+
/** Marks the tail of `state.pending` as an open run of streamed text. */
|
|
29
|
+
const OPEN_TEXT = 'kimi:text';
|
|
30
|
+
/** Marks the tail of `state.pending` as an open run of streamed reasoning. */
|
|
31
|
+
const OPEN_THINK = 'kimi:think';
|
|
32
|
+
/** Reads Kimi CLI and Kimi Code wire logs. */
|
|
33
|
+
export const kimiAdapter = {
|
|
34
|
+
kind: 'kimi',
|
|
35
|
+
displayName: 'Kimi',
|
|
36
|
+
defaultRoots(home) {
|
|
37
|
+
return [joinLocalPath(home, '.kimi', 'sessions'), joinLocalPath(home, '.kimi-code', 'sessions')];
|
|
38
|
+
},
|
|
39
|
+
matches(relativePath) {
|
|
40
|
+
const path = relativePath.toLowerCase();
|
|
41
|
+
if (!path.endsWith('/wire.jsonl') && path !== 'wire.jsonl')
|
|
42
|
+
return false;
|
|
43
|
+
// The newer layout files every agent under `agents/<name>/`; only the main
|
|
44
|
+
// thread is a session of its own.
|
|
45
|
+
return !path.includes('/agents/') || path.includes('/agents/main/');
|
|
46
|
+
},
|
|
47
|
+
createState() {
|
|
48
|
+
return createSharedState();
|
|
49
|
+
},
|
|
50
|
+
step(line, state, options) {
|
|
51
|
+
const record = parseRecord(line);
|
|
52
|
+
if (record === undefined)
|
|
53
|
+
return [];
|
|
54
|
+
const legacy = objectField(record, 'message');
|
|
55
|
+
// The old wire's `message` is itself an event envelope; the new wire's is
|
|
56
|
+
// an ordinary chat message. The presence of a `type` inside it decides.
|
|
57
|
+
return typeof legacy?.['type'] === 'string'
|
|
58
|
+
? stepLegacy(legacy, state, options)
|
|
59
|
+
: stepModern(record, state, options);
|
|
60
|
+
},
|
|
61
|
+
flush(state, _options) {
|
|
62
|
+
closeRun(state);
|
|
63
|
+
return flushAssistant(state);
|
|
64
|
+
},
|
|
65
|
+
head(headLines, _tailLines) {
|
|
66
|
+
let createdAt;
|
|
67
|
+
let firstPrompt;
|
|
68
|
+
for (const line of headLines) {
|
|
69
|
+
const record = parseRecord(line);
|
|
70
|
+
if (record === undefined)
|
|
71
|
+
continue;
|
|
72
|
+
createdAt ??= parseTimestamp(record['time'] ?? record['created_at'] ?? record['timestamp']);
|
|
73
|
+
if (firstPrompt !== undefined)
|
|
74
|
+
continue;
|
|
75
|
+
const legacy = objectField(record, 'message');
|
|
76
|
+
if (typeof legacy?.['type'] === 'string') {
|
|
77
|
+
if (legacy['type'] !== 'TurnBegin' && legacy['type'] !== 'SteerInput')
|
|
78
|
+
continue;
|
|
79
|
+
firstPrompt = blankToUndefined(partsText(objectField(legacy, 'payload')?.['user_input']));
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (record['type'] === 'turn.prompt') {
|
|
83
|
+
firstPrompt = blankToUndefined(partsText(record['input']));
|
|
84
|
+
}
|
|
85
|
+
else if (record['type'] === 'context.append_message') {
|
|
86
|
+
const message = objectField(record, 'message');
|
|
87
|
+
if (message?.['role'] === 'user')
|
|
88
|
+
firstPrompt = blankToUndefined(partsText(message['content']));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// Kimi keeps a renamed session's title in the sibling `state.json`, which a
|
|
92
|
+
// probe of this file cannot reach, so the opening prompt is the name.
|
|
93
|
+
const title = normalizeTitle(firstPrompt ?? '');
|
|
94
|
+
return {
|
|
95
|
+
...title === '' ? {} : { title },
|
|
96
|
+
...firstPrompt === undefined ? {} : { firstPrompt },
|
|
97
|
+
...createdAt === undefined ? {} : { createdAt },
|
|
98
|
+
};
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
/** Fold one event from the older `{timestamp, message: {type, payload}}` wire. */
|
|
102
|
+
function stepLegacy(message, state, options) {
|
|
103
|
+
const payload = objectField(message, 'payload') ?? {};
|
|
104
|
+
switch (message['type']) {
|
|
105
|
+
case 'TurnBegin':
|
|
106
|
+
case 'SteerInput': {
|
|
107
|
+
// `SteerInput` is text typed while the agent was already working. It is
|
|
108
|
+
// still the user speaking, so it opens a turn like any other prompt.
|
|
109
|
+
const text = blankToUndefined(partsText(payload['user_input']));
|
|
110
|
+
if (text === undefined)
|
|
111
|
+
return [];
|
|
112
|
+
closeRun(state);
|
|
113
|
+
return emitUser(state, text);
|
|
114
|
+
}
|
|
115
|
+
case 'TextPart':
|
|
116
|
+
appendChunk(state, OPEN_TEXT, payload['text']);
|
|
117
|
+
return [];
|
|
118
|
+
case 'ThinkPart':
|
|
119
|
+
if (options.includeThinking)
|
|
120
|
+
appendChunk(state, OPEN_THINK, payload['think']);
|
|
121
|
+
return [];
|
|
122
|
+
case 'ToolCall': {
|
|
123
|
+
closeRun(state);
|
|
124
|
+
const fn = objectField(payload, 'function') ?? {};
|
|
125
|
+
pushAssistant(state, renderToolCall(fn['name'], fn['arguments'], options.toolCalls, options.toolSummaryChars));
|
|
126
|
+
return [];
|
|
127
|
+
}
|
|
128
|
+
case 'ToolResult':
|
|
129
|
+
closeRun(state);
|
|
130
|
+
pushAssistant(state, renderToolResult(payload['result'] ?? payload['output'] ?? payload['content'], options.toolResults, options.toolSummaryChars));
|
|
131
|
+
return [];
|
|
132
|
+
case 'StepBegin':
|
|
133
|
+
case 'TurnEnd':
|
|
134
|
+
// A step boundary ends the streamed run without ending the turn: the
|
|
135
|
+
// next fragment starts a new paragraph rather than the same sentence.
|
|
136
|
+
closeRun(state);
|
|
137
|
+
return [];
|
|
138
|
+
default:
|
|
139
|
+
// `ToolCallPart` restates arguments the final `ToolCall` already carries,
|
|
140
|
+
// `SubagentEvent` mirrors another file, and
|
|
141
|
+
// the status events carry no conversation at all.
|
|
142
|
+
return [];
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/** Fold one event from the newer flat `{type, time, …}` wire. */
|
|
146
|
+
function stepModern(record, state, options) {
|
|
147
|
+
switch (record['type']) {
|
|
148
|
+
case 'turn.prompt': {
|
|
149
|
+
const text = blankToUndefined(partsText(record['input']));
|
|
150
|
+
if (text === undefined)
|
|
151
|
+
return [];
|
|
152
|
+
state.seen.add('kimi:prompted');
|
|
153
|
+
closeRun(state);
|
|
154
|
+
return emitUser(state, text);
|
|
155
|
+
}
|
|
156
|
+
case 'context.append_message': {
|
|
157
|
+
// The same prompt is also appended to the context, so this is a fallback
|
|
158
|
+
// for transcripts that carry no `turn.prompt` — taking both would double
|
|
159
|
+
// every user turn.
|
|
160
|
+
if (state.seen.has('kimi:prompted'))
|
|
161
|
+
return [];
|
|
162
|
+
const message = objectField(record, 'message');
|
|
163
|
+
if (message?.['role'] !== 'user')
|
|
164
|
+
return [];
|
|
165
|
+
const text = blankToUndefined(partsText(message['content']));
|
|
166
|
+
if (text === undefined)
|
|
167
|
+
return [];
|
|
168
|
+
closeRun(state);
|
|
169
|
+
return emitUser(state, text);
|
|
170
|
+
}
|
|
171
|
+
case 'context.append_loop_event':
|
|
172
|
+
return stepLoopEvent(objectField(record, 'event') ?? {}, state, options);
|
|
173
|
+
case 'turn.ended':
|
|
174
|
+
closeRun(state);
|
|
175
|
+
return [];
|
|
176
|
+
default:
|
|
177
|
+
return [];
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
/** Fold one `context.append_loop_event`, where the newer wire keeps assistant content. */
|
|
181
|
+
function stepLoopEvent(event, state, options) {
|
|
182
|
+
switch (event['type']) {
|
|
183
|
+
case 'content.part': {
|
|
184
|
+
const part = objectField(event, 'part') ?? {};
|
|
185
|
+
if (part['type'] === 'text')
|
|
186
|
+
appendChunk(state, OPEN_TEXT, part['text']);
|
|
187
|
+
else if (part['type'] === 'think' && options.includeThinking)
|
|
188
|
+
appendChunk(state, OPEN_THINK, part['think']);
|
|
189
|
+
return [];
|
|
190
|
+
}
|
|
191
|
+
case 'tool.call':
|
|
192
|
+
closeRun(state);
|
|
193
|
+
pushAssistant(state, renderToolCall(event['name'], event['args'], options.toolCalls, options.toolSummaryChars));
|
|
194
|
+
return [];
|
|
195
|
+
case 'tool.result':
|
|
196
|
+
closeRun(state);
|
|
197
|
+
pushAssistant(state, renderToolResult(event['output'] ?? event['result'] ?? event['content'], options.toolResults, options.toolSummaryChars));
|
|
198
|
+
return [];
|
|
199
|
+
case 'step.begin':
|
|
200
|
+
case 'step.end':
|
|
201
|
+
closeRun(state);
|
|
202
|
+
return [];
|
|
203
|
+
default:
|
|
204
|
+
// Everything else is internal bookkeeping.
|
|
205
|
+
return [];
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Concatenate one streamed fragment onto the run of the same kind.
|
|
210
|
+
*
|
|
211
|
+
* Appends in place when a run of this kind is already open, because the
|
|
212
|
+
* fragments are halves of one sentence. Anything else — a different kind, a
|
|
213
|
+
* tool call, a step boundary — closes the run first, so the shared buffer's
|
|
214
|
+
* blank-line join only ever falls between things that really are separate.
|
|
215
|
+
* @param state - fold state, mutated in place.
|
|
216
|
+
* @param kind - which run this fragment belongs to.
|
|
217
|
+
* @param value - the fragment, of unknown type.
|
|
218
|
+
*/
|
|
219
|
+
function appendChunk(state, kind, value) {
|
|
220
|
+
if (typeof value !== 'string' || value === '')
|
|
221
|
+
return;
|
|
222
|
+
const last = state.pending.length - 1;
|
|
223
|
+
if (last >= 0 && state.seen.has(kind)) {
|
|
224
|
+
state.pending[last] = `${state.pending[last] ?? ''}${value}`;
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
closeRun(state);
|
|
228
|
+
state.pending.push(value);
|
|
229
|
+
state.seen.add(kind);
|
|
230
|
+
}
|
|
231
|
+
/** End any open streamed run, so the next fragment starts its own entry. */
|
|
232
|
+
function closeRun(state) {
|
|
233
|
+
state.seen.delete(OPEN_TEXT);
|
|
234
|
+
state.seen.delete(OPEN_THINK);
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Flatten a Kimi `ContentPart` value to text.
|
|
238
|
+
*
|
|
239
|
+
* A prompt is a string on some builds and an array of `{text}` parts on
|
|
240
|
+
* others. Parts are joined without a separator: they are fragments of one
|
|
241
|
+
* message, not a list of messages.
|
|
242
|
+
* @param input - the raw value, of unknown shape.
|
|
243
|
+
* @returns the flattened text; empty when nothing readable was found.
|
|
244
|
+
*/
|
|
245
|
+
function partsText(input) {
|
|
246
|
+
if (typeof input === 'string')
|
|
247
|
+
return input;
|
|
248
|
+
if (!Array.isArray(input))
|
|
249
|
+
return '';
|
|
250
|
+
const parts = [];
|
|
251
|
+
for (const part of input) {
|
|
252
|
+
if (typeof part === 'string')
|
|
253
|
+
parts.push(part);
|
|
254
|
+
else if (typeof part === 'object' && part !== null) {
|
|
255
|
+
const text = part['text'];
|
|
256
|
+
if (typeof text === 'string')
|
|
257
|
+
parts.push(text);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return parts.join('');
|
|
261
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenClaw sessions: `~/.openclaw/agents/<agent>/sessions/<id>.jsonl`.
|
|
3
|
+
*
|
|
4
|
+
* An event stream of two shapes: a `session` record carrying the id, `cwd`,
|
|
5
|
+
* and start time, then `message` records wrapping a Messages-API message whose
|
|
6
|
+
* content is either a string or a block array.
|
|
7
|
+
*
|
|
8
|
+
* The detail that needs handling is OpenClaw's gateway, which appends
|
|
9
|
+
* `\n[message_id: …]` to text it relays. It is routing metadata, and left in
|
|
10
|
+
* place it reads as part of what was said, so it is stripped from every body.
|
|
11
|
+
*
|
|
12
|
+
* OpenClaw also keeps the session's display name in a sibling `sessions.json`
|
|
13
|
+
* index rather than in the transcript. A pure adapter cannot open it, so the
|
|
14
|
+
* title falls to the opening prompt — which is what OpenClaw itself shows for
|
|
15
|
+
* an unnamed session anyway.
|
|
16
|
+
*
|
|
17
|
+
* @module dsh-reference-anything/local-agent/adapters/openclaw
|
|
18
|
+
*/
|
|
19
|
+
import type { TranscriptAdapter } from '../types.ts';
|
|
20
|
+
/** Reads OpenClaw session transcripts. */
|
|
21
|
+
export declare const openclawAdapter: TranscriptAdapter;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenClaw sessions: `~/.openclaw/agents/<agent>/sessions/<id>.jsonl`.
|
|
3
|
+
*
|
|
4
|
+
* An event stream of two shapes: a `session` record carrying the id, `cwd`,
|
|
5
|
+
* and start time, then `message` records wrapping a Messages-API message whose
|
|
6
|
+
* content is either a string or a block array.
|
|
7
|
+
*
|
|
8
|
+
* The detail that needs handling is OpenClaw's gateway, which appends
|
|
9
|
+
* `\n[message_id: …]` to text it relays. It is routing metadata, and left in
|
|
10
|
+
* place it reads as part of what was said, so it is stripped from every body.
|
|
11
|
+
*
|
|
12
|
+
* OpenClaw also keeps the session's display name in a sibling `sessions.json`
|
|
13
|
+
* index rather than in the transcript. A pure adapter cannot open it, so the
|
|
14
|
+
* title falls to the opening prompt — which is what OpenClaw itself shows for
|
|
15
|
+
* an unnamed session anyway.
|
|
16
|
+
*
|
|
17
|
+
* @module dsh-reference-anything/local-agent/adapters/openclaw
|
|
18
|
+
*/
|
|
19
|
+
import { joinLocalPath } from "../path.js";
|
|
20
|
+
import { arrayField, contentBlockUserText, createSharedState, emitUser, flushAssistant, normalizeTitle, objectField, parseRecord, parseTimestamp, pushAssistant, pushContentBlocks, renderToolResult, stringField, } from "./shared.js";
|
|
21
|
+
/** Routing metadata the gateway appends to relayed text. */
|
|
22
|
+
const MESSAGE_ID_SUFFIX = '\n[message_id:';
|
|
23
|
+
/** Reads OpenClaw session transcripts. */
|
|
24
|
+
export const openclawAdapter = {
|
|
25
|
+
kind: 'openclaw',
|
|
26
|
+
displayName: 'OpenClaw',
|
|
27
|
+
defaultRoots(home) {
|
|
28
|
+
return [joinLocalPath(home, '.openclaw', 'agents')];
|
|
29
|
+
},
|
|
30
|
+
matches(relativePath) {
|
|
31
|
+
const path = relativePath.toLowerCase();
|
|
32
|
+
return path.includes('/sessions/') && path.endsWith('.jsonl');
|
|
33
|
+
},
|
|
34
|
+
createState() {
|
|
35
|
+
return createSharedState();
|
|
36
|
+
},
|
|
37
|
+
step(line, state, options) {
|
|
38
|
+
const record = parseRecord(line);
|
|
39
|
+
if (record === undefined || record['type'] !== 'message')
|
|
40
|
+
return [];
|
|
41
|
+
const message = objectField(record, 'message');
|
|
42
|
+
if (message === undefined)
|
|
43
|
+
return [];
|
|
44
|
+
const content = message['content'];
|
|
45
|
+
if (message['role'] === 'user') {
|
|
46
|
+
const text = contentBlockUserText(content);
|
|
47
|
+
return text === undefined ? [] : emitUser(state, stripMessageId(text));
|
|
48
|
+
}
|
|
49
|
+
if (message['role'] === 'toolResult') {
|
|
50
|
+
pushAssistant(state, renderToolResult(content, options.toolResults, options.toolSummaryChars));
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
if (message['role'] !== 'assistant')
|
|
54
|
+
return [];
|
|
55
|
+
if (typeof content === 'string')
|
|
56
|
+
pushAssistant(state, stripMessageId(content));
|
|
57
|
+
else
|
|
58
|
+
pushContentBlocks(state, arrayField(message, 'content'), options);
|
|
59
|
+
return [];
|
|
60
|
+
},
|
|
61
|
+
flush(state, _options) {
|
|
62
|
+
return flushAssistant(state);
|
|
63
|
+
},
|
|
64
|
+
head(headLines, _tailLines) {
|
|
65
|
+
let cwd;
|
|
66
|
+
let createdAt;
|
|
67
|
+
let firstPrompt;
|
|
68
|
+
for (const line of headLines) {
|
|
69
|
+
const record = parseRecord(line);
|
|
70
|
+
if (record === undefined)
|
|
71
|
+
continue;
|
|
72
|
+
if (record['type'] === 'session') {
|
|
73
|
+
cwd ??= stringField(record, 'cwd');
|
|
74
|
+
createdAt ??= parseTimestamp(record['timestamp']);
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (record['type'] !== 'message' || firstPrompt !== undefined)
|
|
78
|
+
continue;
|
|
79
|
+
const message = objectField(record, 'message');
|
|
80
|
+
if (message?.['role'] !== 'user')
|
|
81
|
+
continue;
|
|
82
|
+
createdAt ??= parseTimestamp(record['timestamp']);
|
|
83
|
+
const text = contentBlockUserText(message['content']);
|
|
84
|
+
if (text !== undefined)
|
|
85
|
+
firstPrompt = stripMessageId(text);
|
|
86
|
+
}
|
|
87
|
+
const title = normalizeTitle(firstPrompt ?? basename(cwd) ?? '');
|
|
88
|
+
return {
|
|
89
|
+
...title === '' ? {} : { title },
|
|
90
|
+
...cwd === undefined ? {} : { cwd },
|
|
91
|
+
...firstPrompt === undefined ? {} : { firstPrompt },
|
|
92
|
+
...createdAt === undefined ? {} : { createdAt },
|
|
93
|
+
};
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
/**
|
|
97
|
+
* Drop the gateway's trailing `[message_id: …]` marker.
|
|
98
|
+
* @param text - one message body.
|
|
99
|
+
* @returns the body without the marker, or unchanged when it carried none.
|
|
100
|
+
*/
|
|
101
|
+
function stripMessageId(text) {
|
|
102
|
+
const at = text.lastIndexOf(MESSAGE_ID_SUFFIX);
|
|
103
|
+
return at === -1 ? text : text.slice(0, at).trimEnd();
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* The last segment of a recorded path, as a last-resort session name.
|
|
107
|
+
*
|
|
108
|
+
* Deliberately not `node:path`'s: the transcript may have been written on a
|
|
109
|
+
* different platform than the one reading it, so both separators are honoured
|
|
110
|
+
* regardless of which one this host uses.
|
|
111
|
+
* @param path - the recorded `cwd`, if there was one.
|
|
112
|
+
* @returns the final segment, or undefined when there is nothing to name.
|
|
113
|
+
*/
|
|
114
|
+
function basename(path) {
|
|
115
|
+
if (path === undefined)
|
|
116
|
+
return undefined;
|
|
117
|
+
const trimmed = path.replace(/[\\/]+$/u, '');
|
|
118
|
+
const segment = trimmed.split(/[\\/]/u).pop();
|
|
119
|
+
return segment === undefined || segment === '' ? undefined : segment;
|
|
120
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
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 type { QueryAdapter, SqliteReader } from '../types.ts';
|
|
21
|
+
/** Reads opencode's session database. */
|
|
22
|
+
export declare const opencodeAdapter: QueryAdapter;
|
|
23
|
+
/** Reads mimocode's session database, minus its own background traffic. */
|
|
24
|
+
export declare const mimocodeAdapter: QueryAdapter;
|
|
25
|
+
/**
|
|
26
|
+
* Whether a session is MiMo's own background work rather than the user's.
|
|
27
|
+
*
|
|
28
|
+
* Exported for the tests, which is the only way to pin a filter whose whole job
|
|
29
|
+
* is to be invisible when it works.
|
|
30
|
+
* @param title - the session's recorded title.
|
|
31
|
+
* @returns whether discovery should hide it.
|
|
32
|
+
*/
|
|
33
|
+
export declare function isBackgroundSession(title: string): boolean;
|
|
34
|
+
/**
|
|
35
|
+
* The working directory a session ran in, when the session row did not record one.
|
|
36
|
+
*
|
|
37
|
+
* Older opencode databases leave `session.directory` empty and put the path on
|
|
38
|
+
* each message instead, so this is the fallback discovery uses before deciding
|
|
39
|
+
* a session belongs to no workspace.
|
|
40
|
+
* @param db - a read-only reader over the database.
|
|
41
|
+
* @param sessionId - the conversation to inspect.
|
|
42
|
+
* @returns the recorded cwd, or undefined when no message carries one.
|
|
43
|
+
*/
|
|
44
|
+
export declare function sessionCwd(db: SqliteReader, sessionId: string): string | undefined;
|