@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.
- package/CHANGELOG.md +116 -0
- package/README.md +11 -51
- package/data/price-aliases.json +1 -0
- package/data/price-data.json +1 -1
- package/dist/cli/batch.d.ts +2 -0
- package/dist/cli/busy-evidence.d.ts +27 -0
- package/dist/cli/chat-cycle.d.ts +59 -0
- package/dist/cli/chat-exec.d.ts +83 -0
- package/dist/cli/chat-policy.d.ts +22 -0
- package/dist/cli/chat.d.ts +23 -0
- package/dist/cli/code-resolver.d.ts +8 -0
- package/dist/cli/common.d.ts +54 -0
- package/dist/cli/context.d.ts +12 -0
- package/dist/cli/cost.d.ts +2 -0
- package/dist/cli/export.d.ts +2 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.js +1062 -1400
- package/dist/cli/infra.d.ts +78 -0
- package/dist/cli/ls.d.ts +2 -0
- package/dist/cli/mailbox-cli.d.ts +6 -0
- package/dist/cli/managed.d.ts +62 -0
- package/dist/cli/mcp-install.d.ts +23 -0
- package/dist/cli/mcp.d.ts +2 -0
- package/dist/cli/paths.d.ts +50 -0
- package/dist/cli/plan-wizard.d.ts +42 -0
- package/dist/cli/plan.d.ts +20 -0
- package/dist/cli/queue-delivery.d.ts +107 -0
- package/dist/cli/ratelimit.d.ts +3 -0
- package/dist/cli/remote-fetch.d.ts +7 -0
- package/dist/cli/run-queries.d.ts +82 -0
- package/dist/cli/running.d.ts +5 -0
- package/dist/cli/schema.sql +20 -1
- package/dist/cli/script.d.ts +6 -0
- package/dist/cli/script.js +2097 -0
- package/dist/cli/self-identity.d.ts +25 -0
- package/dist/cli/session-idle.d.ts +118 -0
- package/dist/cli/status.d.ts +22 -0
- package/dist/cli/tail.d.ts +18 -0
- package/dist/cli/target-resolver.d.ts +139 -0
- package/dist/cli/team-status.d.ts +91 -0
- package/dist/cli/teamfile-compat.d.ts +71 -0
- package/dist/cli/teamfile-store.d.ts +18 -0
- package/dist/cli/teamfile-template.d.ts +40 -0
- package/dist/cli/template-env.d.ts +25 -0
- package/dist/cli/terminal-lock.d.ts +25 -0
- package/dist/cli/terminal.d.ts +23 -0
- package/dist/cli/time-filter.d.ts +10 -0
- package/dist/cli/update-notifier.d.ts +11 -0
- package/dist/cli/version-util.d.ts +2 -0
- package/dist/config/seed_templates.d.ts +1 -0
- package/dist/db/backfill-event-log.d.ts +26 -0
- package/dist/db/delegation.d.ts +37 -0
- package/dist/db/escalation_vocab.d.ts +45 -0
- package/dist/db/event-log.d.ts +40 -0
- package/dist/db/index.d.ts +1213 -0
- package/dist/db/inflight-acquire.d.ts +70 -0
- package/dist/db/invocation-event-projections.d.ts +65 -0
- package/dist/db/ledger-identity.test.d.ts +1 -0
- package/dist/db/live-event-projections.d.ts +92 -0
- package/dist/db/migrate.d.ts +51 -0
- package/dist/db/open-readonly.d.ts +2 -0
- package/dist/db/process-liveness.d.ts +37 -0
- package/dist/db/sqlite.d.ts +47 -0
- package/dist/db/sqlite.test.d.ts +1 -0
- package/dist/db/terminal-lock.d.ts +14 -0
- package/dist/db/tracked-invocation-reconcile.d.ts +52 -0
- package/dist/db/tracked-invocation.d.ts +116 -0
- package/dist/mcp/dispatch.d.ts +45 -0
- package/dist/mcp/mediation.d.ts +128 -0
- package/dist/mcp/path-warning.d.ts +7 -0
- package/dist/mcp/server.d.ts +63 -0
- package/dist/mcp/tools.d.ts +24 -0
- package/dist/providers/adapter.d.ts +265 -0
- package/dist/providers/adapters/agy-adapter.d.ts +17 -0
- package/dist/providers/adapters/claude-adapter.d.ts +20 -0
- package/dist/providers/adapters/codex-adapter.d.ts +22 -0
- package/dist/providers/adapters/copilot-adapter.d.ts +16 -0
- package/dist/providers/adapters/dsh-adapter.d.ts +89 -0
- package/dist/providers/adapters/gemini-adapter.d.ts +44 -0
- package/dist/providers/adapters/hermes-adapter.d.ts +71 -0
- package/dist/providers/adapters/index.d.ts +35 -0
- package/dist/providers/adapters/kimi-adapter.d.ts +16 -0
- package/dist/providers/adapters/lmplayer-adapter.d.ts +14 -0
- package/dist/providers/adapters/opencode-adapter.d.ts +11 -0
- package/dist/providers/adapters/pi-adapter.d.ts +17 -0
- package/dist/providers/adapters/print-turn.d.ts +29 -0
- package/dist/providers/adapters/qwen-adapter.d.ts +11 -0
- package/dist/providers/agy.d.ts +32 -0
- package/dist/providers/base-adapter.d.ts +107 -0
- package/dist/providers/base.d.ts +12 -0
- package/dist/providers/binary-check.d.ts +102 -0
- package/dist/providers/bundle.d.ts +118 -0
- package/dist/providers/claude-mock.d.ts +90 -0
- package/dist/providers/claude.d.ts +21 -0
- package/dist/providers/codex-config.d.ts +15 -0
- package/dist/providers/codex.d.ts +25 -0
- package/dist/providers/config/model-selection-lint.d.ts +45 -0
- package/dist/providers/config/team-config.d.ts +183 -0
- package/dist/providers/config/types.d.ts +146 -0
- package/dist/providers/config/user-config.d.ts +88 -0
- package/dist/providers/copilot.d.ts +21 -0
- package/dist/providers/debug.d.ts +128 -0
- package/dist/providers/dsh-sidecar.d.ts +65 -0
- package/dist/providers/dsh-zstd.d.ts +27 -0
- package/dist/providers/dsh.d.ts +37 -0
- package/dist/providers/frontmatter.d.ts +46 -0
- package/dist/providers/gemini-session-reader.d.ts +151 -0
- package/dist/providers/gemini.d.ts +39 -0
- package/dist/providers/hermes.d.ts +90 -0
- package/dist/providers/index.d.ts +421 -0
- package/dist/providers/kimi.d.ts +37 -0
- package/dist/providers/model-catalog.d.ts +141 -0
- package/dist/providers/one-shot-timeout.d.ts +1 -0
- package/dist/providers/opencode-config.d.ts +62 -0
- package/dist/providers/opencode.d.ts +40 -0
- package/dist/providers/parsers/acp.d.ts +47 -0
- package/dist/providers/parsers/codex-app-server.d.ts +60 -0
- package/dist/providers/parsers/stream-json.d.ts +57 -0
- package/dist/providers/passive-readers/agy.d.ts +155 -0
- package/dist/providers/passive-readers/copilot.d.ts +173 -0
- package/dist/providers/passive-readers/dsh.d.ts +34 -0
- package/dist/providers/passive-readers/kimi.d.ts +44 -0
- package/dist/providers/passive-readers/opencode.d.ts +89 -0
- package/dist/providers/passive-readers/qwen.d.ts +151 -0
- package/dist/providers/passive-readers/shared.d.ts +138 -0
- package/dist/providers/passive-session-reader.d.ts +188 -0
- package/dist/providers/path-utils.d.ts +1 -0
- package/dist/providers/pi.d.ts +32 -0
- package/dist/providers/pricing.d.ts +107 -0
- package/dist/providers/qwen.d.ts +22 -0
- package/dist/providers/ratelimit-readings.d.ts +32 -0
- package/dist/providers/registry.d.ts +31 -0
- package/dist/providers/resume.d.ts +43 -0
- package/dist/providers/seed-instruction.d.ts +3 -0
- package/dist/providers/self-session-env.d.ts +21 -0
- package/dist/providers/session-limit.d.ts +9 -0
- package/dist/providers/strip-ansi.d.ts +5 -0
- package/dist/providers/transport-errors.d.ts +94 -0
- package/dist/providers/transport-lifecycle.d.ts +63 -0
- package/dist/providers/transport-wrappers.d.ts +110 -0
- package/dist/providers/transports/acp-profiles/config-option-step.d.ts +24 -0
- package/dist/providers/transports/acp-profiles/copilot.d.ts +12 -0
- package/dist/providers/transports/acp-profiles/gemini.d.ts +22 -0
- package/dist/providers/transports/acp-profiles/hermes.d.ts +27 -0
- package/dist/providers/transports/acp-profiles/index.d.ts +19 -0
- package/dist/providers/transports/acp-profiles/kimi.d.ts +24 -0
- package/dist/providers/transports/acp-profiles/opencode.d.ts +26 -0
- package/dist/providers/transports/acp-profiles/types.d.ts +258 -0
- package/dist/providers/transports/acp-rpc.d.ts +18 -0
- package/dist/providers/transports/acp.d.ts +215 -0
- package/dist/providers/transports/codex-app-server.d.ts +190 -0
- package/dist/providers/transports/hold-open.d.ts +120 -0
- package/dist/providers/transports/manager.d.ts +21 -0
- package/dist/providers/transports/stream-json.d.ts +162 -0
- package/dist/providers/transports/types.d.ts +56 -0
- package/dist/providers/types.d.ts +154 -0
- package/dist/providers/yolo-contract.d.ts +131 -0
- package/dist/script/adapter.d.ts +101 -0
- package/dist/script/batch-script.d.ts +7 -0
- package/dist/script/index.d.ts +3 -0
- package/dist/script/lmscript.d.ts +550 -0
- package/dist/shared/policy.d.ts +74 -0
- package/dist/shared-lib/db.d.ts +25 -0
- package/dist/shared-lib/passive-session-reader.d.ts +2 -0
- package/dist/shared-lib/provider-parse.d.ts +6 -0
- package/dist/templates/basedir.d.ts +52 -0
- package/dist/uri/lmctl_uri.d.ts +38 -0
- package/durable-memory/model_selection.md +122 -25
- package/examples/README.md +103 -0
- package/examples/anonymous-panel.lms +3 -0
- package/examples/batch-prompts.txt +9 -0
- package/examples/batchscript.lms +67 -0
- package/examples/n8n/agent-lib.lms +56 -0
- package/examples/n8n/git-lib.lms +93 -0
- package/examples/n8n/mock-support.db +0 -0
- package/examples/n8n/multi-agent-review.lms +107 -0
- package/examples/n8n/news-digest.lms +159 -0
- package/examples/n8n/rag-search.lms +163 -0
- package/examples/n8n/support-agent.lms +88 -0
- package/examples/n8n/ticket-triage.lms +156 -0
- package/package.json +13 -17
- 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
|
+
}
|