@poncho-ai/cli 0.37.0 → 0.38.1

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/src/logger.ts ADDED
@@ -0,0 +1,9 @@
1
+ export {
2
+ createLogger,
3
+ setLogLevel,
4
+ formatError,
5
+ url,
6
+ muted,
7
+ num,
8
+ type Logger,
9
+ } from "@poncho-ai/sdk";
@@ -0,0 +1,283 @@
1
+ import { resolve } from "node:path";
2
+ import { createInterface } from "node:readline/promises";
3
+ import {
4
+ LocalMcpBridge,
5
+ loadPonchoConfig,
6
+ } from "@poncho-ai/harness";
7
+ import dotenv from "dotenv";
8
+ import {
9
+ writeConfigFile,
10
+ ensureEnvPlaceholder,
11
+ removeEnvPlaceholder,
12
+ } from "./scaffolding.js";
13
+
14
+ const normalizeMcpName = (entry: { url?: string; name?: string }): string =>
15
+ entry.name ?? entry.url ?? `mcp_${Date.now()}`;
16
+
17
+ export { normalizeMcpName };
18
+
19
+ export const mcpAdd = async (
20
+ workingDir: string,
21
+ options: {
22
+ url?: string;
23
+ name?: string;
24
+ envVars?: string[];
25
+ authBearerEnv?: string;
26
+ headers?: string[];
27
+ },
28
+ ): Promise<void> => {
29
+ const config = (await loadPonchoConfig(workingDir)) ?? { mcp: [] };
30
+ const mcp = [...(config.mcp ?? [])];
31
+ if (!options.url) {
32
+ throw new Error("Remote MCP only: provide --url for a remote MCP server.");
33
+ }
34
+ if (options.url.startsWith("ws://") || options.url.startsWith("wss://")) {
35
+ throw new Error("WebSocket MCP URLs are no longer supported. Use an HTTP MCP endpoint.");
36
+ }
37
+ if (!options.url.startsWith("http://") && !options.url.startsWith("https://")) {
38
+ throw new Error("Invalid MCP URL. Expected http:// or https://.");
39
+ }
40
+ const parsedHeaders: Record<string, string> | undefined =
41
+ options.headers && options.headers.length > 0
42
+ ? Object.fromEntries(
43
+ options.headers.map((h) => {
44
+ const idx = h.indexOf(":");
45
+ if (idx < 1) {
46
+ throw new Error(`Invalid header format "${h}". Expected "Name: value".`);
47
+ }
48
+ return [h.slice(0, idx).trim(), h.slice(idx + 1).trim()];
49
+ }),
50
+ )
51
+ : undefined;
52
+ const serverName = options.name ?? normalizeMcpName({ url: options.url });
53
+ mcp.push({
54
+ name: serverName,
55
+ url: options.url,
56
+ env: options.envVars ?? [],
57
+ auth: options.authBearerEnv
58
+ ? {
59
+ type: "bearer",
60
+ tokenEnv: options.authBearerEnv,
61
+ }
62
+ : undefined,
63
+ headers: parsedHeaders,
64
+ });
65
+
66
+ await writeConfigFile(workingDir, { ...config, mcp });
67
+ let envSeedMessage: string | undefined;
68
+ if (options.authBearerEnv) {
69
+ const envPath = resolve(workingDir, ".env");
70
+ const envExamplePath = resolve(workingDir, ".env.example");
71
+ const addedEnv = await ensureEnvPlaceholder(envPath, options.authBearerEnv);
72
+ const addedEnvExample = await ensureEnvPlaceholder(envExamplePath, options.authBearerEnv);
73
+ if (addedEnv || addedEnvExample) {
74
+ envSeedMessage = `Added ${options.authBearerEnv}= to ${addedEnv ? ".env" : ""}${addedEnv && addedEnvExample ? " and " : ""}${addedEnvExample ? ".env.example" : ""}.`;
75
+ }
76
+ }
77
+ const nextSteps: string[] = [];
78
+ let step = 1;
79
+ if (options.authBearerEnv) {
80
+ nextSteps.push(` ${step}) Set token in .env: ${options.authBearerEnv}=...`);
81
+ step += 1;
82
+ }
83
+ nextSteps.push(` ${step}) Discover tools: poncho mcp tools list ${serverName}`);
84
+ step += 1;
85
+ nextSteps.push(` ${step}) Select tools: poncho mcp tools select ${serverName}`);
86
+ step += 1;
87
+ nextSteps.push(` ${step}) Verify config: poncho mcp list`);
88
+ process.stdout.write(
89
+ [
90
+ `MCP server added: ${serverName}`,
91
+ ...(envSeedMessage ? [envSeedMessage] : []),
92
+ "Next steps:",
93
+ ...nextSteps,
94
+ "",
95
+ ].join("\n"),
96
+ );
97
+ };
98
+
99
+ export const mcpList = async (workingDir: string): Promise<void> => {
100
+ const config = await loadPonchoConfig(workingDir);
101
+ const mcp = config?.mcp ?? [];
102
+ if (mcp.length === 0) {
103
+ process.stdout.write("No MCP servers configured.\n");
104
+ return;
105
+ }
106
+ process.stdout.write("Configured MCP servers:\n");
107
+ for (const entry of mcp) {
108
+ const auth =
109
+ entry.auth?.type === "bearer" ? `auth=bearer:${entry.auth.tokenEnv}` : "auth=none";
110
+ const headerKeys = entry.headers ? Object.keys(entry.headers) : [];
111
+ const headerInfo = headerKeys.length > 0 ? `, headers=${headerKeys.join(",")}` : "";
112
+ process.stdout.write(
113
+ `- ${entry.name ?? entry.url} (remote: ${entry.url}, ${auth}${headerInfo})\n`,
114
+ );
115
+ }
116
+ };
117
+
118
+ export const mcpRemove = async (workingDir: string, name: string): Promise<void> => {
119
+ const config = (await loadPonchoConfig(workingDir)) ?? { mcp: [] };
120
+ const before = config.mcp ?? [];
121
+ const removed = before.filter((entry) => normalizeMcpName(entry) === name);
122
+ const filtered = before.filter((entry) => normalizeMcpName(entry) !== name);
123
+ await writeConfigFile(workingDir, { ...config, mcp: filtered });
124
+ const removedTokenEnvNames = new Set(
125
+ removed
126
+ .map((entry) =>
127
+ entry.auth?.type === "bearer" ? entry.auth.tokenEnv?.trim() ?? "" : "",
128
+ )
129
+ .filter((value) => value.length > 0),
130
+ );
131
+ const stillUsedTokenEnvNames = new Set(
132
+ filtered
133
+ .map((entry) =>
134
+ entry.auth?.type === "bearer" ? entry.auth.tokenEnv?.trim() ?? "" : "",
135
+ )
136
+ .filter((value) => value.length > 0),
137
+ );
138
+ const removedFromExample: string[] = [];
139
+ for (const tokenEnv of removedTokenEnvNames) {
140
+ if (stillUsedTokenEnvNames.has(tokenEnv)) {
141
+ continue;
142
+ }
143
+ const changed = await removeEnvPlaceholder(resolve(workingDir, ".env.example"), tokenEnv);
144
+ if (changed) {
145
+ removedFromExample.push(tokenEnv);
146
+ }
147
+ }
148
+ process.stdout.write(`Removed MCP server: ${name}\n`);
149
+ if (removedFromExample.length > 0) {
150
+ process.stdout.write(
151
+ `Removed unused token placeholder(s) from .env.example: ${removedFromExample.join(", ")}\n`,
152
+ );
153
+ }
154
+ };
155
+
156
+ const resolveMcpEntry = async (
157
+ workingDir: string,
158
+ serverName: string,
159
+ ): Promise<{ config: import("@poncho-ai/harness").PonchoConfig; index: number }> => {
160
+ const config = (await loadPonchoConfig(workingDir)) ?? { mcp: [] };
161
+ const entries = config.mcp ?? [];
162
+ const index = entries.findIndex((entry) => normalizeMcpName(entry) === serverName);
163
+ if (index < 0) {
164
+ throw new Error(`MCP server "${serverName}" is not configured.`);
165
+ }
166
+ return { config, index };
167
+ };
168
+
169
+ export { resolveMcpEntry };
170
+
171
+ const discoverMcpTools = async (
172
+ workingDir: string,
173
+ serverName: string,
174
+ ): Promise<string[]> => {
175
+ dotenv.config({ path: resolve(workingDir, ".env") });
176
+ const { config, index } = await resolveMcpEntry(workingDir, serverName);
177
+ const entry = (config.mcp ?? [])[index];
178
+ const bridge = new LocalMcpBridge({ mcp: [entry] });
179
+ try {
180
+ await bridge.startLocalServers();
181
+ await bridge.discoverTools();
182
+ return bridge.listDiscoveredTools(normalizeMcpName(entry));
183
+ } finally {
184
+ await bridge.stopLocalServers();
185
+ }
186
+ };
187
+
188
+ export { discoverMcpTools };
189
+
190
+ export const mcpToolsList = async (
191
+ workingDir: string,
192
+ serverName: string,
193
+ ): Promise<void> => {
194
+ const discovered = await discoverMcpTools(workingDir, serverName);
195
+ if (discovered.length === 0) {
196
+ process.stdout.write(`No tools discovered for MCP server "${serverName}".\n`);
197
+ return;
198
+ }
199
+ process.stdout.write(`Discovered tools for "${serverName}":\n`);
200
+ for (const tool of discovered) {
201
+ process.stdout.write(`- ${tool}\n`);
202
+ }
203
+ };
204
+
205
+ export const mcpToolsSelect = async (
206
+ workingDir: string,
207
+ serverName: string,
208
+ options: {
209
+ all?: boolean;
210
+ toolsCsv?: string;
211
+ },
212
+ ): Promise<void> => {
213
+ const discovered = await discoverMcpTools(workingDir, serverName);
214
+ if (discovered.length === 0) {
215
+ process.stdout.write(`No tools discovered for MCP server "${serverName}".\n`);
216
+ return;
217
+ }
218
+ let selected: string[] = [];
219
+ if (options.all) {
220
+ selected = [...discovered];
221
+ } else if (options.toolsCsv && options.toolsCsv.trim().length > 0) {
222
+ const requested = options.toolsCsv
223
+ .split(",")
224
+ .map((part) => part.trim())
225
+ .filter((part) => part.length > 0);
226
+ selected = discovered.filter((tool) => requested.includes(tool));
227
+ } else {
228
+ process.stdout.write(`Discovered tools for "${serverName}":\n`);
229
+ discovered.forEach((tool, idx) => {
230
+ process.stdout.write(`${idx + 1}. ${tool}\n`);
231
+ });
232
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
233
+ const answer = await rl.question(
234
+ "Enter comma-separated tool numbers/names to allow (or * for all): ",
235
+ );
236
+ rl.close();
237
+ const raw = answer.trim();
238
+ if (raw === "*") {
239
+ selected = [...discovered];
240
+ } else {
241
+ const tokens = raw
242
+ .split(",")
243
+ .map((part) => part.trim())
244
+ .filter((part) => part.length > 0);
245
+ const fromIndex = tokens
246
+ .map((token) => Number.parseInt(token, 10))
247
+ .filter((value) => !Number.isNaN(value))
248
+ .map((index) => discovered[index - 1])
249
+ .filter((value): value is string => typeof value === "string");
250
+ const byName = discovered.filter((tool) => tokens.includes(tool));
251
+ selected = [...new Set([...fromIndex, ...byName])];
252
+ }
253
+ }
254
+ if (selected.length === 0) {
255
+ throw new Error("No valid tools selected.");
256
+ }
257
+ const includePatterns =
258
+ selected.length === discovered.length
259
+ ? [`${serverName}/*`]
260
+ : selected.sort();
261
+ process.stdout.write(`Selected MCP tools: ${includePatterns.join(", ")}\n`);
262
+ process.stdout.write(
263
+ "\nRequired next step: add MCP intent in AGENT.md or SKILL.md allowed-tools. Without this, these MCP tools will not be registered for the model.\n",
264
+ );
265
+ process.stdout.write(
266
+ "\nOption A: AGENT.md (global fallback intent)\n" +
267
+ "Paste this into AGENT.md frontmatter:\n" +
268
+ "---\n" +
269
+ "allowed-tools:\n" +
270
+ includePatterns.map((tool) => ` - mcp:${tool}`).join("\n") +
271
+ "\n---\n",
272
+ );
273
+ process.stdout.write(
274
+ "\nOption B: SKILL.md (only when that skill is activated)\n" +
275
+ "Paste this into SKILL.md frontmatter:\n" +
276
+ "---\n" +
277
+ "allowed-tools:\n" +
278
+ includePatterns.map((tool) => ` - mcp:${tool}`).join("\n") +
279
+ "\napproval-required:\n" +
280
+ includePatterns.map((tool) => ` - mcp:${tool}`).join("\n") +
281
+ "\n---\n",
282
+ );
283
+ };
@@ -0,0 +1,150 @@
1
+ import { spawn } from "node:child_process";
2
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
3
+ import { resolve } from "node:path";
4
+ import { generateAgentId } from "@poncho-ai/harness";
5
+ import {
6
+ AGENT_TEMPLATE,
7
+ ENV_TEMPLATE,
8
+ GITIGNORE_TEMPLATE,
9
+ PACKAGE_TEMPLATE,
10
+ README_TEMPLATE,
11
+ SKILL_TEMPLATE,
12
+ SKILL_TOOL_TEMPLATE,
13
+ TEST_TEMPLATE,
14
+ } from "./templates.js";
15
+ import {
16
+ ensureFile,
17
+ renderConfigFile,
18
+ scaffoldDeployTarget,
19
+ } from "./scaffolding.js";
20
+ import { runPnpmInstall } from "./skills.js";
21
+ import {
22
+ runInitOnboarding,
23
+ type InitOnboardingOptions,
24
+ } from "./init-onboarding.js";
25
+ import {
26
+ initializeOnboardingMarker,
27
+ } from "./init-feature-context.js";
28
+
29
+ const gitInit = (cwd: string): Promise<boolean> =>
30
+ new Promise((resolve) => {
31
+ const child = spawn("git", ["init"], { cwd, stdio: "ignore" });
32
+ child.on("error", () => resolve(false));
33
+ child.on("close", (code) => resolve(code === 0));
34
+ });
35
+
36
+ export const initProject = async (
37
+ projectName: string,
38
+ options?: {
39
+ workingDir?: string;
40
+ onboarding?: InitOnboardingOptions;
41
+ envExampleOverride?: string;
42
+ },
43
+ ): Promise<void> => {
44
+ const baseDir = options?.workingDir ?? process.cwd();
45
+ const projectDir = resolve(baseDir, projectName);
46
+ await mkdir(projectDir, { recursive: true });
47
+
48
+ const onboardingOptions: InitOnboardingOptions = options?.onboarding ?? {
49
+ yes: true,
50
+ interactive: false,
51
+ };
52
+ const onboarding = await runInitOnboarding(onboardingOptions);
53
+ const agentId = generateAgentId();
54
+
55
+ const G = "\x1b[32m";
56
+ const D = "\x1b[2m";
57
+ const B = "\x1b[1m";
58
+ const CY = "\x1b[36m";
59
+ const YW = "\x1b[33m";
60
+ const R = "\x1b[0m";
61
+
62
+ process.stdout.write("\n");
63
+
64
+ const scaffoldFiles: Array<{ path: string; content: string }> = [
65
+ {
66
+ path: "AGENT.md",
67
+ content: AGENT_TEMPLATE(projectName, agentId, {
68
+ modelProvider: onboarding.agentModel.provider,
69
+ modelName: onboarding.agentModel.name,
70
+ }),
71
+ },
72
+ { path: "poncho.config.js", content: renderConfigFile(onboarding.config) },
73
+ { path: "package.json", content: await PACKAGE_TEMPLATE(projectName, projectDir) },
74
+ { path: "README.md", content: README_TEMPLATE(projectName) },
75
+ { path: ".env.example", content: options?.envExampleOverride ?? onboarding.envExample ?? ENV_TEMPLATE },
76
+ { path: ".gitignore", content: GITIGNORE_TEMPLATE },
77
+ { path: "tests/basic.yaml", content: TEST_TEMPLATE },
78
+ { path: "skills/starter/SKILL.md", content: SKILL_TEMPLATE },
79
+ { path: "skills/starter/scripts/starter-echo.ts", content: SKILL_TOOL_TEMPLATE },
80
+ ];
81
+ if (onboarding.envFile) {
82
+ scaffoldFiles.push({ path: ".env", content: onboarding.envFile });
83
+ }
84
+
85
+ for (const file of scaffoldFiles) {
86
+ await ensureFile(resolve(projectDir, file.path), file.content);
87
+ process.stdout.write(` ${D}+${R} ${D}${file.path}${R}\n`);
88
+ }
89
+
90
+ if (onboarding.deployTarget !== "none") {
91
+ const deployFiles = await scaffoldDeployTarget(projectDir, onboarding.deployTarget);
92
+ for (const filePath of deployFiles) {
93
+ process.stdout.write(` ${D}+${R} ${D}${filePath}${R}\n`);
94
+ }
95
+ }
96
+
97
+ await initializeOnboardingMarker(projectDir, {
98
+ allowIntro: !(onboardingOptions.yes ?? false),
99
+ });
100
+
101
+ process.stdout.write("\n");
102
+
103
+ // Install dependencies so subsequent commands (e.g. `poncho add`) succeed.
104
+ try {
105
+ await runPnpmInstall(projectDir);
106
+ process.stdout.write(` ${G}✓${R} ${D}Installed dependencies${R}\n`);
107
+ } catch {
108
+ process.stdout.write(
109
+ ` ${YW}!${R} Could not install dependencies — run ${D}pnpm install${R} manually\n`,
110
+ );
111
+ }
112
+
113
+ const gitOk = await gitInit(projectDir);
114
+ if (gitOk) {
115
+ process.stdout.write(` ${G}✓${R} ${D}Initialized git${R}\n`);
116
+ }
117
+
118
+ process.stdout.write(` ${G}✓${R} ${B}${projectName}${R} is ready\n`);
119
+ process.stdout.write("\n");
120
+ process.stdout.write(` ${B}Get started${R}\n`);
121
+ process.stdout.write("\n");
122
+ process.stdout.write(` ${D}$${R} cd ${projectName}\n`);
123
+ process.stdout.write("\n");
124
+ process.stdout.write(` ${CY}Web UI${R} ${D}$${R} poncho dev\n`);
125
+ process.stdout.write(` ${CY}CLI interactive${R} ${D}$${R} poncho run --interactive\n`);
126
+ process.stdout.write("\n");
127
+ if (onboarding.envNeedsUserInput) {
128
+ process.stdout.write(
129
+ ` ${YW}!${R} Make sure you add your keys to the ${B}.env${R} file.\n`,
130
+ );
131
+ }
132
+ process.stdout.write(` ${D}The agent will introduce itself on your first session.${R}\n`);
133
+ process.stdout.write("\n");
134
+ };
135
+
136
+ export const updateAgentGuidance = async (workingDir: string): Promise<boolean> => {
137
+ const agentPath = resolve(workingDir, "AGENT.md");
138
+ const content = await readFile(agentPath, "utf8");
139
+ const guidanceSectionPattern =
140
+ /\n## Configuration Assistant Context[\s\S]*?(?=\n## |\n# |$)|\n## Skill Authoring Guidance[\s\S]*?(?=\n## |\n# |$)/g;
141
+ const normalized = content.replace(/\s+$/g, "");
142
+ const updated = normalized.replace(guidanceSectionPattern, "").replace(/\n{3,}/g, "\n\n");
143
+ if (updated === normalized) {
144
+ process.stdout.write("AGENT.md does not contain deprecated embedded local guidance.\n");
145
+ return false;
146
+ }
147
+ await writeFile(agentPath, `${updated}\n`, "utf8");
148
+ process.stdout.write("Removed deprecated embedded local guidance from AGENT.md.\n");
149
+ return true;
150
+ };
@@ -0,0 +1,145 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { basename, resolve } from "node:path";
3
+ import {
4
+ AgentHarness,
5
+ TelemetryEmitter,
6
+ createConversationStore,
7
+ createConversationStoreFromEngine,
8
+ createUploadStore,
9
+ ensureAgentIdentity,
10
+ loadPonchoConfig,
11
+ resolveStateConfig,
12
+ type PonchoConfig,
13
+ type ConversationStore,
14
+ } from "@poncho-ai/harness";
15
+ import type { FileInput, RunInput } from "@poncho-ai/sdk";
16
+ import dotenv from "dotenv";
17
+ import { extToMime, resolveHarnessEnvironment } from "./http-utils.js";
18
+
19
+ export const runOnce = async (
20
+ task: string,
21
+ options: {
22
+ params: Record<string, string>;
23
+ json: boolean;
24
+ filePaths: string[];
25
+ workingDir?: string;
26
+ },
27
+ ): Promise<void> => {
28
+ const workingDir = options.workingDir ?? process.cwd();
29
+ dotenv.config({ path: resolve(workingDir, ".env") });
30
+ const config = await loadPonchoConfig(workingDir);
31
+ const uploadStore = await createUploadStore(config?.uploads, workingDir);
32
+ const harness = new AgentHarness({ workingDir, uploadStore });
33
+ const telemetry = new TelemetryEmitter(config?.telemetry);
34
+ await harness.initialize();
35
+
36
+ const fileInputs: FileInput[] = await Promise.all(
37
+ options.filePaths.map(async (filePath) => {
38
+ const absPath = resolve(workingDir, filePath);
39
+ const buf = await readFile(absPath);
40
+ const ext = absPath.split(".").pop()?.toLowerCase() ?? "";
41
+ return {
42
+ data: buf.toString("base64"),
43
+ mediaType: extToMime(ext),
44
+ filename: basename(filePath),
45
+ };
46
+ }),
47
+ );
48
+
49
+ const input: RunInput = {
50
+ task,
51
+ parameters: options.params,
52
+ files: fileInputs.length > 0 ? fileInputs : undefined,
53
+ };
54
+
55
+ if (options.json) {
56
+ const output = await harness.runToCompletion(input);
57
+ for (const event of output.events) {
58
+ await telemetry.emit(event);
59
+ }
60
+ process.stdout.write(`${JSON.stringify(output, null, 2)}\n`);
61
+ return;
62
+ }
63
+
64
+ for await (const event of harness.runWithTelemetry(input)) {
65
+ await telemetry.emit(event);
66
+ if (event.type === "model:chunk") {
67
+ process.stdout.write(event.content);
68
+ }
69
+ if (event.type === "run:error") {
70
+ process.stderr.write(`\nError: ${event.error.message}\n`);
71
+ }
72
+ if (event.type === "run:completed") {
73
+ process.stdout.write("\n");
74
+ }
75
+ if (event.type === "run:cancelled") {
76
+ process.stdout.write("\n");
77
+ process.stderr.write("Run cancelled.\n");
78
+ }
79
+ }
80
+ };
81
+
82
+ export const runInteractive = async (
83
+ workingDir: string,
84
+ params: Record<string, string>,
85
+ ): Promise<void> => {
86
+ dotenv.config({ path: resolve(workingDir, ".env") });
87
+ const config = await loadPonchoConfig(workingDir);
88
+
89
+ const uploadStore = await createUploadStore(config?.uploads, workingDir);
90
+ const harness = new AgentHarness({
91
+ workingDir,
92
+ environment: resolveHarnessEnvironment(),
93
+ uploadStore,
94
+ });
95
+ await harness.initialize();
96
+ const identity = await ensureAgentIdentity(workingDir);
97
+ try {
98
+ const { runInteractiveInk } = await import("./run-interactive-ink.js");
99
+ await (
100
+ runInteractiveInk as (input: {
101
+ harness: AgentHarness;
102
+ params: Record<string, string>;
103
+ workingDir: string;
104
+ config?: PonchoConfig;
105
+ conversationStore: ConversationStore;
106
+ }) => Promise<void>
107
+ )({
108
+ harness,
109
+ params,
110
+ workingDir,
111
+ config,
112
+ conversationStore: (() => {
113
+ if (!harness.storageEngine) {
114
+ process.stderr.write(
115
+ "[poncho] WARNING: harness.storageEngine is undefined. " +
116
+ "This usually means an outdated @poncho-ai/harness (< 0.37.0) is installed. " +
117
+ "Falling back to in-memory storage — conversations will NOT be persisted. " +
118
+ "Fix: `pnpm up @poncho-ai/harness@latest` or add a pnpm.overrides entry to force resolution.\n",
119
+ );
120
+ return createConversationStore(resolveStateConfig(config), { workingDir, agentId: identity.id });
121
+ }
122
+ return createConversationStoreFromEngine(harness.storageEngine);
123
+ })(),
124
+ });
125
+ } finally {
126
+ await harness.shutdown();
127
+ }
128
+ };
129
+
130
+ export const listTools = async (workingDir: string): Promise<void> => {
131
+ dotenv.config({ path: resolve(workingDir, ".env") });
132
+ const harness = new AgentHarness({ workingDir });
133
+ await harness.initialize();
134
+ const tools = harness.listTools();
135
+
136
+ if (tools.length === 0) {
137
+ process.stdout.write("No tools registered.\n");
138
+ return;
139
+ }
140
+
141
+ process.stdout.write("Available tools:\n");
142
+ for (const tool of tools) {
143
+ process.stdout.write(`- ${tool.name}: ${tool.description}\n`);
144
+ }
145
+ };