@tangle-network/sdk-provider-cli-base 0.13.17 → 0.13.19

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.
@@ -0,0 +1,244 @@
1
+ import { G as isLocalMcpConfig, H as CliProviderAdapterBase, I as plaintextResponseParser, K as localMcpArgv, Tt as sessionObservedEvents, V as recordCliTokenUsage, it as partitionImageAttachments, ot as prependPromptSection, q as localMcpEnv } from "../../src-Fn8Pu1xq.mjs";
2
+ import { resolveTangleRouterDefaults } from "@tangle-network/agent-interface";
3
+ import { lchown, mkdir, writeFile } from "node:fs/promises";
4
+ import { dirname, join } from "node:path";
5
+ import { DatabaseSync } from "node:sqlite";
6
+ //#region src/adapters/hermes/adapter.ts
7
+ /** Hermes provider name to the env var that holds its API key. */
8
+ const PROVIDER_API_KEY_ENV = {
9
+ auto: "OPENROUTER_API_KEY",
10
+ openrouter: "OPENROUTER_API_KEY",
11
+ anthropic: "ANTHROPIC_API_KEY",
12
+ "openai-codex": "OPENAI_API_KEY",
13
+ nous: "NOUS_API_KEY",
14
+ custom: "OPENAI_API_KEY"
15
+ };
16
+ /**
17
+ * Maps a hermes inference provider to the env var hermes consults for the
18
+ * provider base URL. Used to point hermes at tangle-router or a self-hosted
19
+ * OpenAI-compatible endpoint without writing config files at runtime.
20
+ */
21
+ const PROVIDER_BASE_URL_ENV = {
22
+ auto: null,
23
+ openrouter: "OPENROUTER_BASE_URL",
24
+ anthropic: "ANTHROPIC_BASE_URL",
25
+ "openai-codex": "OPENAI_BASE_URL",
26
+ nous: null,
27
+ custom: "OPENAI_BASE_URL"
28
+ };
29
+ var HermesProviderAdapter = class extends CliProviderAdapterBase {
30
+ provider = "hermes";
31
+ profileHarness = "hermes";
32
+ activeSessionId;
33
+ defaultCliPath = "hermes";
34
+ isBenignNonZeroExit({ exitCode, stderr, state }) {
35
+ if (exitCode === null || exitCode === 0) return false;
36
+ if (state.textBuffer.trim().length > 0) return true;
37
+ return /session_id:\s*\S/.test(stderr);
38
+ }
39
+ buildMcpAddArgs(name, config) {
40
+ const args = [
41
+ "mcp",
42
+ "add",
43
+ name
44
+ ];
45
+ if (!isLocalMcpConfig(config)) return [
46
+ ...args,
47
+ "--url",
48
+ config.url
49
+ ];
50
+ const [command, ...commandArgs] = localMcpArgv(config);
51
+ args.push("--command", command);
52
+ if (commandArgs.length > 0) args.push("--args", ...commandArgs);
53
+ const environment = localMcpEnv(config);
54
+ if (environment && Object.keys(environment).length > 0) args.push("--env", ...Object.entries(environment).map(([key, value]) => `${key}=${value}`));
55
+ return args;
56
+ }
57
+ /** The contract store, or a workspace fallback when no runtime home is set. */
58
+ resolveHermesHome() {
59
+ const runtimeHome = this.config.runtimeHome?.trim();
60
+ if (runtimeHome) return join(runtimeHome, ".hermes");
61
+ if (!this.activeSessionId) throw new Error("Hermes native session id is required before resolving HERMES_HOME");
62
+ return join(process.env.AGENT_WORKSPACE_ROOT?.trim() || this.storagePath, ".hermes", "sessions", this.activeSessionId);
63
+ }
64
+ async preparePromptInput(parts, workspaceRoot, sessionId) {
65
+ if (!/^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/.test(sessionId)) throw new Error("Hermes native session id is not a safe directory name");
66
+ if (this.activeSessionId && this.activeSessionId !== sessionId) throw new Error("Hermes backend cannot serve two native sessions");
67
+ this.activeSessionId = sessionId;
68
+ const home = this.resolveHermesHome();
69
+ const created = await mkdir(home, { recursive: true });
70
+ await writeFile(join(home, "config.yaml"), "agent:\n auto_recovery_cycles: 0\n", { flag: "wx" }).catch(() => void 0);
71
+ const owner = this.subprocessIdentity();
72
+ if (owner) {
73
+ const sessions = dirname(home);
74
+ const top = created !== void 0 && sessions.startsWith(`${created}/`) ? created : sessions;
75
+ for (let dir = home;; dir = dirname(dir)) {
76
+ await lchown(dir, owner.uid, owner.gid);
77
+ if (dir === top) break;
78
+ }
79
+ }
80
+ return super.preparePromptInput(parts, workspaceRoot, sessionId, { inlineImageBudgetBytes: 0 });
81
+ }
82
+ nativeAttachmentKinds() {
83
+ return ["image"];
84
+ }
85
+ buildExecArgs(params) {
86
+ const args = ["chat", "--quiet"];
87
+ const gatewayDefaults = resolveTangleRouterDefaults();
88
+ const configuredProvider = params.config.inferenceProvider;
89
+ const defaultThroughGateway = !configuredProvider || configuredProvider === "openrouter" || configuredProvider === "custom" || configuredProvider === "openai-codex";
90
+ const model = params.model ?? params.config.model ?? (defaultThroughGateway ? gatewayDefaults.model : void 0);
91
+ if (model) {
92
+ args.push("--model", model);
93
+ const provider = configuredProvider ?? "openrouter";
94
+ if (provider) args.push("--provider", provider);
95
+ }
96
+ if (params.config.toolsets?.length) args.push("--toolsets", params.config.toolsets.join(","));
97
+ if (params.sessionId) {
98
+ this.releaseTurnLease(params.sessionId);
99
+ args.push("--resume", params.sessionId);
100
+ }
101
+ if (params.config.bypassPermissions !== false) args.push("--yolo");
102
+ if (params.config.extraFlags?.length) args.push(...params.config.extraFlags);
103
+ const { native: nativeImages, message: messageWithStrandedMarkers } = partitionImageAttachments({
104
+ message: params.message,
105
+ attachments: params.attachments,
106
+ qualifies: (attachment) => attachment.path !== void 0,
107
+ maxNative: 1
108
+ });
109
+ const nativePath = nativeImages[0]?.path;
110
+ const fullMessage = prependPromptSection(params.systemPrompt, messageWithStrandedMarkers);
111
+ if (nativePath) args.push("--image", nativePath);
112
+ args.push("-q", fullMessage);
113
+ return {
114
+ command: this.getCliPath(),
115
+ args,
116
+ useStdin: false
117
+ };
118
+ }
119
+ releaseTurnLease(sessionId) {
120
+ const home = this.config.runtimeHome?.trim();
121
+ if (!home) return;
122
+ try {
123
+ const db = new DatabaseSync(join(home, ".hermes", "state.db"), { timeout: 2e3 });
124
+ try {
125
+ db.prepare("DELETE FROM session_turn_leases WHERE conversation_id = ?").run(sessionId);
126
+ } finally {
127
+ db.close();
128
+ }
129
+ } catch {
130
+ return;
131
+ }
132
+ }
133
+ buildExecEnv(config) {
134
+ const env = this.agentEnvironmentValues();
135
+ env.HERMES_HOME = this.resolveHermesHome();
136
+ const authMode = config.authMode ?? (config.apiKey ? "api-key" : "oauth");
137
+ const gatewayDefaults = resolveTangleRouterDefaults();
138
+ const configuredProvider = config.inferenceProvider;
139
+ const provider = configuredProvider ?? "openrouter";
140
+ const defaultThroughGateway = !configuredProvider || configuredProvider === "openrouter" || configuredProvider === "custom" || configuredProvider === "openai-codex";
141
+ if (authMode === "api-key" && config.apiKey) {
142
+ const keyEnv = PROVIDER_API_KEY_ENV[provider];
143
+ if (keyEnv) env[keyEnv] = config.apiKey;
144
+ }
145
+ const baseUrl = config.baseUrl || (defaultThroughGateway ? gatewayDefaults.baseUrl : void 0);
146
+ if (baseUrl) {
147
+ const urlEnv = PROVIDER_BASE_URL_ENV[provider];
148
+ if (urlEnv) env[urlEnv] = baseUrl;
149
+ }
150
+ const model = config.model ?? (defaultThroughGateway ? gatewayDefaults.model : void 0);
151
+ if (model) env.HERMES_INFERENCE_MODEL = model;
152
+ return env;
153
+ }
154
+ async afterCliProcessExit(params) {
155
+ const hermesSessionId = parseHermesSessionId(params.stderr);
156
+ if (!hermesSessionId) return;
157
+ for (const event of sessionObservedEvents(params.state, "hermes", hermesSessionId)) params.onEvent?.(event);
158
+ if (!params.exit.success) return;
159
+ const exported = await this.runCli({
160
+ command: this.getCliPath(),
161
+ args: [
162
+ "sessions",
163
+ "export",
164
+ "--session-id",
165
+ hermesSessionId,
166
+ "-"
167
+ ],
168
+ cwd: params.workspaceRoot,
169
+ env: params.env,
170
+ timeoutMs: 15e3
171
+ });
172
+ if (!exported.exit.success) {
173
+ this.logger.warn("hermes: session export failed after run", {
174
+ sessionId: hermesSessionId,
175
+ exitCode: exported.exit.code,
176
+ stderr: exported.stderr.slice(0, 500)
177
+ });
178
+ return;
179
+ }
180
+ let session;
181
+ try {
182
+ session = parseHermesExport(exported.stdout ?? "");
183
+ } catch (error) {
184
+ this.logger.warn("hermes: session export returned invalid JSON", {
185
+ sessionId: hermesSessionId,
186
+ error: error instanceof Error ? error.message : String(error)
187
+ });
188
+ return;
189
+ }
190
+ if (!session) {
191
+ this.logger.warn("hermes: session export did not return JSON", { sessionId: hermesSessionId });
192
+ return;
193
+ }
194
+ recordCliTokenUsage(params.state, {
195
+ usage: {
196
+ input_tokens: session.input_tokens,
197
+ output_tokens: session.output_tokens,
198
+ cache_read_input_tokens: session.cache_read_tokens,
199
+ cache_creation_input_tokens: session.cache_write_tokens
200
+ },
201
+ cost_usd: session.actual_cost_usd ?? session.estimated_cost_usd
202
+ });
203
+ }
204
+ createEventParser(state, onEvent) {
205
+ return plaintextResponseParser(state, onEvent, {
206
+ classify: (line) => line.startsWith("API call failed") || line.startsWith("Error:") || line.startsWith("hermes:") ? {
207
+ type: "error",
208
+ message: line
209
+ } : {
210
+ type: "text",
211
+ line
212
+ },
213
+ map: (event, response) => {
214
+ switch (event.type) {
215
+ case "text":
216
+ response.text(event.line);
217
+ return [];
218
+ case "error":
219
+ response.error(event.message);
220
+ return [{
221
+ type: "warning",
222
+ code: "HERMES_ERROR",
223
+ message: event.message
224
+ }];
225
+ case "complete": return [];
226
+ }
227
+ },
228
+ emptyFailureDetail: "hermes errored without producing a response body"
229
+ });
230
+ }
231
+ };
232
+ function parseHermesSessionId(stderr) {
233
+ return /(?:^|\n)\s*session_id:\s*([^\s]+)/i.exec(stderr)?.[1];
234
+ }
235
+ function parseHermesExport(stdout) {
236
+ const line = stdout.split(/\r?\n/).map((entry) => entry.trim()).find(Boolean);
237
+ if (!line) return void 0;
238
+ const parsed = JSON.parse(line);
239
+ return typeof parsed === "object" && parsed !== null ? parsed : void 0;
240
+ }
241
+ //#endregion
242
+ export { HermesProviderAdapter };
243
+
244
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1,50 @@
1
+ import { Mt as CliExecArgs, Pt as CliProviderConfig, Rt as ExecutionState, ht as CliProviderAdapterBase, n as HarnessId } from "../../index-BpO3fvbf.mjs";
2
+ import { StreamParserConfig } from "@tangle-network/sdk-cli-runner";
3
+ import { StreamEvent } from "@tangle-network/agent-interface";
4
+
5
+ //#region src/adapters/nanoclaw/types.d.ts
6
+ type NanoclawAuthMode = "api-key" | "oauth";
7
+ interface NanoclawConfig extends CliProviderConfig {
8
+ projectRoot?: string;
9
+ apiKey?: string;
10
+ authMode?: NanoclawAuthMode;
11
+ baseUrl?: string;
12
+ }
13
+ interface NanoclawTextEvent {
14
+ type: "text";
15
+ line: string;
16
+ }
17
+ interface NanoclawErrorEvent {
18
+ type: "error";
19
+ message: string;
20
+ }
21
+ interface NanoclawUsageEvent {
22
+ type: "usage";
23
+ usage: Record<string, unknown>;
24
+ raw: string;
25
+ }
26
+ interface NanoclawSessionEvent {
27
+ type: "session";
28
+ id: string;
29
+ }
30
+ type NanoclawEvent = NanoclawTextEvent | NanoclawErrorEvent | NanoclawUsageEvent | NanoclawSessionEvent;
31
+ //#endregion
32
+ //#region src/adapters/nanoclaw/adapter.d.ts
33
+ declare class NanoclawProviderAdapter extends CliProviderAdapterBase<NanoclawConfig, NanoclawEvent> {
34
+ readonly provider = "nanoclaw";
35
+ protected readonly profileHarness: HarnessId;
36
+ protected readonly defaultCliPath = "tsx";
37
+ protected buildExecArgs(params: {
38
+ message: string;
39
+ config: NanoclawConfig;
40
+ model?: string;
41
+ sessionId?: string;
42
+ workspaceRoot: string;
43
+ systemPrompt?: string;
44
+ }): CliExecArgs;
45
+ protected buildExecEnv(config: NanoclawConfig): Record<string, string | undefined>;
46
+ protected createEventParser(state: ExecutionState, onEvent?: (event: StreamEvent) => void): StreamParserConfig<NanoclawEvent>;
47
+ }
48
+ //#endregion
49
+ export { type NanoclawConfig, type NanoclawEvent, NanoclawProviderAdapter };
50
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1,116 @@
1
+ import { H as CliProviderAdapterBase, I as plaintextResponseParser, Tt as sessionObservedEvents, V as recordCliTokenUsage, ot as prependPromptSection, z as parseCliUsageLine } from "../../src-Fn8Pu1xq.mjs";
2
+ import { normalizeOpenAiCompatibleBaseUrl, resolveTangleRouterDefaults } from "@tangle-network/agent-interface";
3
+ import { join } from "node:path";
4
+ //#region src/adapters/nanoclaw/adapter.ts
5
+ var NanoclawProviderAdapter = class extends CliProviderAdapterBase {
6
+ provider = "nanoclaw";
7
+ profileHarness = "nanoclaw";
8
+ defaultCliPath = "tsx";
9
+ buildExecArgs(params) {
10
+ const projectRoot = params.config.projectRoot ?? process.env.NANOCLAW_PROJECT_ROOT ?? params.workspaceRoot;
11
+ const fullMessage = prependPromptSection(params.systemPrompt, params.message);
12
+ return {
13
+ command: join(projectRoot, "node_modules", ".bin", "tsx"),
14
+ args: [
15
+ join(projectRoot, "scripts", "chat.ts"),
16
+ "nanoclaw-chat",
17
+ params.sessionId ?? "-",
18
+ fullMessage
19
+ ],
20
+ cwd: projectRoot,
21
+ useStdin: false
22
+ };
23
+ }
24
+ buildExecEnv(config) {
25
+ const env = this.agentEnvironmentValues();
26
+ if (config.projectRoot) env.NANOCLAW_PROJECT_ROOT = config.projectRoot;
27
+ env.NANOCLAW_NATIVE_CREDENTIALS ??= "true";
28
+ env.COREPACK_ENABLE_DOWNLOAD_PROMPT = "0";
29
+ const modelKey = config.apiKey ?? env.OPENCODE_MODEL_API_KEY;
30
+ const authMode = config.authMode ?? (modelKey ? "api-key" : "oauth");
31
+ if (authMode === "api-key" && modelKey) {
32
+ env.OPENCODE_MODEL_API_KEY = modelKey;
33
+ env.OPENAI_API_KEY = modelKey;
34
+ env.OPENROUTER_API_KEY = modelKey;
35
+ env.ANTHROPIC_AUTH_TOKEN = modelKey;
36
+ } else if (authMode !== "api-key") {
37
+ env.OPENCODE_MODEL_API_KEY = void 0;
38
+ env.OPENAI_API_KEY = void 0;
39
+ env.OPENROUTER_API_KEY = void 0;
40
+ env.ANTHROPIC_AUTH_TOKEN = void 0;
41
+ }
42
+ const gatewayDefaults = resolveTangleRouterDefaults(env);
43
+ const openAiBaseUrl = config.baseUrl ? normalizeOpenAiCompatibleBaseUrl(config.baseUrl) : gatewayDefaults.baseUrl;
44
+ env.OPENAI_BASE_URL ??= openAiBaseUrl;
45
+ env.OPENROUTER_BASE_URL ??= openAiBaseUrl;
46
+ env.ANTHROPIC_BASE_URL ??= resolveAnthropicBaseUrl(config.baseUrl ?? gatewayDefaults.rootUrl);
47
+ const model = config.model ?? gatewayDefaults.model;
48
+ if (model) env.TANGLE_ROUTER_MODEL = model;
49
+ return env;
50
+ }
51
+ createEventParser(state, onEvent) {
52
+ return plaintextResponseParser(state, onEvent, {
53
+ classify: classifyNanoclawLine,
54
+ map: (event, response) => {
55
+ switch (event.type) {
56
+ case "text":
57
+ response.text(event.line);
58
+ return [];
59
+ case "session":
60
+ if (state.sessionId === event.id) return [];
61
+ return sessionObservedEvents(state, "nanoclaw", event.id);
62
+ case "error":
63
+ response.error(event.message);
64
+ return [{
65
+ type: "warning",
66
+ code: "NANOCLAW_ERROR",
67
+ message: event.message
68
+ }];
69
+ case "usage":
70
+ recordCliTokenUsage(state, event.usage);
71
+ return [];
72
+ }
73
+ },
74
+ emptyFailureDetail: "nanoclaw exited without producing a response body"
75
+ });
76
+ }
77
+ };
78
+ function classifyNanoclawLine(line) {
79
+ if (line.length === 0) return null;
80
+ if (line.startsWith("nanoclaw-session:")) {
81
+ const id = line.slice(17).trim();
82
+ return id ? {
83
+ type: "session",
84
+ id
85
+ } : null;
86
+ }
87
+ if (line.startsWith("usage:")) {
88
+ try {
89
+ const usage = parseCliUsageLine(line);
90
+ if (usage) return {
91
+ type: "usage",
92
+ usage,
93
+ raw: line
94
+ };
95
+ } catch {}
96
+ return {
97
+ type: "error",
98
+ message: `Unparseable NanoClaw usage line: ${line}`
99
+ };
100
+ }
101
+ if (line.startsWith("NanoClaw daemon not reachable") || line.startsWith("CLI socket error:") || line.startsWith("timeout:")) return {
102
+ type: "error",
103
+ message: line
104
+ };
105
+ return {
106
+ type: "text",
107
+ line
108
+ };
109
+ }
110
+ function resolveAnthropicBaseUrl(baseUrl) {
111
+ return baseUrl.replace(/\/+$/, "").replace(/\/v\d+[^/]*$/, "");
112
+ }
113
+ //#endregion
114
+ export { NanoclawProviderAdapter };
115
+
116
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1,68 @@
1
+ import { Mt as CliExecArgs, Pt as CliProviderConfig, Rt as ExecutionState, ht as CliProviderAdapterBase, n as HarnessId, o as WorkspacePlanApplication } from "../../index-BpO3fvbf.mjs";
2
+ import { StreamParserConfig } from "@tangle-network/sdk-cli-runner";
3
+ import { CliAuthFile, ProviderConfig, StreamEvent } from "@tangle-network/agent-interface";
4
+
5
+ //#region src/adapters/openclaw/adapter.d.ts
6
+ /**
7
+ * OpenClaw configuration. `authMode` "config-only" means openclaw uses the
8
+ * config already in $HOME (a workspace restored from a snapshot); otherwise
9
+ * the adapter synthesizes a router-backed `~/.openclaw/openclaw.json` unless
10
+ * `authFiles` already carries one.
11
+ */
12
+ interface OpenclawConfig extends CliProviderConfig {
13
+ authMode?: "auth-files" | "config-only";
14
+ }
15
+ /**
16
+ * OpenClaw's embedded agent: `openclaw agent --local --json --agent main
17
+ * --session-id <id> -m "<prompt>"`, configured by `~/.openclaw/openclaw.json`.
18
+ * It does not stream: `--json` prints one envelope when the turn ends, and
19
+ * that envelope is the only place it reports usage and its session id.
20
+ */
21
+ declare class OpenclawProviderAdapter extends CliProviderAdapterBase<OpenclawConfig, never> {
22
+ readonly provider = "openclaw";
23
+ protected readonly profileHarness: HarnessId;
24
+ protected readonly defaultCliPath = "openclaw";
25
+ /**
26
+ * Managed `~/.openclaw/openclaw.json` synthesized during initialize() and
27
+ * materialized to disk by the sidecar via getManagedAuthFiles(). Empty when
28
+ * the caller manages openclaw config itself.
29
+ */
30
+ private managedAuthFiles;
31
+ /**
32
+ * Openclaw reads NO credentials from env — it loads them from
33
+ * `~/.openclaw/openclaw.json`. A managed sandbox provisions none, so without
34
+ * a synthesized config openclaw falls back to its bundled `claude-cli`
35
+ * backend (which needs a Claude login) and the session never starts. Always
36
+ * synthesize a router-backed config so `openclaw agent --local` authenticates
37
+ * against the Tangle router, unless the caller manages config itself
38
+ * (authMode "config-only") or already supplied an `openclaw.json`.
39
+ */
40
+ protected afterInitialize(_config: ProviderConfig): Promise<void>;
41
+ getManagedAuthFiles(): CliAuthFile[];
42
+ private computeManagedAuthFiles;
43
+ protected buildExecArgs(params: {
44
+ message: string;
45
+ config: OpenclawConfig;
46
+ model?: string;
47
+ sessionId?: string;
48
+ mintedSessionId?: string;
49
+ workspaceRoot: string;
50
+ systemPrompt?: string;
51
+ }): CliExecArgs;
52
+ protected buildExecEnv(config: OpenclawConfig): Record<string, string | undefined>;
53
+ /**
54
+ * openclaw's file and exec tools resolve against its agent workspace
55
+ * (`~/.openclaw/workspace` by default), not the process cwd, so a turn that
56
+ * did not pin it edited a directory the caller never sees.
57
+ */
58
+ protected buildTurnEnvironment(executionConfig: OpenclawConfig, profilePlan: WorkspacePlanApplication | undefined, workspaceRoot: string, correlationHeaders: Readonly<Record<string, string>> | undefined): Record<string, string | undefined>;
59
+ protected createEventParser(state: ExecutionState, onEvent?: (event: StreamEvent) => void): StreamParserConfig<never>;
60
+ /**
61
+ * Parse the buffered stdout as openclaw's JSON envelope and synthesize the
62
+ * canonical message.part.updated + status:completed events.
63
+ */
64
+ private flushEnvelope;
65
+ }
66
+ //#endregion
67
+ export { OpenclawProviderAdapter };
68
+ //# sourceMappingURL=index.d.mts.map