@lmctl-ai/lmctl 0.1.342 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (182) hide show
  1. package/CHANGELOG.md +116 -0
  2. package/README.md +11 -51
  3. package/data/price-aliases.json +1 -0
  4. package/data/price-data.json +1 -1
  5. package/dist/cli/batch.d.ts +2 -0
  6. package/dist/cli/busy-evidence.d.ts +27 -0
  7. package/dist/cli/chat-cycle.d.ts +59 -0
  8. package/dist/cli/chat-exec.d.ts +83 -0
  9. package/dist/cli/chat-policy.d.ts +22 -0
  10. package/dist/cli/chat.d.ts +23 -0
  11. package/dist/cli/code-resolver.d.ts +8 -0
  12. package/dist/cli/common.d.ts +54 -0
  13. package/dist/cli/context.d.ts +12 -0
  14. package/dist/cli/cost.d.ts +2 -0
  15. package/dist/cli/export.d.ts +2 -0
  16. package/dist/cli/index.d.ts +3 -0
  17. package/dist/cli/index.js +1062 -1400
  18. package/dist/cli/infra.d.ts +78 -0
  19. package/dist/cli/ls.d.ts +2 -0
  20. package/dist/cli/mailbox-cli.d.ts +6 -0
  21. package/dist/cli/managed.d.ts +62 -0
  22. package/dist/cli/mcp-install.d.ts +23 -0
  23. package/dist/cli/mcp.d.ts +2 -0
  24. package/dist/cli/paths.d.ts +50 -0
  25. package/dist/cli/plan-wizard.d.ts +42 -0
  26. package/dist/cli/plan.d.ts +20 -0
  27. package/dist/cli/queue-delivery.d.ts +107 -0
  28. package/dist/cli/ratelimit.d.ts +3 -0
  29. package/dist/cli/remote-fetch.d.ts +7 -0
  30. package/dist/cli/run-queries.d.ts +82 -0
  31. package/dist/cli/running.d.ts +5 -0
  32. package/dist/cli/schema.sql +20 -1
  33. package/dist/cli/script.d.ts +6 -0
  34. package/dist/cli/script.js +2097 -0
  35. package/dist/cli/self-identity.d.ts +25 -0
  36. package/dist/cli/session-idle.d.ts +118 -0
  37. package/dist/cli/status.d.ts +22 -0
  38. package/dist/cli/tail.d.ts +18 -0
  39. package/dist/cli/target-resolver.d.ts +139 -0
  40. package/dist/cli/team-status.d.ts +91 -0
  41. package/dist/cli/teamfile-compat.d.ts +71 -0
  42. package/dist/cli/teamfile-store.d.ts +18 -0
  43. package/dist/cli/teamfile-template.d.ts +40 -0
  44. package/dist/cli/template-env.d.ts +25 -0
  45. package/dist/cli/terminal-lock.d.ts +25 -0
  46. package/dist/cli/terminal.d.ts +23 -0
  47. package/dist/cli/time-filter.d.ts +10 -0
  48. package/dist/cli/update-notifier.d.ts +11 -0
  49. package/dist/cli/version-util.d.ts +2 -0
  50. package/dist/config/seed_templates.d.ts +1 -0
  51. package/dist/db/backfill-event-log.d.ts +26 -0
  52. package/dist/db/delegation.d.ts +37 -0
  53. package/dist/db/escalation_vocab.d.ts +45 -0
  54. package/dist/db/event-log.d.ts +40 -0
  55. package/dist/db/index.d.ts +1213 -0
  56. package/dist/db/inflight-acquire.d.ts +70 -0
  57. package/dist/db/invocation-event-projections.d.ts +65 -0
  58. package/dist/db/ledger-identity.test.d.ts +1 -0
  59. package/dist/db/live-event-projections.d.ts +92 -0
  60. package/dist/db/migrate.d.ts +51 -0
  61. package/dist/db/open-readonly.d.ts +2 -0
  62. package/dist/db/process-liveness.d.ts +37 -0
  63. package/dist/db/sqlite.d.ts +47 -0
  64. package/dist/db/sqlite.test.d.ts +1 -0
  65. package/dist/db/terminal-lock.d.ts +14 -0
  66. package/dist/db/tracked-invocation-reconcile.d.ts +52 -0
  67. package/dist/db/tracked-invocation.d.ts +116 -0
  68. package/dist/mcp/dispatch.d.ts +45 -0
  69. package/dist/mcp/mediation.d.ts +128 -0
  70. package/dist/mcp/path-warning.d.ts +7 -0
  71. package/dist/mcp/server.d.ts +63 -0
  72. package/dist/mcp/tools.d.ts +24 -0
  73. package/dist/providers/adapter.d.ts +265 -0
  74. package/dist/providers/adapters/agy-adapter.d.ts +17 -0
  75. package/dist/providers/adapters/claude-adapter.d.ts +20 -0
  76. package/dist/providers/adapters/codex-adapter.d.ts +22 -0
  77. package/dist/providers/adapters/copilot-adapter.d.ts +16 -0
  78. package/dist/providers/adapters/dsh-adapter.d.ts +89 -0
  79. package/dist/providers/adapters/gemini-adapter.d.ts +44 -0
  80. package/dist/providers/adapters/hermes-adapter.d.ts +71 -0
  81. package/dist/providers/adapters/index.d.ts +35 -0
  82. package/dist/providers/adapters/kimi-adapter.d.ts +16 -0
  83. package/dist/providers/adapters/lmplayer-adapter.d.ts +14 -0
  84. package/dist/providers/adapters/opencode-adapter.d.ts +11 -0
  85. package/dist/providers/adapters/pi-adapter.d.ts +17 -0
  86. package/dist/providers/adapters/print-turn.d.ts +29 -0
  87. package/dist/providers/adapters/qwen-adapter.d.ts +11 -0
  88. package/dist/providers/agy.d.ts +32 -0
  89. package/dist/providers/base-adapter.d.ts +107 -0
  90. package/dist/providers/base.d.ts +12 -0
  91. package/dist/providers/binary-check.d.ts +102 -0
  92. package/dist/providers/bundle.d.ts +118 -0
  93. package/dist/providers/claude-mock.d.ts +90 -0
  94. package/dist/providers/claude.d.ts +21 -0
  95. package/dist/providers/codex-config.d.ts +15 -0
  96. package/dist/providers/codex.d.ts +25 -0
  97. package/dist/providers/config/model-selection-lint.d.ts +45 -0
  98. package/dist/providers/config/team-config.d.ts +183 -0
  99. package/dist/providers/config/types.d.ts +146 -0
  100. package/dist/providers/config/user-config.d.ts +88 -0
  101. package/dist/providers/copilot.d.ts +21 -0
  102. package/dist/providers/debug.d.ts +128 -0
  103. package/dist/providers/dsh-sidecar.d.ts +65 -0
  104. package/dist/providers/dsh-zstd.d.ts +27 -0
  105. package/dist/providers/dsh.d.ts +37 -0
  106. package/dist/providers/frontmatter.d.ts +46 -0
  107. package/dist/providers/gemini-session-reader.d.ts +151 -0
  108. package/dist/providers/gemini.d.ts +39 -0
  109. package/dist/providers/hermes.d.ts +90 -0
  110. package/dist/providers/index.d.ts +421 -0
  111. package/dist/providers/kimi.d.ts +37 -0
  112. package/dist/providers/model-catalog.d.ts +141 -0
  113. package/dist/providers/one-shot-timeout.d.ts +1 -0
  114. package/dist/providers/opencode-config.d.ts +62 -0
  115. package/dist/providers/opencode.d.ts +40 -0
  116. package/dist/providers/parsers/acp.d.ts +47 -0
  117. package/dist/providers/parsers/codex-app-server.d.ts +60 -0
  118. package/dist/providers/parsers/stream-json.d.ts +57 -0
  119. package/dist/providers/passive-readers/agy.d.ts +155 -0
  120. package/dist/providers/passive-readers/copilot.d.ts +173 -0
  121. package/dist/providers/passive-readers/dsh.d.ts +34 -0
  122. package/dist/providers/passive-readers/kimi.d.ts +44 -0
  123. package/dist/providers/passive-readers/opencode.d.ts +89 -0
  124. package/dist/providers/passive-readers/qwen.d.ts +151 -0
  125. package/dist/providers/passive-readers/shared.d.ts +138 -0
  126. package/dist/providers/passive-session-reader.d.ts +188 -0
  127. package/dist/providers/path-utils.d.ts +1 -0
  128. package/dist/providers/pi.d.ts +32 -0
  129. package/dist/providers/pricing.d.ts +107 -0
  130. package/dist/providers/qwen.d.ts +22 -0
  131. package/dist/providers/ratelimit-readings.d.ts +32 -0
  132. package/dist/providers/registry.d.ts +31 -0
  133. package/dist/providers/resume.d.ts +43 -0
  134. package/dist/providers/seed-instruction.d.ts +3 -0
  135. package/dist/providers/self-session-env.d.ts +21 -0
  136. package/dist/providers/session-limit.d.ts +9 -0
  137. package/dist/providers/strip-ansi.d.ts +5 -0
  138. package/dist/providers/transport-errors.d.ts +94 -0
  139. package/dist/providers/transport-lifecycle.d.ts +63 -0
  140. package/dist/providers/transport-wrappers.d.ts +110 -0
  141. package/dist/providers/transports/acp-profiles/config-option-step.d.ts +24 -0
  142. package/dist/providers/transports/acp-profiles/copilot.d.ts +12 -0
  143. package/dist/providers/transports/acp-profiles/gemini.d.ts +22 -0
  144. package/dist/providers/transports/acp-profiles/hermes.d.ts +27 -0
  145. package/dist/providers/transports/acp-profiles/index.d.ts +19 -0
  146. package/dist/providers/transports/acp-profiles/kimi.d.ts +24 -0
  147. package/dist/providers/transports/acp-profiles/opencode.d.ts +26 -0
  148. package/dist/providers/transports/acp-profiles/types.d.ts +258 -0
  149. package/dist/providers/transports/acp-rpc.d.ts +18 -0
  150. package/dist/providers/transports/acp.d.ts +215 -0
  151. package/dist/providers/transports/codex-app-server.d.ts +190 -0
  152. package/dist/providers/transports/hold-open.d.ts +120 -0
  153. package/dist/providers/transports/manager.d.ts +21 -0
  154. package/dist/providers/transports/stream-json.d.ts +162 -0
  155. package/dist/providers/transports/types.d.ts +56 -0
  156. package/dist/providers/types.d.ts +154 -0
  157. package/dist/providers/yolo-contract.d.ts +131 -0
  158. package/dist/script/adapter.d.ts +101 -0
  159. package/dist/script/batch-script.d.ts +7 -0
  160. package/dist/script/index.d.ts +3 -0
  161. package/dist/script/lmscript.d.ts +550 -0
  162. package/dist/shared/policy.d.ts +74 -0
  163. package/dist/shared-lib/db.d.ts +25 -0
  164. package/dist/shared-lib/passive-session-reader.d.ts +2 -0
  165. package/dist/shared-lib/provider-parse.d.ts +6 -0
  166. package/dist/templates/basedir.d.ts +52 -0
  167. package/dist/uri/lmctl_uri.d.ts +38 -0
  168. package/durable-memory/model_selection.md +108 -25
  169. package/examples/README.md +103 -0
  170. package/examples/anonymous-panel.lms +3 -0
  171. package/examples/batch-prompts.txt +9 -0
  172. package/examples/batchscript.lms +67 -0
  173. package/examples/n8n/agent-lib.lms +56 -0
  174. package/examples/n8n/git-lib.lms +93 -0
  175. package/examples/n8n/mock-support.db +0 -0
  176. package/examples/n8n/multi-agent-review.lms +107 -0
  177. package/examples/n8n/news-digest.lms +159 -0
  178. package/examples/n8n/rag-search.lms +163 -0
  179. package/examples/n8n/support-agent.lms +88 -0
  180. package/examples/n8n/ticket-triage.lms +156 -0
  181. package/package.json +13 -17
  182. package/dist/cli/side_effect_classifier.json +0 -19
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Passive (read-only, bounded, certified) reader for OpenCode sessions.
3
+ *
4
+ * ## Relationship to lmplayer (the investigated finding, from real stores)
5
+ *
6
+ * lmplayer IS an opencode fork/wrapper, and the two share the opencode SQLite
7
+ * schema EXACTLY: `message(id,session_id,time_created,time_updated,data)`,
8
+ * `part(id,message_id,session_id,time_created,time_updated,data)`,
9
+ * `session(id,project_id,…)`, with the per-row `data` column holding JSON. Verified
10
+ * on this box: an opencode assistant `message.data` is
11
+ * `{parentID, role:"assistant", tokens:{input,output,reasoning,cache:{read,write}},
12
+ * modelID, providerID, time:{created,completed}}` and a tool `part.data` is
13
+ * `{type:"tool", tool, callID, state:{status,input,output,metadata:{display}}}` —
14
+ * byte-for-byte the shapes the dispatcher's lmplayer reader already handles.
15
+ *
16
+ * What DIFFERS is ONLY the store location and the label:
17
+ * - opencode canonical store: `$XDG_DATA_HOME/opencode/opencode.db` (large, live)
18
+ * and `…/opencode/opencode-local.db`, plus `$XDG_CONFIG_HOME/opencode/opencode.db`.
19
+ * - lmplayer store: `$XDG_DATA_HOME/lmplayer/opencode*.db` (with extra
20
+ * lmplayer-only `session_job*` tables — the source of the
21
+ * `session-job-notification` synthetic part type, filtered here too).
22
+ *
23
+ * So this reader reuses the lmplayer extraction verbatim but labels every source
24
+ * `provider:"opencode"` — NEVER "lmplayer", even though the SQLite shape is
25
+ * identical (lmbi's explicit "no cross-provider mislabeling" requirement). The
26
+ * label flows from `source.provider`, which discovery sets to `"opencode"`.
27
+ *
28
+ * LANDMINE for wiring: the `OPENCODE_DB` env var currently routes the dispatcher's
29
+ * LMPLAYER default paths — pointing it at the opencode store would label opencode
30
+ * data as `lmplayer`. This reader deliberately does NOT read that env; an explicit
31
+ * store override goes through `OpencodePassiveReaderOptions.dbPaths`.
32
+ *
33
+ * De-duplicate the mirrored helpers (parseApplyPatch / classifyTool / firstPath /
34
+ * writeInputBytes / sqliteRevision / the row reader) against the dispatcher's own
35
+ * copies at wiring time.
36
+ */
37
+ import type { ReadLimits, Snapshot, Source, SourceState } from "../passive-session-reader.js";
38
+ export declare const OPENCODE_PROVIDER = "opencode";
39
+ /** opencode's own canonical stores (NOT lmplayer's, NOT the OPENCODE_DB env). */
40
+ export declare function defaultOpencodeDbPaths(): string[];
41
+ interface OpencodeMessageRow extends Record<string, unknown> {
42
+ id: string;
43
+ time_created: number;
44
+ time_updated: number;
45
+ data: string;
46
+ }
47
+ interface OpencodePartRow extends Record<string, unknown> {
48
+ id: string;
49
+ message_id: string;
50
+ time_created: number;
51
+ time_updated: number;
52
+ data: string;
53
+ }
54
+ /** Extract per-response metadata from an opencode session's rows. Verbatim
55
+ * lmplayer logic, labeled through `source.provider` (= "opencode"). */
56
+ export declare function readOpencodeRows(source: Source, revision: string, rows: {
57
+ messages: OpencodeMessageRow[];
58
+ parts: OpencodePartRow[];
59
+ }, limits: ReadLimits): Snapshot;
60
+ export interface OpencodePassiveReaderOptions {
61
+ /** Overrides the default opencode store paths (does NOT read `OPENCODE_DB`). */
62
+ dbPaths?: string[];
63
+ sqliteBusyTimeoutMs?: number;
64
+ }
65
+ /** Keyset watermark for cross-DB discovery (one page continues after this key). */
66
+ export interface OpencodeDiscoveryCursor {
67
+ source_id: string;
68
+ sessionid: string;
69
+ }
70
+ export declare class OpencodePassiveReader {
71
+ private readonly dbPaths;
72
+ private readonly sqliteBusyTimeoutMs;
73
+ constructor(options?: OpencodePassiveReaderOptions);
74
+ private openDb;
75
+ /**
76
+ * Enumerate up to `cap` sources ordered by `source_id\0sessionid`, deduped by
77
+ * session id (the first configured DB that holds it is canonical). `after`
78
+ * continues a previous page. Bounded SQL (`LIMIT`) — no traversal budget, so
79
+ * `truncated` is always false.
80
+ */
81
+ discover(limits: ReadLimits, cap: number, after?: OpencodeDiscoveryCursor | null): Promise<{
82
+ sources: Source[];
83
+ truncated: boolean;
84
+ }>;
85
+ inspect(source: Source, limits: ReadLimits): SourceState;
86
+ read(source: Source, revision: string, limits: ReadLimits): Snapshot;
87
+ }
88
+ export declare function createOpencodePassiveReader(options?: OpencodePassiveReaderOptions): OpencodePassiveReader;
89
+ export {};
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Passive (read-only, bounded, certified) reader for Qwen Code sessions.
3
+ *
4
+ * ## Why this reader does not follow either of qwen.ts's own headers
5
+ *
6
+ * `src/providers/qwen.ts` carries two conflicting claims: its file header says
7
+ * "Qwen Code is a Claude Code fork — same JSONL session format", while its
8
+ * `QwenUsageMetadata` comment says "Qwen Code is a Gemini CLI fork; usageMetadata
9
+ * follows Gemini API semantics". Neither label alone describes the records on
10
+ * disk, so this reader was written from the records instead. What a census of
11
+ * every `*.jsonl` under the local projects store actually shows (1155 records,
12
+ * 0 parse failures):
13
+ *
14
+ * - The ENVELOPE is Claude-shaped: `uuid`, `parentUuid`, `sessionId`,
15
+ * `timestamp`, `type`, `cwd`, `version`, `gitBranch` on 1155/1155 records.
16
+ * - The record TYPES diverge from Claude: `system` (592), `assistant` (276),
17
+ * `tool_result` (238), `user` (49). Claude folds tool results into `user`
18
+ * rows; qwen gives them their own type. That makes a `user` row an
19
+ * unambiguous external prompt admission here — no tool_result sniffing.
20
+ * - Usage exists in TWO shapes simultaneously, so "which fork" is the wrong
21
+ * question:
22
+ * 1. `assistant.usageMetadata` — Gemini camelCase
23
+ * (`promptTokenCount` / `candidatesTokenCount` / `thoughtsTokenCount` /
24
+ * `totalTokenCount` / `cachedContentTokenCount`), on 276 records, with
25
+ * NO per-response identity of any kind.
26
+ * 2. `system` rows with `subtype:"ui_telemetry"` and
27
+ * `systemPayload.uiEvent["event.name"] === "qwen-code.api_response"` —
28
+ * snake_case (`input_token_count` / `output_token_count` /
29
+ * `cached_content_token_count` / `thoughts_token_count` /
30
+ * `total_token_count`), on 289 records, each carrying a native
31
+ * `response_id` (`chatcmpl-…`).
32
+ *
33
+ * This reader treats shape (2) as authoritative, because it is the ONLY source
34
+ * with a native per-response id — the contract forbids fabricating a
35
+ * `response_id` out of an envelope uuid — and it strictly covers shape (1)
36
+ * (289 ≥ 276; the extra 13 are `subagent_name` responses that have no
37
+ * top-level assistant row at all).
38
+ *
39
+ * ## Token semantics: measured, not assumed
40
+ *
41
+ * All 289 local api_response records satisfy, with zero violations:
42
+ * input_token_count + output_token_count === total_token_count
43
+ * cached_content_token_count <= input_token_count
44
+ * thoughts_token_count <= output_token_count
45
+ *
46
+ * i.e. cached is a SUBSET of input and thoughts a SUBSET of output — Gemini
47
+ * semantics, now verified on real records rather than inherited from a comment.
48
+ * The three invariants are re-checked at read time; a violation makes the whole
49
+ * snapshot `partial` rather than emitting numbers whose meaning we cannot stand
50
+ * behind.
51
+ *
52
+ * `input_cache_write` is NULL, not 0: no cache-write counter exists anywhere in
53
+ * this wire (no such key on any of the 289 records), so its value is unrecorded,
54
+ * not known-zero.
55
+ *
56
+ * `model_provider` is NULL. The nearest native field is `auth_type`
57
+ * (`openai` / `qwen-oauth` locally), which is an AUTHENTICATION method, not a
58
+ * model-provider identity; reporting `qwen-oauth` as a provider would be a
59
+ * mislabel. See the note in the owner report.
60
+ */
61
+ import type { ReadLimits, Snapshot, Source, SourceState } from "../passive-session-reader.js";
62
+ import { type TokenValues } from "./shared.js";
63
+ export declare const QWEN_PROVIDER = "qwen";
64
+ /** One raw line of a qwen chat transcript. Only fields this reader reads. */
65
+ interface QwenLine {
66
+ uuid?: unknown;
67
+ parentUuid?: unknown;
68
+ sessionId?: unknown;
69
+ type?: unknown;
70
+ subtype?: unknown;
71
+ timestamp?: unknown;
72
+ message?: unknown;
73
+ systemPayload?: unknown;
74
+ }
75
+ export interface QwenPassiveReaderOptions {
76
+ /**
77
+ * Discovery roots, in the caller's preferred order. Defaults to the live store
78
+ * plus every retained backup store (see `defaultQwenProjectRoots`).
79
+ */
80
+ qwenProjectsDirs?: string[];
81
+ maxDiscoveryTraversalVisits?: number;
82
+ }
83
+ /**
84
+ * Default qwen discovery roots.
85
+ *
86
+ * `~/.qwen.bk1/projects` is a RETAINED ACCOUNT HISTORY store, not a redundant
87
+ * copy to skip. Verified on this box: 30 transcripts there against 27 in the
88
+ * live root, with **zero identity overlap** — 0 shared file names, 0 shared
89
+ * in-record `sessionId`s and 0 shared `response_id`s — over disjoint telemetry
90
+ * spans (backup 2026-03-14 → 04-12, live 2026-04-14 → 07-02). Excluding it
91
+ * would silently drop 89 real responses from any cost total.
92
+ */
93
+ export declare function defaultQwenProjectRoots(): string[];
94
+ /**
95
+ * Map one api_response telemetry event onto the contract's four token slots.
96
+ * Returns null when a required counter is missing/not a non-negative integer, or
97
+ * when any of the three measured invariants fails — the caller turns that into a
98
+ * `partial` snapshot rather than guessing.
99
+ */
100
+ export declare function qwenTokens(event: Record<string, unknown>): TokenValues | null;
101
+ export declare function readQwen(lines: QwenLine[], source: Source, revision: string, limits: ReadLimits): Snapshot;
102
+ /**
103
+ * Do the records agree with the session id the caller asked for? True when no
104
+ * record names a session at all (nothing to contradict) or when every named
105
+ * session equals `sessionid`. False on any disagreement, including a transcript
106
+ * that names two different sessions.
107
+ */
108
+ export declare function qwenRecordsMatchSession(lines: QwenLine[], sessionid: string): boolean;
109
+ export declare class QwenPassiveReader {
110
+ /**
111
+ * Resolved discovery roots, in configured order. Exposed so a caller can map a
112
+ * `source_id` back to the root it came from without re-deriving the defaults.
113
+ */
114
+ readonly roots: readonly string[];
115
+ private readonly maxDiscoveryTraversalVisits;
116
+ constructor(options?: QwenPassiveReaderOptions);
117
+ /**
118
+ * Enumerate up to `cap` sources across EVERY configured root, ordered by real
119
+ * path. `afterPath` continues a previous page; real-path order is total, so one
120
+ * cursor resumes the merged stream correctly.
121
+ *
122
+ * ## Provenance and cross-root identity
123
+ *
124
+ * `source_id` is the file's absolute real path, so it names the root a session
125
+ * came from (`…/.qwen/projects/…` vs `…/.qwen.bk1/projects/…`) without any
126
+ * extra field on the shared `Source` shape. Two sessions that happen to share a
127
+ * `sessionid` across roots therefore stay two DISTINCT sources with distinct
128
+ * `source_id`s, and both are read.
129
+ *
130
+ * This reader performs **no identity dedup** — that is the consumer's job, on
131
+ * real ids. The only collapsing done here is by real path, which removes a file
132
+ * reachable through two configured roots (e.g. one root symlinked inside
133
+ * another); that is the same physical file, never two sessions that merely look
134
+ * alike. Nothing is ever dropped because a similarly-named file exists
135
+ * elsewhere.
136
+ */
137
+ discover(limits: ReadLimits, cap: number, afterPath?: string | null): Promise<{
138
+ sources: Source[];
139
+ truncated: boolean;
140
+ }>;
141
+ /**
142
+ * A source_id is only ours when it is a real, non-symlink file contained by one
143
+ * of the configured roots AND its basename is the claimed session id. Both
144
+ * checks run before any read.
145
+ */
146
+ private matches;
147
+ inspect(source: Source, limits: ReadLimits): Promise<SourceState>;
148
+ read(source: Source, revision: string, limits: ReadLimits): Promise<Snapshot>;
149
+ }
150
+ export declare function createQwenPassiveReader(options?: QwenPassiveReaderOptions): QwenPassiveReader;
151
+ export {};
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Self-contained primitives for the per-provider passive readers that live in
3
+ * this directory (agy, qwen).
4
+ *
5
+ * WHY THIS FILE EXISTS: the equivalent helpers inside
6
+ * `src/providers/passive-session-reader.ts` are module-private, and that file is
7
+ * concurrently owned by the discovery-fairness redesign. Re-implementing the
8
+ * handful we need here keeps this slice landable without touching the shared
9
+ * dispatcher. Only the PUBLIC contract types are imported from it, so there is
10
+ * exactly one definition of the wire shape.
11
+ *
12
+ * At wiring time these should be de-duplicated against the dispatcher's own
13
+ * copies (see PASSIVE_EXTRACTION_SEMANTICS below — that constant in particular
14
+ * MUST stay equal to the dispatcher's `EXTRACTION_SEMANTICS`).
15
+ */
16
+ import type { ExternalIoEntry, PricingFacts, ReadLimits } from "../passive-session-reader.js";
17
+ /**
18
+ * Extraction-semantics tag folded into every revision string.
19
+ *
20
+ * MUST equal `EXTRACTION_SEMANTICS` in `../passive-session-reader.ts` (currently
21
+ * "bx3"). A revision is compared by lmbi as a plain string, so a reader emitting
22
+ * a different tag for the same native bytes would look permanently un-indexed.
23
+ * Bump both together whenever extraction semantics change — this file's real
24
+ * de-dup against the dispatcher's copy is still pending (see file header);
25
+ * until then a bump here is easy to miss, as happened with the bx2→bx3 change.
26
+ */
27
+ export declare const PASSIVE_EXTRACTION_SEMANTICS = "bx3";
28
+ export type FileDirection = "read" | "written";
29
+ export interface FileObservation {
30
+ path: string;
31
+ bytes: number | null;
32
+ }
33
+ export interface TokenValues {
34
+ input_uncached: number | null;
35
+ input_cached: number | null;
36
+ input_cache_write: number | null;
37
+ output: number | null;
38
+ }
39
+ export declare const EMPTY_TOKENS: TokenValues;
40
+ export declare function record(value: unknown): Record<string, unknown> | null;
41
+ export declare function nonnegativeInteger(value: unknown): number | null;
42
+ /** Non-negative integer counter, or null. Used for pricing_facts counts. */
43
+ export declare function optCount(value: unknown): number | null;
44
+ export declare function canonicalTimestamp(value: unknown): string | null;
45
+ export declare function checkedLimits(limits: ReadLimits): void;
46
+ /**
47
+ * Deterministic UTF-16 code-unit ordering for opaque identifiers/paths. ICU
48
+ * collation can differ across Node builds; two processes must agree on cursor
49
+ * order, so code-unit comparison is the safe primitive.
50
+ */
51
+ export declare function compareCodeUnits(left: string, right: string): number;
52
+ /**
53
+ * Per-response file observations keyed by (call id, direction, path) — the
54
+ * "smallest honest unit". Two DIFFERENT call ids to the same path stay separate;
55
+ * a replayed record of the SAME call id coalesces (first recorded wins).
56
+ * `bytes` is UTF-8 content size: a number (including 0), or null when the
57
+ * content is genuinely unavailable.
58
+ */
59
+ export declare class ObservationSet {
60
+ private readonly map;
61
+ record(callId: string, direction: FileDirection, path: unknown, bytes: number | null): void;
62
+ files(direction: FileDirection): FileObservation[];
63
+ }
64
+ /**
65
+ * Collector for non-file external-I/O entries. Deduplicates by native
66
+ * (id, direction, kind); a MEASURED id-less entry is never deduplicated
67
+ * (content/byte dedup is forbidden by the contract), while id-less null-byte
68
+ * coverage markers may merge (one unknown marker forces NULL as well as two).
69
+ */
70
+ export declare class ExternalIoSet {
71
+ private readonly map;
72
+ record(entry: ExternalIoEntry): void;
73
+ entries(): ExternalIoEntry[];
74
+ }
75
+ /**
76
+ * Per-direction aggregate: numeric ONLY when the whole direction is known —
77
+ * `files` non-null with every byte numeric AND every external_io entry in this
78
+ * direction numeric. ANY null contributor → null (never a partial sum posing as
79
+ * a total). A fully-classified direction with no contributors → 0.
80
+ */
81
+ export declare function aggregateDirection(files: FileObservation[] | null, io: ExternalIoEntry[] | null, direction: FileDirection): number | null;
82
+ /**
83
+ * Build a validated pricing_facts object, dropping fields that are not present
84
+ * as non-negative integers and enforcing the two invariants: reasoning ≤ output,
85
+ * and (5m + 1h) cache-write TTL counts ≤ the known cache-write total. Returns
86
+ * null when nothing valid is present. A recorded 0 is preserved (0 ≠ unknown).
87
+ */
88
+ export declare function buildPricingFacts(raw: {
89
+ cacheWrite5m?: unknown;
90
+ cacheWrite1h?: unknown;
91
+ reasoning?: unknown;
92
+ webSearch?: unknown;
93
+ webFetch?: unknown;
94
+ }, outputTokens: number | null, cacheWriteTotal: number | null): PricingFacts | null;
95
+ export declare function isInternalControlTool(name: unknown): boolean;
96
+ /** Record an opaque/uncertified tool call as unknown in BOTH directions. */
97
+ export declare function recordUnattributedBoth(io: ExternalIoSet, callId: string | null): void;
98
+ /**
99
+ * UTF-8 byte size of the content WRITTEN by a write call, taken from the call's
100
+ * OWN input (never the file on disk). Returns 0 for a genuinely empty write,
101
+ * null when no content field is present.
102
+ */
103
+ export declare function writeInputBytes(input: unknown): number | null;
104
+ /**
105
+ * Resolve `path` to its real location and require it to be a regular,
106
+ * non-symlink file contained by one of `roots`. Returns the real path, or null.
107
+ */
108
+ export declare function containedRealFile(path: string, roots: string[]): Promise<string | null>;
109
+ /**
110
+ * Read a JSONL file within the byte budget and hash its exact native bytes.
111
+ * Returns null when the file is missing, oversized, or changed underneath the
112
+ * read (dev/ino/size/mtime re-checked after) — a torn read is never reported as
113
+ * a clean revision.
114
+ */
115
+ export declare function jsonlRevision(path: string, limits: ReadLimits): Promise<{
116
+ revision: string;
117
+ data: Buffer;
118
+ } | null>;
119
+ /**
120
+ * Parse a complete JSONL buffer. Returns null when the file does not end on a
121
+ * record boundary (a live append caught mid-line) or any line is not a JSON
122
+ * object — a torn/partial file is never silently treated as complete.
123
+ */
124
+ export declare function parseJsonl<T>(data: Buffer, limits: ReadLimits): T[] | null;
125
+ /** Minimal read-only `node:sqlite` surface used by the sqlite-backed readers. */
126
+ export interface SqliteStatement {
127
+ all(...params: unknown[]): unknown[];
128
+ get(...params: unknown[]): unknown;
129
+ }
130
+ export interface SqliteDatabase {
131
+ close(): void;
132
+ exec(sql: string): void;
133
+ prepare(sql: string): SqliteStatement;
134
+ }
135
+ export type SqliteDatabaseConstructor = new (path: string, options?: {
136
+ open?: boolean;
137
+ readOnly?: boolean;
138
+ }) => SqliteDatabase;
@@ -0,0 +1,188 @@
1
+ import type { CopilotSessionUsage } from "./passive-readers/copilot.js";
2
+ export interface Source {
3
+ provider: string;
4
+ sessionid: string;
5
+ source_id: string;
6
+ }
7
+ export interface ReadLimits {
8
+ signal: AbortSignal;
9
+ maxBytes: number;
10
+ maxRows: number;
11
+ }
12
+ export type SourceState = {
13
+ status: "ready";
14
+ revision: string;
15
+ } | {
16
+ status: "partial" | "unsupported" | "unavailable";
17
+ };
18
+ /** A non-file external-I/O contributor for a response (phase 3 / bx2). File
19
+ * reads/writes stay in files_read/files_written and are NEVER duplicated here.
20
+ * An `unattributed` entry is a coverage marker (bytes=null, basis="unknown")
21
+ * that forces its whole direction's aggregate to NULL. */
22
+ export interface ExternalIoEntry {
23
+ direction: "read" | "write";
24
+ kind: "prompt" | "web" | "other" | "unattributed";
25
+ /** Native admission/call id for dedup; never a content body. */
26
+ id: string | null;
27
+ /** Bounded metadata ref (web URL sans credentials/query/fragment); null for
28
+ * prompt/unattributed; never a raw command/query/prompt body. */
29
+ ref: string | null;
30
+ bytes: number | null;
31
+ basis: "input_text" | "normalized_content" | "certified_counter" | "unknown";
32
+ }
33
+ /** Closed whitelist of native pricing/fidelity facts for an eventual lmprice
34
+ * join (note 387). Counts only — no rates, service tier/speed, credentials, or
35
+ * account info. `reasoning_output_tokens` is a subset of `tokens.output`; the
36
+ * cache-write TTL counts cannot exceed `tokens.input_cache_write`. */
37
+ export interface PricingFacts {
38
+ cache_write_5m_tokens?: number | null;
39
+ cache_write_1h_tokens?: number | null;
40
+ reasoning_output_tokens?: number | null;
41
+ web_search_requests?: number | null;
42
+ web_fetch_requests?: number | null;
43
+ }
44
+ export interface NativeResponseMetadata {
45
+ response_id: string;
46
+ timestamp: string | null;
47
+ model: string | null;
48
+ tokens: {
49
+ input_uncached: number | null;
50
+ input_cached: number | null;
51
+ input_cache_write: number | null;
52
+ output: number | null;
53
+ };
54
+ files_read: Array<{
55
+ path: string;
56
+ bytes: number | null;
57
+ }> | null;
58
+ files_written: Array<{
59
+ path: string;
60
+ bytes: number | null;
61
+ }> | null;
62
+ read_bytes?: number | null;
63
+ write_bytes?: number | null;
64
+ external_io?: ExternalIoEntry[] | null;
65
+ model_provider?: string | null;
66
+ pricing_facts?: PricingFacts | null;
67
+ }
68
+ export interface Discovery {
69
+ sources: Source[];
70
+ complete: boolean;
71
+ cursor: string | null;
72
+ }
73
+ export type Snapshot = {
74
+ status: "complete";
75
+ source: Source;
76
+ revision: string;
77
+ checkpoint: string | null;
78
+ responses: NativeResponseMetadata[];
79
+ /** Native session-scoped usage snapshots (currently: copilot's
80
+ * `session.shutdown` records) — surfaced BESIDE responses, never
81
+ * distributed into them, never a synthetic response. `undefined`/absent
82
+ * for providers with no such native concept; `null` for a provider that
83
+ * has the concept but this session has none yet (e.g. still live);
84
+ * present-and-non-empty only when real native snapshots exist. Every
85
+ * relevant native snapshot is retained (never "last wins") since a
86
+ * provider's own scope semantics may not cover the whole session. */
87
+ session_usage?: CopilotSessionUsage[] | null;
88
+ /** Explicit, additive coverage marker: `responses` above are real,
89
+ * identified, fully-valid rows — never fabricated or clamped-to-zero —
90
+ * but a KNOWN native-format gap means some other, uncapturable activity
91
+ * in this same source could not be attributed to a response (e.g. a
92
+ * Codex source mixing legacy `event_msg`/`token_count` telemetry
93
+ * alongside modern `token_usage_record`s: the old-format usage is
94
+ * conservatively treated as an unresolved gap whenever it co-occurs with
95
+ * captured rows — matching raw token counts is NOT proof of native
96
+ * identity, since distinct requests can share identical small integer
97
+ * counts, so a numeric match never clears this marker). Absent ⇒ legacy
98
+ * full-coverage semantics (nothing else exists to omit) —
99
+ * NEVER default a missing marker to "partial" or vice versa. A caller
100
+ * MUST reject any value other than "partial" here rather than guess a
101
+ * new meaning for an unrecognized marker. This is a coverage flag only:
102
+ * it never changes the validity of `responses`, and a caller retains
103
+ * and journals those rows while simply never advancing its incremental
104
+ * revision/retention watermark past a snapshot carrying this flag. */
105
+ capture_status?: "partial";
106
+ } | {
107
+ status: "partial" | "unsupported" | "unavailable";
108
+ };
109
+ /**
110
+ * Effective discovery-coverage certification (lmbi backfill-reader-contract.md).
111
+ * This is an explicit certification of what this reader instance actually
112
+ * covers, NOT a list inferred from rows already returned — a caller must be
113
+ * able to tell "no adapter for this provider" apart from "adapter exists, its
114
+ * store is genuinely empty."
115
+ */
116
+ export interface PassiveSessionReaderCapabilities {
117
+ /** All configured native account roots, no team/task/session filter. */
118
+ scope: "account";
119
+ /** Changes when extraction semantics OR effective declared root
120
+ * configuration change, so a caller knows to start a safe new traversal
121
+ * rather than trust a stale cursor/cache against new coverage. */
122
+ revision: string;
123
+ /** Providers this instance can discover/inspect/read at all (certified,
124
+ * even where token metering itself is only partial — see `metering`). */
125
+ providers: string[];
126
+ /** Per-provider metering completeness. Informational only — never an
127
+ * admission gate; an unlisted provider makes no metering claim either way. */
128
+ metering: Record<string, "per_response" | "partial" | "unavailable" | "session_only">;
129
+ }
130
+ export interface PassiveSessionReader {
131
+ discover(limits: ReadLimits & {
132
+ maxSources: number;
133
+ after?: string;
134
+ }): Promise<Discovery>;
135
+ inspect(source: Source, limits: ReadLimits): Promise<SourceState>;
136
+ read(source: Source, revision: string, limits: ReadLimits): Promise<Snapshot>;
137
+ capabilities(): PassiveSessionReaderCapabilities;
138
+ }
139
+ export interface PassiveSessionReaderOptions {
140
+ claudeProjectsDir?: string;
141
+ codexSessionDirs?: string[];
142
+ lmplayerDbPaths?: string[];
143
+ /** Overrides agy's default `$HOME/.gemini/antigravity-cli/conversations`. */
144
+ agyConversationsDir?: string;
145
+ /** Overrides dsh's default `~/.dsh/sessions` (or `$DSH_HOME/sessions`). */
146
+ dshHome?: string;
147
+ /** Overrides qwen's default live+backup project roots. */
148
+ qwenProjectsDirs?: string[];
149
+ /** Overrides opencode's default store paths (does NOT read `OPENCODE_DB`). */
150
+ opencodeDbPaths?: string[];
151
+ /** Overrides copilot's default `$HOME/.copilot/session-state`. */
152
+ copilotSessionStateDir?: string;
153
+ /** Overrides kimi's default `$KIMI_CODE_HOME`/`~/.kimi-code`. */
154
+ kimiHome?: string;
155
+ sqliteBusyTimeoutMs?: number;
156
+ /** Directory-traversal visit budget for discovery (see
157
+ * MAX_DISCOVERY_TRAVERSAL_VISITS). Overridable mainly so tests can exercise
158
+ * the truncation path without a pathological tree. */
159
+ maxDiscoveryTraversalVisits?: number;
160
+ }
161
+ export declare class NativePassiveSessionReader implements PassiveSessionReader {
162
+ private readonly claudeProjectsDir;
163
+ private readonly codexSessionDirs;
164
+ private readonly lmplayerDbPaths;
165
+ private readonly sqliteBusyTimeoutMs;
166
+ private readonly maxDiscoveryTraversalVisits;
167
+ private readonly agyReader;
168
+ private readonly dshReader;
169
+ private readonly qwenReader;
170
+ private readonly opencodeReader;
171
+ private readonly copilotReader;
172
+ private readonly kimiReader;
173
+ constructor(options?: PassiveSessionReaderOptions);
174
+ capabilities(): PassiveSessionReaderCapabilities;
175
+ discover(limits: ReadLimits & {
176
+ maxSources: number;
177
+ after?: string;
178
+ }): Promise<Discovery>;
179
+ inspect(source: Source, limits: ReadLimits): Promise<SourceState>;
180
+ read(source: Source, revision: string, limits: ReadLimits): Promise<Snapshot>;
181
+ private discoverProvider;
182
+ private jsonlSourceMatches;
183
+ private openLmplayer;
184
+ private discoverLmplayer;
185
+ private inspectLmplayer;
186
+ private readLmplayer;
187
+ }
188
+ export declare function createPassiveSessionReader(options?: PassiveSessionReaderOptions): PassiveSessionReader;
@@ -0,0 +1 @@
1
+ export declare function canonicalPath(path: string): string;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * pi coding agent session provider.
3
+ *
4
+ * pi stores sessions as JSONL files under
5
+ * ~/.pi/agent/sessions/--<encoded-cwd>--/<timestamp>_<session-id>.jsonl
6
+ * where <encoded-cwd> is the cwd with the leading separator stripped and
7
+ * "/", "\\", ":" replaced by "-". The first line is a session header
8
+ * ({"type":"session","id":...,"cwd":...}); subsequent lines are tree-linked
9
+ * entries (message / model_change / compaction / ...). File format:
10
+ * packages/coding-agent/docs/session-format.md in the pi repo.
11
+ */
12
+ import type { SessionDetail, SessionProvider, SessionSummary } from "./types.js";
13
+ interface PiProviderOptions {
14
+ piDir?: string;
15
+ }
16
+ export declare class PiProvider implements SessionProvider {
17
+ name: "pi";
18
+ private baseDir;
19
+ constructor(opts?: PiProviderOptions);
20
+ getWatchPaths(): {
21
+ directory: string;
22
+ filePattern: RegExp;
23
+ }[];
24
+ discoverSessions(): Promise<SessionSummary[]>;
25
+ private listSessionFiles;
26
+ private readHeader;
27
+ private extractSummary;
28
+ loadSession(sessionId: string): Promise<SessionDetail | null>;
29
+ resolveSessionPath(sessionId: string): Promise<string | null>;
30
+ deleteSession(sessionId: string): Promise<boolean>;
31
+ }
32
+ export {};