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,101 @@
1
+ /**
2
+ * Which drives this package can talk to, and how a reference names one.
3
+ *
4
+ * Kept pure — no cordis, no `node:fs` — so the routing rules can be tested
5
+ * without mounting a plugin or reaching a network.
6
+ *
7
+ * @module dsh-reference-anything/cloud-drive/registry
8
+ */
9
+ import { ReferenceAnythingError } from "../../errors.js";
10
+ import { OpenListDriveProvider } from "./providers/openlist.js";
11
+ /** Every drive the package supports. */
12
+ export const DRIVE_KINDS = ['openlist'];
13
+ /**
14
+ * Constructors for the drives that actually work.
15
+ *
16
+ * `Partial` keeps startup validation explicit if a future declared drive has no
17
+ * implementation.
18
+ */
19
+ export const DRIVE_PROVIDERS = {
20
+ openlist: options => new OpenListDriveProvider(options),
21
+ };
22
+ /**
23
+ * Build the provider for one drive.
24
+ *
25
+ * @param kind - which drive.
26
+ * @param options - transport and clock seams passed through to the provider.
27
+ * @returns the provider, or `undefined` when that drive has no implementation.
28
+ */
29
+ export function providerFor(kind, options) {
30
+ return DRIVE_PROVIDERS[kind]?.(options);
31
+ }
32
+ /** Separator between the drive name and the drive's own file id. */
33
+ const REF_SEPARATOR = ':';
34
+ /**
35
+ * Compose the `id` half of a reference to one drive file.
36
+ *
37
+ * The drive name is carried in the id rather than in the source id so that all
38
+ * drives share one `ReferenceSource`, one grant gate, and one menu group.
39
+ *
40
+ * @param kind - which drive owns the file.
41
+ * @param fileId - the provider's own opaque id.
42
+ */
43
+ export function encodeDriveId(kind, fileId) {
44
+ if (kind !== 'openlist')
45
+ throw new ReferenceAnythingError('cloud-drive: unsupported drive', 'REFERENCE_INVALID_URI');
46
+ const path = normalizeOpenListPath(fileId);
47
+ return `${kind}${REF_SEPARATOR}${Buffer.from(JSON.stringify({ v: 1, path }), 'utf8').toString('base64url')}`;
48
+ }
49
+ /**
50
+ * Split a reference id back into a drive and the provider's file id.
51
+ *
52
+ * Splits on the *first* separator only: a provider's id may itself contain
53
+ * one.
54
+ *
55
+ * @param id - the `id` half of a `dsh-ref:` reference this source owns.
56
+ * @throws ReferenceAnythingError when the id names no known drive.
57
+ */
58
+ export function decodeDriveId(id) {
59
+ if (id.startsWith('baidu:') || id.startsWith('pds:')) {
60
+ throw new ReferenceAnythingError('cloud-drive: 旧版百度网盘/PDS 引用不能再读取;请在 OpenList 中重新选择该文件以迁移引用。', 'REFERENCE_INVALID_URI');
61
+ }
62
+ const cut = id.indexOf(REF_SEPARATOR);
63
+ const kind = cut === -1 ? '' : id.slice(0, cut);
64
+ const payload = cut === -1 ? '' : id.slice(cut + 1);
65
+ if (payload === '' || !isDriveKind(kind)) {
66
+ throw new ReferenceAnythingError(`cloud-drive: malformed reference id`, 'REFERENCE_INVALID_URI');
67
+ }
68
+ try {
69
+ if (!/^[A-Za-z0-9_-]+$/.test(payload))
70
+ throw new Error('not base64url');
71
+ const decoded = JSON.parse(Buffer.from(payload, 'base64url').toString('utf8'));
72
+ if (!isOpenListRef(decoded))
73
+ throw new Error('invalid shape');
74
+ return { kind, fileId: normalizeOpenListPath(decoded.path) };
75
+ }
76
+ catch {
77
+ throw new ReferenceAnythingError('cloud-drive: malformed OpenList reference id', 'REFERENCE_INVALID_URI');
78
+ }
79
+ }
80
+ /** Whether a string is one of the declared drive names. */
81
+ export function isDriveKind(value) {
82
+ return DRIVE_KINDS.includes(value);
83
+ }
84
+ /** Normalize absolute OpenList paths and reject traversal rather than resolving it. */
85
+ export function normalizeOpenListPath(value) {
86
+ if (typeof value !== 'string' || value.length === 0 || !value.startsWith('/') || value.includes('\\') || value.includes('\0')) {
87
+ throw new ReferenceAnythingError('cloud-drive: OpenList path must be absolute', 'REFERENCE_INVALID_URI');
88
+ }
89
+ const parts = value.split('/');
90
+ if (parts.some(part => part === '..')) {
91
+ throw new ReferenceAnythingError('cloud-drive: OpenList path traversal is not allowed', 'REFERENCE_INVALID_URI');
92
+ }
93
+ const clean = parts.filter(part => part !== '' && part !== '.');
94
+ return clean.length === 0 ? '/' : `/${clean.join('/')}`;
95
+ }
96
+ function isOpenListRef(value) {
97
+ if (value === null || typeof value !== 'object' || Array.isArray(value))
98
+ return false;
99
+ const row = value;
100
+ return row.v === 1 && typeof row.path === 'string' && Object.keys(row).length === 2;
101
+ }
@@ -0,0 +1,169 @@
1
+ /**
2
+ * Vocabulary for referencing files that live in a personal cloud drive.
3
+ *
4
+ * A drive is not a conversation, and the difference shapes everything here. A
5
+ * transcript is append-only and self-describing; a drive holds arbitrary bytes
6
+ * behind an authenticated, rate-limited, latency-bound API, and the same file
7
+ * can be reachable three different ways depending on what the provider has
8
+ * already indexed. So a provider is not a parser — it is a small transport
9
+ * that answers three questions: what is in here, is there text I can have
10
+ * without downloading, and can I read a byte range.
11
+ *
12
+ * The types are free of `node:fs` and cordis so a provider can be exercised
13
+ * against a stub `fetch` without mounting a plugin.
14
+ *
15
+ * @module dsh-reference-anything/cloud-drive/types
16
+ */
17
+ import type { FetchLike } from './providers/http.ts';
18
+ /**
19
+ * One supported drive product.
20
+ *
21
+ * OpenList is the only supported drive surface. Its stable locator is an
22
+ * absolute, normalized path encoded by the registry.
23
+ */
24
+ export type DriveKind = 'openlist';
25
+ /**
26
+ * One file or folder as listing sees it, without its bytes.
27
+ *
28
+ * Deliberately smaller than a `ReferenceSummary`: this is the provider's own
29
+ * vocabulary, and the mapping into the package's public wire types happens in
30
+ * one place so a provider cannot accidentally widen what reaches the client.
31
+ */
32
+ export interface DriveEntry {
33
+ /** Which provider owns {@link DriveEntry.id}. */
34
+ readonly kind: DriveKind;
35
+ /**
36
+ * Provider-scoped opaque id, stable across listings.
37
+ *
38
+ * Never exposed as a raw download URL. OpenList uses an absolute normalized
39
+ * path internally and the registry encodes it into the public reference id.
40
+ */
41
+ readonly id: string;
42
+ /** Basename for display, never empty — a nameless file falls back to its id. */
43
+ readonly name: string;
44
+ /**
45
+ * Full path as the provider displays it.
46
+ *
47
+ * User-facing only: it is a label and never an argument.
48
+ */
49
+ readonly path: string;
50
+ /** Size in bytes; `0` for a folder. */
51
+ readonly size: number;
52
+ /** Last modification in Unix epoch milliseconds, when the provider reports it. */
53
+ readonly modifiedAt?: number;
54
+ /** Folders are listed so the menu can show structure, but they are not readable. */
55
+ readonly isDirectory: boolean;
56
+ /**
57
+ * Server-extracted passage that came back with this entry, if any.
58
+ *
59
+ * Only semantic search produces this — a directory listing never does. It is
60
+ * a *recalled passage*, not the document, so it is a preview and never a
61
+ * substitute for a real read.
62
+ *
63
+ * Never copied into a `ReferenceSummary`: summaries flow to the client and
64
+ * into snippet-adjacent surfaces, and body text belongs only inside
65
+ * `reference_read`'s untrusted-data envelope.
66
+ */
67
+ readonly excerpt?: string;
68
+ /** True only when this row came from a bounded traversal after indexed search was unavailable. */
69
+ readonly searchIncomplete?: boolean;
70
+ }
71
+ /** What one ranged read actually returned. */
72
+ export interface DriveReadResult {
73
+ /** The bytes received, which may be fewer than requested at end of file. */
74
+ readonly bytes: Uint8Array;
75
+ /**
76
+ * Whether the provider honoured the requested range.
77
+ *
78
+ * `false` means it answered a range request with the whole file. The caller
79
+ * must be able to tell, because silently absorbing a multi-gigabyte body is
80
+ * the exact failure a ranged read exists to prevent.
81
+ */
82
+ readonly ranged: boolean;
83
+ /** Total file size when the response disclosed it, in bytes. */
84
+ readonly totalSize?: number;
85
+ }
86
+ /**
87
+ * One drive's transport: authentication, listing, and reading.
88
+ *
89
+ * Kept minimal on purpose. OpenList owns authentication, driver-specific
90
+ * details, and pagination behind its single filesystem surface.
91
+ */
92
+ /**
93
+ * Construction-time seams every provider accepts.
94
+ *
95
+ * `root` is the absolute OpenList directory where browsing begins. Credentials
96
+ * live in the host-only OpenList credential file rather than plugin config.
97
+ */
98
+ export interface DriveProviderOptions {
99
+ /** Where listing starts when the user has typed no query. */
100
+ readonly root?: string;
101
+ /** HTTP transport; defaults to the global `fetch`. */
102
+ readonly fetch?: FetchLike;
103
+ /** Clock in epoch milliseconds, for credential expiry and signed-URL aging. */
104
+ readonly now?: () => number;
105
+ /** Host-only OpenList credential file; defaults to the managed location. */
106
+ readonly configPath?: string;
107
+ /** Host-local credential supplier; ignored by non-OpenList transports. */
108
+ readonly credentials?: (refresh?: boolean) => Promise<{
109
+ readonly endpoint: string;
110
+ readonly token?: string;
111
+ } | undefined>;
112
+ }
113
+ export interface DriveProvider {
114
+ readonly kind: DriveKind;
115
+ /** Product name for menus and error text. */
116
+ readonly displayName: string;
117
+ /**
118
+ * Whether a usable credential exists locally.
119
+ *
120
+ * Must stay local: no network, no subprocess. Discovery calls this on every
121
+ * source before fanning out, and a configured-but-offline drive should
122
+ * report available and fail inside {@link DriveProvider.list} instead, so a
123
+ * dead network reads as a failed listing rather than a vanished menu group.
124
+ */
125
+ credentialed(): Promise<boolean>;
126
+ /**
127
+ * Enumerate entries matching a query.
128
+ * @param query - free text; empty means "the default directory listing".
129
+ * @param limit - hard cap on returned entries.
130
+ * @param signal - cancellation from the caller.
131
+ */
132
+ list(query: string, limit: number, signal?: AbortSignal): Promise<readonly DriveEntry[]>;
133
+ /**
134
+ * Describe one file without downloading it.
135
+ *
136
+ * Exists because a reference outlives the listing that produced it: a
137
+ * mention pasted into tomorrow's session names an id and nothing else, and
138
+ * the read that follows still has to report a label and decide whether the
139
+ * file is text before it spends a byte of transfer quota.
140
+ *
141
+ * @param id - a {@link DriveEntry.id} this provider owns.
142
+ * @param signal - cancellation from the caller.
143
+ * @returns the entry, or `undefined` when no such file exists.
144
+ */
145
+ describe(id: string, signal?: AbortSignal): Promise<DriveEntry | undefined>;
146
+ /**
147
+ * Tier 1: text the server already extracted, with no download.
148
+ * @returns the passage, or `undefined` when the provider has none — which is
149
+ * the common case and not an error.
150
+ */
151
+ extractedText(id: string, signal?: AbortSignal): Promise<string | undefined>;
152
+ /**
153
+ * Whether ranged reads work, or `undefined` until the first read has probed.
154
+ *
155
+ * Resolved at runtime rather than declared. A range request answered with
156
+ * `200` demotes the provider for the rest of the process.
157
+ */
158
+ supportsRange: boolean | undefined;
159
+ /**
160
+ * Read bytes `[start, end)`.
161
+ *
162
+ * @param id - a {@link DriveEntry.id} this provider owns.
163
+ * @param start - first byte, counted from zero.
164
+ * @param end - exclusive upper bound.
165
+ * @param signal - cancellation from the caller.
166
+ * @returns the bytes plus whether the range was honoured.
167
+ */
168
+ read(id: string, start: number, end: number, signal?: AbortSignal): Promise<DriveReadResult>;
169
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Vocabulary for referencing files that live in a personal cloud drive.
3
+ *
4
+ * A drive is not a conversation, and the difference shapes everything here. A
5
+ * transcript is append-only and self-describing; a drive holds arbitrary bytes
6
+ * behind an authenticated, rate-limited, latency-bound API, and the same file
7
+ * can be reachable three different ways depending on what the provider has
8
+ * already indexed. So a provider is not a parser — it is a small transport
9
+ * that answers three questions: what is in here, is there text I can have
10
+ * without downloading, and can I read a byte range.
11
+ *
12
+ * The types are free of `node:fs` and cordis so a provider can be exercised
13
+ * against a stub `fetch` without mounting a plugin.
14
+ *
15
+ * @module dsh-reference-anything/cloud-drive/types
16
+ */
17
+ export {};
@@ -46,6 +46,8 @@ export declare class FileReferenceSource implements ReferenceSource {
46
46
  private readonly roots;
47
47
  private readonly extensions;
48
48
  readonly id = "file";
49
+ /** Ungated on purpose: the configured roots are already inside the task's scope. */
50
+ readonly requiresGrant = false;
49
51
  /**
50
52
  * @param roots - absolute or process-relative directories to search.
51
53
  * @param extensions - file extensions treated as exports.
@@ -30,6 +30,8 @@ export class FileReferenceSource {
30
30
  roots;
31
31
  extensions;
32
32
  id = FILE_SOURCE_ID;
33
+ /** Ungated on purpose: the configured roots are already inside the task's scope. */
34
+ requiresGrant = false;
33
35
  /**
34
36
  * @param roots - absolute or process-relative directories to search.
35
37
  * @param extensions - file extensions treated as exports.
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Claude Code transcripts: `~/.claude/projects/<project-slug>/<uuid>.jsonl`.
3
+ *
4
+ * The file is an append-only log of everything the CLI drew, not a
5
+ * conversation: on a measured 70-file sample it held 6,277 `user` records of
6
+ * which only 565 carried a string body — the other 5,712 were arrays of
7
+ * `tool_result` blocks — against 11,434 `assistant` records that split text,
8
+ * reasoning, and each tool call into separate entries. Both facts drive the
9
+ * fold below, because projecting records one-to-one would bury the
10
+ * conversation under its own plumbing.
11
+ *
12
+ * @module dsh-reference-anything/local-agent/adapters/claude-code
13
+ */
14
+ import type { TranscriptAdapter } from '../types.ts';
15
+ /** Reads Claude Code's session transcripts. */
16
+ export declare const claudeCodeAdapter: TranscriptAdapter;
@@ -0,0 +1,171 @@
1
+ /**
2
+ * Claude Code transcripts: `~/.claude/projects/<project-slug>/<uuid>.jsonl`.
3
+ *
4
+ * The file is an append-only log of everything the CLI drew, not a
5
+ * conversation: on a measured 70-file sample it held 6,277 `user` records of
6
+ * which only 565 carried a string body — the other 5,712 were arrays of
7
+ * `tool_result` blocks — against 11,434 `assistant` records that split text,
8
+ * reasoning, and each tool call into separate entries. Both facts drive the
9
+ * fold below, because projecting records one-to-one would bury the
10
+ * conversation under its own plumbing.
11
+ *
12
+ * @module dsh-reference-anything/local-agent/adapters/claude-code
13
+ */
14
+ import { joinLocalPath } from "../path.js";
15
+ import { COMPACTION_MARKER, arrayField, cleanUserText, createSharedState, emitUser, flushAssistant, normalizeTitle, objectField, parseRecord, parseTimestamp, pushAssistant, renderToolCall, pushToolResults, stringField, } from "./shared.js";
16
+ /** Reads Claude Code's session transcripts. */
17
+ export const claudeCodeAdapter = {
18
+ kind: 'claude-code',
19
+ displayName: 'Claude Code',
20
+ defaultRoots(home) {
21
+ return [joinLocalPath(home, '.claude', 'projects')];
22
+ },
23
+ matches(relativePath) {
24
+ return relativePath.toLowerCase().endsWith('.jsonl');
25
+ },
26
+ createState() {
27
+ return createSharedState();
28
+ },
29
+ step(line, state, options) {
30
+ const record = parseRecord(line);
31
+ if (record === undefined)
32
+ return [];
33
+ // `isMeta` marks records the CLI generated about itself — the caveat that
34
+ // precedes a slash command's output, for one. They read as the user
35
+ // speaking and are not.
36
+ if (record['isMeta'] === true)
37
+ return [];
38
+ if (record['isSidechain'] === true && !options.includeSidechains)
39
+ return [];
40
+ const type = record['type'];
41
+ if (type === 'user')
42
+ return stepUser(record, state, options);
43
+ if (type === 'assistant')
44
+ return stepAssistant(record, state, options);
45
+ // Every other record type — `ai-title`, `last-prompt`, `mode`,
46
+ // `attachment`, `file-history-*`, `atis-latch` — is CLI state, not
47
+ // conversation. Titles come from head() instead, where a bounded probe can
48
+ // reach the newest one.
49
+ return [];
50
+ },
51
+ flush(state, _options) {
52
+ return flushAssistant(state);
53
+ },
54
+ head(headLines, tailLines) {
55
+ const found = {};
56
+ // Head first, then tail, and later wins for every field that can be
57
+ // rewritten: Claude Code re-emits `ai-title` as its guess improves — 1,421
58
+ // times across the same sample — so a head-only read gets a stale name.
59
+ for (const line of [...headLines, ...tailLines]) {
60
+ const record = parseRecord(line);
61
+ if (record === undefined)
62
+ continue;
63
+ const type = record['type'];
64
+ if (type === 'ai-title')
65
+ found.aiTitle = stringField(record, 'aiTitle') ?? found.aiTitle;
66
+ else if (type === 'summary')
67
+ found.summary = stringField(record, 'summary') ?? found.summary;
68
+ else if (type === 'last-prompt')
69
+ found.lastPrompt = stringField(record, 'lastPrompt') ?? found.lastPrompt;
70
+ found.slug = stringField(record, 'slug') ?? found.slug;
71
+ found.cwd = stringField(record, 'cwd') ?? found.cwd;
72
+ if (found.createdAt === undefined)
73
+ found.createdAt = parseTimestamp(record['timestamp']);
74
+ }
75
+ // The opening prompt comes from the head alone — the tail's user records
76
+ // are the newest ones and would name the wrong thing.
77
+ for (const line of headLines) {
78
+ const record = parseRecord(line);
79
+ if (record === undefined || record['type'] !== 'user')
80
+ continue;
81
+ if (record['isMeta'] === true || record['isCompactSummary'] === true)
82
+ continue;
83
+ const text = userText(record);
84
+ if (text !== undefined) {
85
+ found.firstPrompt = text;
86
+ break;
87
+ }
88
+ }
89
+ const title = normalizeTitle(found.aiTitle ?? found.summary ?? found.lastPrompt ?? found.slug ?? found.firstPrompt ?? '');
90
+ return {
91
+ ...title === '' ? {} : { title },
92
+ ...found.cwd === undefined ? {} : { cwd: found.cwd },
93
+ ...found.firstPrompt === undefined ? {} : { firstPrompt: found.firstPrompt },
94
+ ...found.createdAt === undefined ? {} : { createdAt: found.createdAt },
95
+ };
96
+ },
97
+ };
98
+ /** Fold one `user` record, which is a real turn far less often than it looks. */
99
+ function stepUser(record, state, options) {
100
+ if (record['isCompactSummary'] === true || record['subtype'] === 'compact_boundary') {
101
+ state.compacted = true;
102
+ const summary = userText(record);
103
+ const closed = flushAssistant(state);
104
+ return summary === undefined
105
+ ? closed
106
+ : [...closed, { role: 'assistant', text: `${COMPACTION_MARKER}\n\n${summary}` }];
107
+ }
108
+ pushToolResults(state, objectField(record, 'message')?.['content'], options);
109
+ const text = userText(record);
110
+ return text === undefined ? [] : emitUser(state, text);
111
+ }
112
+ /** Fold one `assistant` record into the open run rather than into a turn of its own. */
113
+ function stepAssistant(record, state, options) {
114
+ const blocks = arrayField(objectField(record, 'message'), 'content');
115
+ if (blocks === undefined)
116
+ return [];
117
+ for (const block of blocks) {
118
+ if (typeof block !== 'object' || block === null)
119
+ continue;
120
+ const entry = block;
121
+ switch (entry['type']) {
122
+ case 'text':
123
+ pushAssistant(state, typeof entry['text'] === 'string' ? entry['text'] : undefined);
124
+ break;
125
+ case 'thinking':
126
+ if (options.includeThinking) {
127
+ pushAssistant(state, typeof entry['thinking'] === 'string' ? entry['thinking'] : undefined);
128
+ }
129
+ break;
130
+ case 'tool_use':
131
+ pushAssistant(state, renderToolCall(entry['name'], entry['input'], options.toolCalls, options.toolSummaryChars));
132
+ break;
133
+ default:
134
+ // `redacted_thinking` and anything a later CLI adds carry nothing a
135
+ // reader can use; silently skipping keeps new record shapes harmless.
136
+ break;
137
+ }
138
+ }
139
+ return [];
140
+ }
141
+ /**
142
+ * The words a person actually typed in one `user` record, if any.
143
+ *
144
+ * A string body is always a real turn. An array body is usually a batch of
145
+ * `tool_result` blocks the CLI attributes to the user role — plumbing, and the
146
+ * overwhelming majority — but occasionally carries `text` blocks, such as the
147
+ * note left when someone interrupts a tool call. So the discriminator is the
148
+ * block type, not the container type.
149
+ * @param record - one `user` record.
150
+ * @returns the user's text, or undefined when the record is not a turn.
151
+ */
152
+ function userText(record) {
153
+ const content = objectField(record, 'message')?.['content'];
154
+ if (typeof content === 'string')
155
+ return blank(cleanUserText(content));
156
+ if (!Array.isArray(content))
157
+ return undefined;
158
+ const parts = [];
159
+ for (const block of content) {
160
+ if (typeof block !== 'object' || block === null)
161
+ continue;
162
+ const entry = block;
163
+ if (entry['type'] === 'text' && typeof entry['text'] === 'string')
164
+ parts.push(entry['text']);
165
+ }
166
+ return parts.length === 0 ? undefined : blank(cleanUserText(parts.join('\n')));
167
+ }
168
+ /** Collapse an empty result to `undefined`, so callers test one thing. */
169
+ function blank(text) {
170
+ return text.trim() === '' ? undefined : text.trim();
171
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Codex CLI rollouts: `~/.codex/sessions/<yyyy>/<mm>/<dd>/rollout-*.jsonl`.
3
+ *
4
+ * Each line is an envelope `{timestamp, ordinal?, type, payload}`. Measured
5
+ * across 170 rollouts, seven envelope types occur — `response_item`,
6
+ * `event_msg`, `session_meta`, `turn_context`, `world_state`, `compacted`, and
7
+ * `inter_agent_communication_metadata` — but only `response_item` carries
8
+ * conversation.
9
+ *
10
+ * `event_msg` is the trap. Its `agent_message` and `user_message` payloads look
11
+ * like the assistant and the user speaking, and they are: they restate what
12
+ * `response_item` already recorded. Counted per file, `event_msg.agent_message`
13
+ * matched `response_item.message` with `role: 'assistant'` one for one across
14
+ * every rollout that had any, so consuming both would double the transcript.
15
+ *
16
+ * @module dsh-reference-anything/local-agent/adapters/codex
17
+ */
18
+ import type { TranscriptAdapter } from '../types.ts';
19
+ /** Reads Codex CLI rollout logs. */
20
+ export declare const codexAdapter: TranscriptAdapter;