@lmctl-ai/lmctl 0.1.342 → 0.2.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 (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 +122 -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,128 @@
1
+ /**
2
+ * Global debug logging.
3
+ *
4
+ * Usage:
5
+ * initDebug(command) — call once at startup from the root action
6
+ * dlog("message") — append to ~/.lmctl/debug-YYYY-MM-DD-HH.log if debug, no-op otherwise
7
+ * isDebug() — check current state (re-reads config every 5s)
8
+ *
9
+ * Four sources, any one enables debug:
10
+ * 1. --verbose CLI flag (sets _cliDebug at startup)
11
+ * 2. LMCTL_DEBUG=1 env var (checked at startup)
12
+ * 3. debug = true in ~/.lmctl/config.toml (re-read with 5s TTL)
13
+ * 4. {"debug": true} in ~/.lmctl/config.json (re-read with 5s TTL)
14
+ *
15
+ * All debug output goes to ~/.lmctl/debug-YYYY-MM-DD-HH.log
16
+ * (one file per hour, append mode) — NOT stderr. stderr is consumed by the transport layer and progress
17
+ * formatting; debug lines on stderr get lost or mixed with progress
18
+ * events. The log file can be examined after the fact with
19
+ * `ls -lt ~/.lmctl/debug-*.log | head -1` to find the latest, then
20
+ * `tail -f` on it.
21
+ *
22
+ * The file-based config check means you can enable debug mid-session
23
+ * by adding 'debug = true' to config.toml or {"debug": true} to config.json — it takes effect within
24
+ * 5s without restarting any running processes.
25
+ */
26
+ interface Command {
27
+ opts(): Record<string, unknown>;
28
+ optsWithGlobals: () => Record<string, unknown>;
29
+ }
30
+ /**
31
+ * Initialize debug state. Call once at program startup.
32
+ * Reads from Commander --verbose flag or LMCTL_DEBUG env.
33
+ */
34
+ /**
35
+ * Force CLI-level debug on (the top-level `lmctl --debug` flag). Equivalent to
36
+ * `--verbose` / `LMCTL_DEBUG=1` but driven by the parsed CLI flag in
37
+ * src/cli/index.ts, before any subcommand's Commander instance exists. Also
38
+ * exports LMCTL_DEBUG=1 so spawned children / env-based checks inherit it.
39
+ */
40
+ export declare function enableCliDebug(): void;
41
+ export interface CliDebugStateSnapshot {
42
+ cliDebug: boolean;
43
+ lmctlDebugEnv: string | undefined;
44
+ }
45
+ export declare function snapshotCliDebugState(): CliDebugStateSnapshot;
46
+ export declare function restoreCliDebugState(snapshot: CliDebugStateSnapshot): void;
47
+ export declare function initDebug(command?: Command): void;
48
+ /**
49
+ * Check if debug mode is enabled. Cheap: the file-based check is
50
+ * cached with a 5-second TTL so repeated calls in a hot loop are free.
51
+ */
52
+ export declare function isDebug(): boolean;
53
+ export declare function isCliDebug(): boolean;
54
+ /**
55
+ * Write a structured JSONL event to the debug log file.
56
+ * Every line has `ts`, `pid`, and `event` base fields plus any
57
+ * additional fields from the event object.
58
+ *
59
+ * @param event Object with at least `event: string`. Extra fields
60
+ * are spread into the JSON record.
61
+ * @param opts `verbose: true` restricts to CLI-level debug only
62
+ * (--verbose or LMCTL_DEBUG=1, not config.toml).
63
+ */
64
+ export declare function debugLog(event: {
65
+ event: string;
66
+ [key: string]: unknown;
67
+ }, opts?: {
68
+ verbose?: boolean;
69
+ always?: boolean;
70
+ }): void;
71
+ /**
72
+ * Append a debug message. Wraps in JSONL `{"event":"legacy","msg":"..."}`.
73
+ * No-op if debug is disabled. Never throws.
74
+ */
75
+ export declare function dlog(...args: string[]): void;
76
+ /**
77
+ * Verbose-only debug log. Same as dlog() but only fires when debug was
78
+ * enabled via --verbose or LMCTL_DEBUG=1 (the CLI sources). Config.toml
79
+ * debug=true does NOT enable verbose — it only enables the default level.
80
+ *
81
+ * Use dlogv() for high-volume / low-value lines (transport request/response,
82
+ * idle warnings, CWD, seed status). Use dlog() for MCP lifecycle + chat
83
+ * invocation lines that matter even in config-only debug mode.
84
+ */
85
+ export declare function dlogv(...args: string[]): void;
86
+ /**
87
+ * Unconditional structured error log. Bypasses the `isDebug()` gate so
88
+ * transport-layer failures (errors, timeouts, aborts, unhandled
89
+ * incoming requests) are ALWAYS recorded to `~/.lmctl/debug-*.log` for
90
+ * post-mortem — without forcing the user to set `LMCTL_DEBUG=1` ahead
91
+ * of time. Motivated by the opencode06 hang where the chat ran for
92
+ * 42+ minutes with no diagnostic in the debug log.
93
+ *
94
+ * The convention for the event payload is the same as `debugLog`:
95
+ * { event: "transport_error" | "transport_timeout" | "transport_abort"
96
+ * | "acp_unhandled_request" | ...,
97
+ * provider: "claude"|"codex"|...,
98
+ * session_id_short?: string,
99
+ * error_class?: string,
100
+ * error_message?: string,
101
+ * elapsed_ms?: number,
102
+ * ...kind-specific fields }
103
+ *
104
+ * Never throws (inherits `debugLog`'s best-effort guarantee).
105
+ */
106
+ export declare function errorLog(event: {
107
+ event: string;
108
+ [key: string]: unknown;
109
+ }): void;
110
+ /**
111
+ * Remove old hourly debug log files, keeping the most recent `keep`
112
+ * files. Default: keep 24 (one day of hourly logs). Best-effort —
113
+ * never throws.
114
+ *
115
+ * @param keep Number of newest files to retain.
116
+ * @param lmctlDir Directory to scan; defaults to `~/.lmctl/`.
117
+ * Task 81 cleanup invokes this with a custom home
118
+ * so tests don't have to override HOME.
119
+ * @returns `{ kept, removed }` filenames in their respective
120
+ * buckets — both arrays are sorted ascending
121
+ * (oldest first within each bucket). Failure
122
+ * produces a partial / empty result; never throws.
123
+ */
124
+ export declare function rotateDebugLogs(keep?: number, lmctlDir?: string): {
125
+ kept: string[];
126
+ removed: string[];
127
+ };
128
+ export {};
@@ -0,0 +1,65 @@
1
+ /**
2
+ * dsh `web` profile sidecar manager.
3
+ *
4
+ * dsh's headless profile is one-shot-only (no session resume, verified in
5
+ * `backlog/DESIGN-add-dsh-provider.md`). The only proven continuity path is
6
+ * `dsh web`'s internal HTTP RPC surface: it can cold-resume any session
7
+ * (headless- or web-created) by caller-supplied id, and `session.prompt`
8
+ * genuinely carries context forward turn to turn — proven end-to-end in the
9
+ * design doc's research.
10
+ *
11
+ * This module owns exactly one `dsh web` child process per lmctl process,
12
+ * spawned lazily on first use and reused for every `provider=dsh` member's
13
+ * turns (sessions are addressed by id over RPC, so one sidecar can serve
14
+ * many members/teams concurrently — there is no per-member sidecar needed).
15
+ *
16
+ * Verified live (2026-08-23): `dsh web --no-open --port 0` prints exactly
17
+ * one line, `dsh web: http://127.0.0.1:<port>`, once it's ready to accept
18
+ * connections.
19
+ */
20
+ export interface DshRpcError {
21
+ message?: string;
22
+ code?: string;
23
+ [key: string]: unknown;
24
+ }
25
+ export declare class DshRpcCallError extends Error {
26
+ readonly method: string;
27
+ readonly detail: unknown;
28
+ constructor(method: string, detail: unknown);
29
+ }
30
+ export declare class DshSidecar {
31
+ readonly baseUrl: string;
32
+ private readonly child;
33
+ private constructor();
34
+ /** Test seam: a handle bound to an arbitrary base URL with no child
35
+ * process (alive reports false; shutdown is a no-op). */
36
+ static forTesting(baseUrl: string): DshSidecar;
37
+ /**
38
+ * POST to `${baseUrl}/api/<method>` wrapped in the required envelope
39
+ * `{type: "client-request", rpcId, method, payload}` — verified live
40
+ * (2026-08-23) that the server rejects unwrapped bodies with a
41
+ * self-describing zod validation error naming exactly these four
42
+ * fields as missing/wrong, for `session.prompt` specifically (an
43
+ * earlier reading of the design doc's research assumed this endpoint
44
+ * took an unwrapped body; that was wrong, corrected here against the
45
+ * real server).
46
+ */
47
+ rpc(method: string, payload: unknown): Promise<unknown>;
48
+ /** True while the sidecar's child process is still alive. */
49
+ get alive(): boolean;
50
+ shutdown(): void;
51
+ static spawn(env: NodeJS.ProcessEnv): Promise<DshSidecar>;
52
+ }
53
+ /**
54
+ * Lazily spawn (once) and return the shared `dsh web` sidecar for this
55
+ * lmctl process. If a previously-spawned sidecar's process has died, spawns
56
+ * a fresh one rather than returning a dead handle. A FAILED boot attempt
57
+ * must not poison future calls either (F2): the rejected promise is dropped
58
+ * from the cache so the next call retries with whatever environment it
59
+ * brings, instead of rethrowing the stale boot error forever.
60
+ *
61
+ * `spawnFn` is a test seam — production callers always use the default.
62
+ */
63
+ export declare function ensureDshSidecar(env: NodeJS.ProcessEnv, spawnFn?: (env: NodeJS.ProcessEnv) => Promise<DshSidecar>): Promise<DshSidecar>;
64
+ /** Test/shutdown seam — not used by normal adapter turns. */
65
+ export declare function shutdownDshSidecar(): Promise<void>;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Concatenated-frame zstd decoding for dsh session files.
3
+ *
4
+ * dsh's `dsh-session-persistence-jsonl` appends to `session.jsonl.zstd`
5
+ * in flush batches, producing MULTIPLE concatenated zstd frames in one
6
+ * file (verified against real sessions: 12 frames in a 25 KB file, frame
7
+ * descriptor 0x04 = windowed frames with a 4-byte content checksum).
8
+ * Node's `zstdDecompressSync` / `createZstdDecompress` decode only the
9
+ * FIRST frame and silently drop the rest — so the frames must be walked
10
+ * and split manually before decoding.
11
+ *
12
+ * Frame layout (https://github.com/facebook/zstd/blob/dev/doc/zstd_compression_format.md):
13
+ * magic (4) | frame header (variable) | blocks... | checksum (0|4)
14
+ * Frame header:
15
+ * descriptor (1) | window descriptor (0|1) | dict id (0|1|2|4) | content size (0|1|2|4|8)
16
+ * Block header (3 bytes LE): bit0 = last, bits1-2 = type, bits3-23 = size.
17
+ */
18
+ /**
19
+ * Split a buffer of concatenated zstd frames into per-frame slices.
20
+ * Trailing non-frame garbage (a torn append) is dropped silently.
21
+ */
22
+ export declare function splitZstdFrames(buf: Buffer): Buffer[];
23
+ /**
24
+ * Decode a dsh `session.jsonl.zstd` buffer (one or more concatenated
25
+ * zstd frames) to its full JSONL text.
26
+ */
27
+ export declare function decompressDshSessionFile(buf: Buffer): string;
@@ -0,0 +1,37 @@
1
+ /**
2
+ * DeepSeek Harness (`dsh`) session provider.
3
+ *
4
+ * Reads dsh's own on-disk session files directly (`~/.dsh/sessions/<mangled-
5
+ * cwd>/session-<id>/session.jsonl.zstd`) — same read path whether the
6
+ * session was created by the `headless` profile or the `web` sidecar this
7
+ * provider's adapter drives (verified in `backlog/DESIGN-add-dsh-provider.md`:
8
+ * both profiles share one session store).
9
+ *
10
+ * cwd mangling (verified live, 2026-08-23): the absolute cwd has its
11
+ * leading/trailing slashes stripped, remaining `/` replaced with `-`, then
12
+ * the whole thing wrapped in `--` on both ends — e.g. `/tmp/dsh-probe` ->
13
+ * `--tmp-dsh-probe--`. Since a sessionId alone doesn't reveal which mangled
14
+ * directory it lives under, lookups by id scan all of them.
15
+ */
16
+ import type { SessionDetail, SessionProvider, SessionSummary } from "./types.js";
17
+ interface DshProviderOptions {
18
+ dshHome?: string;
19
+ }
20
+ export declare class DshProvider implements SessionProvider {
21
+ name: "dsh";
22
+ private sessionsDir;
23
+ constructor(opts?: DshProviderOptions);
24
+ discoverSessions(): Promise<SessionSummary[]>;
25
+ loadSession(sessionId: string): Promise<SessionDetail | null>;
26
+ resolveSessionPath(sessionId: string): Promise<string | null>;
27
+ deleteSession(sessionId: string): Promise<boolean>;
28
+ /** Absolute path a fresh session for `cwd` will be written to, given a
29
+ * caller-chosen `sessionId` — used by the adapter to poll for the
30
+ * session file after `session.create` + `session.prompt`, without a
31
+ * discovery scan (it already knows both inputs). */
32
+ sessionFilePath(cwd: string, sessionId: string): string;
33
+ private findSessionDir;
34
+ private readRecords;
35
+ private readSummary;
36
+ }
37
+ export {};
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Legacy strict-subset YAML-style frontmatter for `.lmctl` files.
3
+ *
4
+ * Format (visually SKILL.md-like, parser hand-rolled — no js-yaml dep):
5
+ *
6
+ * ---
7
+ * title: Frontend Team
8
+ * description: Builds and maintains the frontend UI
9
+ * members: Lead, Coder, Reviewer
10
+ * ---
11
+ * # Frontend Team
12
+ * …body…
13
+ *
14
+ * Current `.lmctl` files do not require frontmatter. This parser is
15
+ * still used for older files and custom templates that choose to carry
16
+ * metadata. When a file does start with `---`, the block must list the
17
+ * three required keys (`title`, `description`, `members`) and close
18
+ * with `---` on its own line. Anything else — multi-line values, lists,
19
+ * unknown keys — is a hard error.
20
+ *
21
+ * `members` is a comma-separated alias list; whitespace around each
22
+ * is stripped, empty entries dropped (so `"a, , b"` parses as
23
+ * `["a", "b"]`; `""` parses as `[]`).
24
+ */
25
+ export interface Frontmatter {
26
+ title: string;
27
+ description: string;
28
+ /** Aliases as written, in source order. May be `[]` if the value
29
+ * was empty. Caller decides if empty is a problem. */
30
+ members: string[];
31
+ /** Character offset in the original text where the body starts —
32
+ * i.e., the character right after the closing `---` newline. Use
33
+ * `text.slice(bodyOffset)` to get the post-frontmatter content. */
34
+ bodyOffset: number;
35
+ }
36
+ export declare class FrontmatterError extends Error {
37
+ constructor(message: string);
38
+ }
39
+ /**
40
+ * Parse the strict-subset frontmatter block at the start of `text`.
41
+ *
42
+ * Throws `FrontmatterError` for any deviation: missing leading `---`,
43
+ * missing closing `---`, malformed key/value line, unknown key, or
44
+ * absent required key.
45
+ */
46
+ export declare function parseFrontmatter(text: string): Frontmatter;
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Format-aware reader for Gemini session files.
3
+ *
4
+ * Gemini ships TWO on-disk formats and both must coexist indefinitely:
5
+ *
6
+ * ── Old format ─ `.json`, single pretty-printed document
7
+ *
8
+ * {
9
+ * "sessionId": "<uuid>",
10
+ * "projectHash": "<sha>",
11
+ * "startTime": "...",
12
+ * "lastUpdated": "...",
13
+ * "messages": [
14
+ * { "id": "...", "timestamp": "...", "type": "user|gemini",
15
+ * "content": "...", "tokens": { ... } },
16
+ * ...
17
+ * ]
18
+ * }
19
+ *
20
+ * `tokens.input` is per-call prompt usage; cached prompt tokens
21
+ * are a subset carried in `tokens.cached`.
22
+ *
23
+ * ── New format ─ `.jsonl`, line-delimited
24
+ *
25
+ * Line 1 (metadata, no `messages`):
26
+ * {"sessionId":"...","projectHash":"...","startTime":"...",
27
+ * "lastUpdated":"...","kind":"main"}
28
+ *
29
+ * Subsequent lines are EITHER messages (same shape as old
30
+ * `messages[]` entries, with `content` possibly an array of
31
+ * `{text}` parts) OR `$set` patches that mutate metadata fields:
32
+ *
33
+ * {"id":"...","timestamp":"...","type":"user","content":[{"text":"..."}]}
34
+ * {"$set":{"lastUpdated":"..."}}
35
+ * {"id":"...","type":"gemini","tokens":{"input":N,...}}
36
+ *
37
+ * `$set` patches apply in order; last write wins. We reconstruct
38
+ * the equivalent of the old document's top-level fields plus a
39
+ * synthesized `messages[]` so downstream code stays unchanged.
40
+ *
41
+ * ── Detection (content-based, NOT extension-based) ─
42
+ *
43
+ * Try `JSON.parse(rawText)`. Success AND result has an array
44
+ * `messages` field → old shape. Anything else (parse throws, OR
45
+ * parsed object lacks `messages`) → JSONL. The extension is a
46
+ * hint, not a decision; a renamed `.jsonl` containing a single
47
+ * whole-doc JSON still parses as old, and a `.json`-named JSONL
48
+ * still falls through to the line-by-line path.
49
+ */
50
+ /**
51
+ * Result of `detectGeminiSessionFormat`. The third state — `"unknown"` —
52
+ * is Round 11 correction: rather than silently classifying garbage as
53
+ * one format and letting the parser fall over later, detection
54
+ * surfaces it explicitly so callers can emit a structured warning
55
+ * (`gemini_session_format_unknown`) and skip the file. Loud failure
56
+ * on future format drift instead of silent disappearance from
57
+ * `lmctl ls`.
58
+ */
59
+ export type GeminiSessionFormat = "json" | "jsonl" | "unknown";
60
+ /** Common shape returned by the parser regardless of source format.
61
+ * Mirrors the old document's top-level fields plus an optional
62
+ * `kind` (new-format marker). */
63
+ export interface ParsedGeminiSession {
64
+ sessionId?: string;
65
+ projectHash?: string;
66
+ startTime?: string;
67
+ lastUpdated?: string;
68
+ /** "main" | "compact" on JSONL files; absent on old JSON files. */
69
+ kind?: string;
70
+ messages: ParsedGeminiMessage[];
71
+ }
72
+ /** Loose message shape covering both formats — `tokens` and
73
+ * `thoughts` are the audit-relevant fields; `content` is the
74
+ * display-relevant one and may be a string or an array of parts. */
75
+ export interface ParsedGeminiMessage {
76
+ id?: string;
77
+ timestamp?: string;
78
+ type?: string;
79
+ content?: string | Array<{
80
+ text?: string;
81
+ }>;
82
+ thoughts?: Array<{
83
+ subject?: string;
84
+ description?: string;
85
+ timestamp?: string;
86
+ }>;
87
+ model?: string;
88
+ tokens?: {
89
+ input?: number;
90
+ output?: number;
91
+ cached?: number;
92
+ thoughts?: number;
93
+ tool?: number;
94
+ total?: number;
95
+ };
96
+ }
97
+ /**
98
+ * Detect the on-disk format of a Gemini session purely from its raw
99
+ * text content. Does NOT touch the filesystem and does NOT consult
100
+ * the file's extension.
101
+ *
102
+ * Detection order — JSONL FIRST (Round 11 correction):
103
+ *
104
+ * 1. JSONL fast path: file has multiple non-blank lines AND line
105
+ * 1 parses as a JSON object with a `kind` field (the de-facto
106
+ * version marker for the new format). This is cheaper than a
107
+ * whole-file parse on large old-format documents and matches
108
+ * the format users will increasingly see going forward.
109
+ * 2. Whole-file JSON parse: if the file parses as a single object,
110
+ * classify by content — `messages` array means old format;
111
+ * a `kind` field (no messages yet) means a fresh JSONL session
112
+ * that's just one metadata line so far.
113
+ * 3. Anything else → `"unknown"`.
114
+ *
115
+ * Empty / blank input also returns `"unknown"` — callers emit the
116
+ * `gemini_session_format_unknown` warning and skip the file. Throwing
117
+ * here would force every caller to wrap in try/catch and risks the
118
+ * "silently dropped session" failure mode this round was opened to fix.
119
+ */
120
+ export declare function detectGeminiSessionFormat(raw: string): GeminiSessionFormat;
121
+ /**
122
+ * Parse a Gemini session file's raw text into a uniform shape.
123
+ * Detects format via `detectGeminiSessionFormat`; old documents are
124
+ * returned as-is, new JSONL is replayed line-by-line into the
125
+ * equivalent old shape (metadata + synthesized `messages[]`).
126
+ *
127
+ * Throws when format is unknown (empty, malformed, or matching
128
+ * neither known shape). Callers — production code paths that read
129
+ * sessions — should detect with `detectGeminiSessionFormat` FIRST
130
+ * and emit a `gemini_session_format_unknown` warning before letting
131
+ * this function throw, so users see "which file was skipped, why"
132
+ * in the debug log.
133
+ *
134
+ * Individual subsequent JSONL lines that fail to parse (after the
135
+ * metadata line) are skipped silently — partial corruption shouldn't
136
+ * take out the rest of the session.
137
+ */
138
+ export declare function parseGeminiSessionContent(raw: string): ParsedGeminiSession;
139
+ /**
140
+ * Serialize a ParsedGeminiSession back to disk text in the requested
141
+ * format. Used by the fork path to preserve the source file's format
142
+ * (forking a `.jsonl` produces a `.jsonl`; forking a `.json` produces
143
+ * a `.json`).
144
+ *
145
+ * For JSONL output we DO NOT emit `$set` patches — those are append-
146
+ * only history that gemini writes incrementally; a fresh fork has
147
+ * already merged everything into its top-level metadata, so the
148
+ * minimal valid JSONL (metadata line + message lines) round-trips
149
+ * cleanly through gemini's own loader.
150
+ */
151
+ export declare function serializeGeminiSession(session: ParsedGeminiSession, format: GeminiSessionFormat): string;
@@ -0,0 +1,39 @@
1
+ import type { SessionDetail, SessionProvider, SessionSummary } from "./types.js";
2
+ interface GeminiProviderOptions {
3
+ geminiDir?: string;
4
+ claudeDir?: string;
5
+ codexDirs?: string[];
6
+ codexDir?: string;
7
+ codeDir?: string;
8
+ extraWorkspacePaths?: string[];
9
+ }
10
+ export declare class GeminiProvider implements SessionProvider {
11
+ name: "gemini";
12
+ private workspaceHashMap?;
13
+ private geminiProjectsFile;
14
+ private geminiTmpDir;
15
+ private claudeProjectsDir;
16
+ private codexSessionDirs;
17
+ private extraWorkspacePaths;
18
+ constructor(options?: GeminiProviderOptions);
19
+ getWatchPaths(): {
20
+ directory: string;
21
+ filePattern: RegExp;
22
+ }[];
23
+ discoverSessions(): Promise<SessionSummary[]>;
24
+ loadSession(sessionId: string): Promise<SessionDetail | null>;
25
+ private extractSummary;
26
+ private readSessionFile;
27
+ private extractSessionIdFromFilename;
28
+ private findSessionFiles;
29
+ resolveSessionPath(sessionId: string): Promise<string | null>;
30
+ deleteSession(sessionId: string): Promise<boolean>;
31
+ private resolveSessionFile;
32
+ private resolveWorkspace;
33
+ private getWorkspaceHashMap;
34
+ private collectGeminiProjectPaths;
35
+ private collectCodexCwds;
36
+ private collectClaudeCwds;
37
+ private findJsonlFiles;
38
+ }
39
+ export {};
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Hermes Agent ("hermes") session provider.
3
+ *
4
+ * Hermes persists every conversation in ONE SQLite store,
5
+ * `<HERMES_HOME>/state.db` (default `~/.hermes`), whose `sessions` table
6
+ * carries a `source` discriminator (`cli` / gateway platforms / `acp`).
7
+ * The ACP server (`hermes acp`) writes `source='acp'` rows, and its own
8
+ * resume path refuses anything else (`acp_adapter/session.py:341`:
9
+ * `row.get("source") != "acp"` ⇒ no restore). This reader mirrors that
10
+ * contract exactly: ACP-source rows only.
11
+ *
12
+ * Shape facts this reader depends on, each verified against a real store
13
+ * and against `acp_adapter/session.py`:
14
+ *
15
+ * 1. An ACP session only becomes a row once it has history
16
+ * (`_persist()` returns early on empty history), so a session that
17
+ * was created but never prompted does not exist for discovery.
18
+ * 2. Assistant rows keep tool calls in a JSON `tool_calls` column
19
+ * (`[{id, call_id, function:{name, arguments}}]`) while results land
20
+ * on separate `role='tool'` rows keyed by `tool_call_id`. Emitting
21
+ * both keeps tool-call/result pairing intact for session inspection.
22
+ * 3. A resume replays the `active = 1` set only
23
+ * (`get_messages_as_conversation` defaults to `include_inactive=False`),
24
+ * so the transcript here does too: compaction archives pre-summary
25
+ * turns and a rewind flags taken-back rows `active = 0`, and neither
26
+ * belongs in the conversation a resumed member sees.
27
+ *
28
+ * Schema contract: every column both queries name exists in Hermes's real
29
+ * `SCHEMA_SQL` (`hermes_state_common.py`) — asserted against a store a real
30
+ * `SessionDB` produced, not a hand-written fixture
31
+ * (`tests/providers/hermes-real-schema.test.ts`).
32
+ *
33
+ * Token accounting: the `sessions` row carries aggregate counters only
34
+ * (input/output/cache-read/cache-write/reasoning), which is the same
35
+ * "partial" standard as codex/gemini/qwen.
36
+ */
37
+ import type { SessionProvider, SessionSummary, SessionDetail } from "./types.js";
38
+ export interface HermesProviderOptions {
39
+ /**
40
+ * Explicit effective HERMES_HOME. Defaults to `$HERMES_HOME`, then
41
+ * `~/.hermes`. A PROFILE is a different home (`<root>/profiles/<name>`
42
+ * — see `hermes_constants.get_default_hermes_root`), so a caller
43
+ * selecting a profile passes that profile's home path here; there is no
44
+ * separate profile knob and `HERMES_PROFILE` is only the profile's
45
+ * display NAME, never a path segment.
46
+ */
47
+ hermesHome?: string;
48
+ /** Read-only connection `PRAGMA busy_timeout` in ms. Default 5000. */
49
+ busyTimeoutMs?: number;
50
+ /** Max sessions returned by discovery. Default 200. */
51
+ discoverLimit?: number;
52
+ }
53
+ /**
54
+ * Resolve HERMES_HOME the way `hermes_constants.get_hermes_home()` does:
55
+ * an explicit override, then the `HERMES_HOME` env var, then the platform
56
+ * default `~/.hermes`.
57
+ */
58
+ export declare function resolveHermesHome(opts?: {
59
+ hermesHome?: string;
60
+ }): string;
61
+ /** `<HERMES_HOME>/state.db` — the one store every Hermes session lives in. */
62
+ export declare function resolveHermesStateDbPath(opts?: HermesProviderOptions): string;
63
+ export declare class HermesProvider implements SessionProvider {
64
+ readonly name: "hermes";
65
+ private readonly dbPath;
66
+ private readonly busyTimeoutMs;
67
+ private readonly discoverLimit;
68
+ constructor(opts?: HermesProviderOptions);
69
+ /** Exposed for diagnostics (`lmctl ls --provider hermes` shows nothing
70
+ * when this path does not exist, which is the common first-run state). */
71
+ get stateDbPath(): string;
72
+ private openDb;
73
+ private selectSessionRow;
74
+ discoverSessions(): Promise<SessionSummary[]>;
75
+ /**
76
+ * Read the live (`active = 1`) transcript rows for a session.
77
+ *
78
+ * Same set a Hermes resume replays: `active = 0` rows are turns a rewind
79
+ * took back, and the pre-compaction turns an archive flagged
80
+ * `active = 0, compacted = 1` were replaced by the summary in the
81
+ * conversation the member sees. A store old enough to predate the column
82
+ * has no archived rows either, so the fallback is faithful, not lenient.
83
+ */
84
+ private activeMessageRows;
85
+ /** Read only the preview endpoints, not every transcript body. */
86
+ private previewTexts;
87
+ loadSession(sessionId: string): Promise<SessionDetail | null>;
88
+ resolveSessionPath(sessionId: string): Promise<string | null>;
89
+ deleteSession(_sessionId: string): Promise<boolean>;
90
+ }