@townco/cli 0.1.12 → 0.1.13
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/dist/commands/create.js +12 -78
- package/dist/commands/tool-add.js +2 -29
- package/dist/index.js +0 -0
- package/dist/lib/editor-utils.d.ts +15 -0
- package/dist/lib/editor-utils.js +112 -0
- package/package.json +5 -5
- package/dist/commands/tool-stub.d.ts +0 -6
- package/dist/commands/tool-stub.js +0 -376
package/dist/commands/create.js
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { spawn } from "node:child_process";
|
|
3
|
-
import { readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
4
|
-
import { tmpdir } from "node:os";
|
|
5
|
-
import { join } from "node:path";
|
|
6
2
|
import { scaffoldAgent } from "@townco/agent/scaffold";
|
|
7
3
|
import { InputBox, MultiSelect, SingleSelect } from "@townco/ui/tui";
|
|
8
4
|
import { Box, render, Text, useInput } from "ink";
|
|
9
5
|
import TextInput from "ink-text-input";
|
|
10
6
|
import { useEffect, useState } from "react";
|
|
7
|
+
import { openInEditor } from "../lib/editor-utils";
|
|
11
8
|
const AVAILABLE_MODELS = [
|
|
12
9
|
{
|
|
13
10
|
label: "Claude Sonnet 4.5",
|
|
@@ -55,70 +52,6 @@ function NameInput({ nameInput, setNameInput, onSubmit }) {
|
|
|
55
52
|
});
|
|
56
53
|
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, children: "Enter agent name:" }) }), _jsxs(Box, { children: [_jsxs(Text, { children: [">", " "] }), _jsx(TextInput, { value: nameInput, onChange: setNameInput, onSubmit: onSubmit })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: Continue \u2022 Esc: Cancel" }) })] }));
|
|
57
54
|
}
|
|
58
|
-
async function openEditor(initialContent) {
|
|
59
|
-
const tempFile = join(tmpdir(), `agent-prompt-${Date.now()}.txt`);
|
|
60
|
-
try {
|
|
61
|
-
// Write initial content
|
|
62
|
-
writeFileSync(tempFile, initialContent, "utf-8");
|
|
63
|
-
// Try $EDITOR first
|
|
64
|
-
const editor = process.env.EDITOR;
|
|
65
|
-
if (editor) {
|
|
66
|
-
try {
|
|
67
|
-
await new Promise((resolve, reject) => {
|
|
68
|
-
const child = spawn(editor, [tempFile], {
|
|
69
|
-
stdio: "inherit",
|
|
70
|
-
});
|
|
71
|
-
child.on("close", (code) => {
|
|
72
|
-
if (code === 0)
|
|
73
|
-
resolve();
|
|
74
|
-
else
|
|
75
|
-
reject(new Error(`Editor exited with code ${code}`));
|
|
76
|
-
});
|
|
77
|
-
child.on("error", reject);
|
|
78
|
-
});
|
|
79
|
-
const content = readFileSync(tempFile, "utf-8");
|
|
80
|
-
unlinkSync(tempFile);
|
|
81
|
-
return content;
|
|
82
|
-
}
|
|
83
|
-
catch (_error) {
|
|
84
|
-
// Fall through to try 'code'
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
// Try 'code' (VS Code) as fallback
|
|
88
|
-
try {
|
|
89
|
-
await new Promise((resolve, reject) => {
|
|
90
|
-
const child = spawn("code", ["--wait", tempFile], {
|
|
91
|
-
stdio: "inherit",
|
|
92
|
-
});
|
|
93
|
-
child.on("close", (code) => {
|
|
94
|
-
if (code === 0)
|
|
95
|
-
resolve();
|
|
96
|
-
else
|
|
97
|
-
reject(new Error(`Code exited with code ${code}`));
|
|
98
|
-
});
|
|
99
|
-
child.on("error", reject);
|
|
100
|
-
});
|
|
101
|
-
const content = readFileSync(tempFile, "utf-8");
|
|
102
|
-
unlinkSync(tempFile);
|
|
103
|
-
return content;
|
|
104
|
-
}
|
|
105
|
-
catch (_error) {
|
|
106
|
-
// Clean up and return null to signal fallback to inline
|
|
107
|
-
unlinkSync(tempFile);
|
|
108
|
-
return null;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
catch (_error) {
|
|
112
|
-
// Clean up temp file if it exists
|
|
113
|
-
try {
|
|
114
|
-
unlinkSync(tempFile);
|
|
115
|
-
}
|
|
116
|
-
catch {
|
|
117
|
-
// Ignore cleanup errors
|
|
118
|
-
}
|
|
119
|
-
return null;
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
55
|
function CreateApp({ name: initialName, model: initialModel, tools: initialTools, systemPrompt: initialSystemPrompt, overwrite = false, }) {
|
|
123
56
|
// Determine the starting stage based on what's provided
|
|
124
57
|
const determineInitialStage = () => {
|
|
@@ -150,13 +83,14 @@ function CreateApp({ name: initialName, model: initialModel, tools: initialTools
|
|
|
150
83
|
const [agentPath, setAgentPath] = useState(null);
|
|
151
84
|
// Handle opening editor when systemPrompt stage is entered from review
|
|
152
85
|
useEffect(() => {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
86
|
+
(async () => {
|
|
87
|
+
if (stage === "systemPrompt" &&
|
|
88
|
+
isEditingFromReview &&
|
|
89
|
+
!isEditingPrompt &&
|
|
90
|
+
promptEditMode === null) {
|
|
91
|
+
// Trigger editor opening
|
|
92
|
+
setIsEditingPrompt(true);
|
|
93
|
+
const editorContent = await openInEditor(agentDef.systemPrompt || "You are a helpful assistant.");
|
|
160
94
|
if (editorContent !== null) {
|
|
161
95
|
// Editor worked
|
|
162
96
|
setAgentDef({ ...agentDef, systemPrompt: editorContent });
|
|
@@ -170,8 +104,8 @@ function CreateApp({ name: initialName, model: initialModel, tools: initialTools
|
|
|
170
104
|
setIsEditingPrompt(false);
|
|
171
105
|
setSystemPromptInput(agentDef.systemPrompt || "You are a helpful assistant.");
|
|
172
106
|
}
|
|
173
|
-
}
|
|
174
|
-
}
|
|
107
|
+
}
|
|
108
|
+
})();
|
|
175
109
|
}, [stage, isEditingFromReview, isEditingPrompt, promptEditMode, agentDef]);
|
|
176
110
|
// Handle scaffolding when entering "done" stage
|
|
177
111
|
useEffect(() => {
|
|
@@ -271,7 +205,7 @@ function CreateApp({ name: initialName, model: initialModel, tools: initialTools
|
|
|
271
205
|
setStage("systemPrompt");
|
|
272
206
|
// Attempt to open editor
|
|
273
207
|
setIsEditingPrompt(true);
|
|
274
|
-
const editorContent = await
|
|
208
|
+
const editorContent = await openInEditor(agentDef.systemPrompt || "You are a helpful assistant.");
|
|
275
209
|
if (editorContent !== null) {
|
|
276
210
|
// Editor worked
|
|
277
211
|
setAgentDef({ ...agentDef, systemPrompt: editorContent });
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { spawn } from "node:child_process";
|
|
3
2
|
import { existsSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
4
3
|
import { tmpdir } from "node:os";
|
|
5
4
|
import { join } from "node:path";
|
|
@@ -8,6 +7,7 @@ import { MultiSelect } from "@townco/ui/tui";
|
|
|
8
7
|
import { Box, render, Text, useApp, useInput } from "ink";
|
|
9
8
|
import TextInput from "ink-text-input";
|
|
10
9
|
import { useEffect, useState } from "react";
|
|
10
|
+
import { openInEditor } from "../lib/editor-utils";
|
|
11
11
|
import { ensureToolsDir, getToolsDir, saveToolConfig, toolConfigExists, } from "../lib/tool-storage";
|
|
12
12
|
// ============================================================================
|
|
13
13
|
// Helper Functions
|
|
@@ -65,33 +65,6 @@ export default function ${camelCaseName}(_input: unknown) {
|
|
|
65
65
|
}
|
|
66
66
|
`;
|
|
67
67
|
}
|
|
68
|
-
/**
|
|
69
|
-
* Open a file in the user's editor and wait for it to close
|
|
70
|
-
*/
|
|
71
|
-
async function openInEditor(filePath) {
|
|
72
|
-
return new Promise((resolve, reject) => {
|
|
73
|
-
// Get editor from environment, fallback to vi
|
|
74
|
-
const editor = process.env.EDITOR || process.env.VISUAL || "vi";
|
|
75
|
-
// Parse editor command (might include args like "code --wait")
|
|
76
|
-
const editorParts = editor.split(" ");
|
|
77
|
-
const editorCommand = editorParts[0] || "vi";
|
|
78
|
-
const editorArgs = [...editorParts.slice(1), filePath];
|
|
79
|
-
const child = spawn(editorCommand, editorArgs, {
|
|
80
|
-
stdio: "inherit",
|
|
81
|
-
});
|
|
82
|
-
child.on("exit", (code) => {
|
|
83
|
-
if (code === 0 || code === null) {
|
|
84
|
-
resolve();
|
|
85
|
-
}
|
|
86
|
-
else {
|
|
87
|
-
reject(new Error(`Editor exited with code ${code}`));
|
|
88
|
-
}
|
|
89
|
-
});
|
|
90
|
-
child.on("error", (error) => {
|
|
91
|
-
reject(error);
|
|
92
|
-
});
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
68
|
function TextInputStage({ title, value, onChange, onSubmit, onCancel, placeholder, }) {
|
|
96
69
|
useInput((_input, key) => {
|
|
97
70
|
if (key.escape) {
|
|
@@ -211,7 +184,7 @@ function ToolAddApp({ name: initialName }) {
|
|
|
211
184
|
const stubContent = generateStubTemplate(initialName);
|
|
212
185
|
writeFileSync(tempFile, stubContent, "utf-8");
|
|
213
186
|
// Open in editor
|
|
214
|
-
await openInEditor(tempFile);
|
|
187
|
+
await openInEditor({ filePath: tempFile });
|
|
215
188
|
// After editor closes, move to name input stage
|
|
216
189
|
setEditorOpened(true);
|
|
217
190
|
}
|
package/dist/index.js
CHANGED
|
File without changes
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified editor opening function that handles both content and file modes
|
|
3
|
+
*
|
|
4
|
+
* @example Content mode
|
|
5
|
+
* const edited = await openInEditor("Hello world", { extension: '.txt' });
|
|
6
|
+
* if (edited) console.log("User edited:", edited);
|
|
7
|
+
*
|
|
8
|
+
* @example File mode
|
|
9
|
+
* await openInEditor({ filePath: '/path/to/file.ts' });
|
|
10
|
+
*/
|
|
11
|
+
export declare function openInEditor(contentOrOptions: string | {
|
|
12
|
+
filePath: string;
|
|
13
|
+
}, options?: {
|
|
14
|
+
extension?: string;
|
|
15
|
+
}): Promise<string | null>;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
/**
|
|
6
|
+
* Standardized editor fallback chain
|
|
7
|
+
*/
|
|
8
|
+
const EDITOR_FALLBACKS = [
|
|
9
|
+
() => process.env.EDITOR,
|
|
10
|
+
() => process.env.VISUAL,
|
|
11
|
+
() => "code",
|
|
12
|
+
() => "vi",
|
|
13
|
+
];
|
|
14
|
+
/**
|
|
15
|
+
* Parse an editor command that might include arguments
|
|
16
|
+
* e.g., "code --wait" -> ["code", "--wait"]
|
|
17
|
+
*/
|
|
18
|
+
function parseEditorCommand(editor) {
|
|
19
|
+
const parts = editor.trim().split(/\s+/);
|
|
20
|
+
const command = parts[0] || "vi";
|
|
21
|
+
const args = parts.slice(1);
|
|
22
|
+
return [command, args];
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Attempt to spawn an editor and wait for it to close
|
|
26
|
+
*/
|
|
27
|
+
async function spawnEditor(editor, filePath, additionalArgs = []) {
|
|
28
|
+
return new Promise((resolve) => {
|
|
29
|
+
const [command, editorArgs] = parseEditorCommand(editor);
|
|
30
|
+
// Special handling for 'code' - add --wait if not present
|
|
31
|
+
const args = command === "code" && !editorArgs.includes("--wait")
|
|
32
|
+
? ["--wait", ...editorArgs, ...additionalArgs, filePath]
|
|
33
|
+
: [...editorArgs, ...additionalArgs, filePath];
|
|
34
|
+
const child = spawn(command, args, {
|
|
35
|
+
stdio: "inherit",
|
|
36
|
+
});
|
|
37
|
+
child.on("close", (code) => {
|
|
38
|
+
resolve(code === 0 || code === null);
|
|
39
|
+
});
|
|
40
|
+
child.on("error", () => {
|
|
41
|
+
resolve(false);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Try editors in the fallback chain until one succeeds
|
|
47
|
+
*/
|
|
48
|
+
async function tryEditorsInOrder(filePath) {
|
|
49
|
+
for (const getEditor of EDITOR_FALLBACKS) {
|
|
50
|
+
const editor = getEditor();
|
|
51
|
+
if (editor) {
|
|
52
|
+
const success = await spawnEditor(editor, filePath);
|
|
53
|
+
if (success) {
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Unified editor opening function that handles both content and file modes
|
|
62
|
+
*
|
|
63
|
+
* @example Content mode
|
|
64
|
+
* const edited = await openInEditor("Hello world", { extension: '.txt' });
|
|
65
|
+
* if (edited) console.log("User edited:", edited);
|
|
66
|
+
*
|
|
67
|
+
* @example File mode
|
|
68
|
+
* await openInEditor({ filePath: '/path/to/file.ts' });
|
|
69
|
+
*/
|
|
70
|
+
export async function openInEditor(contentOrOptions, options) {
|
|
71
|
+
const isContentMode = typeof contentOrOptions === "string";
|
|
72
|
+
if (isContentMode) {
|
|
73
|
+
// Content mode: create temp file, edit, return content
|
|
74
|
+
const content = contentOrOptions;
|
|
75
|
+
const extension = options?.extension || ".txt";
|
|
76
|
+
const tempFile = join(tmpdir(), `editor-${Date.now()}${extension}`);
|
|
77
|
+
try {
|
|
78
|
+
// Write initial content
|
|
79
|
+
writeFileSync(tempFile, content, "utf-8");
|
|
80
|
+
// Try editors in fallback order
|
|
81
|
+
const success = await tryEditorsInOrder(tempFile);
|
|
82
|
+
if (success) {
|
|
83
|
+
// Read edited content
|
|
84
|
+
const editedContent = readFileSync(tempFile, "utf-8");
|
|
85
|
+
unlinkSync(tempFile);
|
|
86
|
+
return editedContent;
|
|
87
|
+
}
|
|
88
|
+
// All editors failed
|
|
89
|
+
unlinkSync(tempFile);
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
catch (_error) {
|
|
93
|
+
// Clean up temp file if it exists
|
|
94
|
+
try {
|
|
95
|
+
unlinkSync(tempFile);
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
// Ignore cleanup errors
|
|
99
|
+
}
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
// File mode: edit existing file, return void
|
|
105
|
+
const { filePath } = contentOrOptions;
|
|
106
|
+
const success = await tryEditorsInOrder(filePath);
|
|
107
|
+
if (!success) {
|
|
108
|
+
throw new Error("Failed to open editor");
|
|
109
|
+
}
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@townco/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.13",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"town": "./dist/index.js"
|
|
@@ -19,16 +19,16 @@
|
|
|
19
19
|
"build": "tsc"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
|
-
"@townco/tsconfig": "0.1.
|
|
22
|
+
"@townco/tsconfig": "0.1.5",
|
|
23
23
|
"@types/bun": "^1.3.1",
|
|
24
24
|
"@types/react": "^19.2.2"
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"@optique/core": "^0.6.2",
|
|
28
28
|
"@optique/run": "^0.6.2",
|
|
29
|
-
"@townco/agent": "0.1.
|
|
30
|
-
"@townco/secret": "0.1.
|
|
31
|
-
"@townco/ui": "0.1.
|
|
29
|
+
"@townco/agent": "0.1.13",
|
|
30
|
+
"@townco/secret": "0.1.8",
|
|
31
|
+
"@townco/ui": "0.1.8",
|
|
32
32
|
"@types/inquirer": "^9.0.9",
|
|
33
33
|
"ink": "^6.4.0",
|
|
34
34
|
"ink-text-input": "^6.0.0",
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
interface ToolStubProps {
|
|
2
|
-
name?: string;
|
|
3
|
-
}
|
|
4
|
-
declare function ToolStubApp({ name: initialName }: ToolStubProps): import("react/jsx-runtime").JSX.Element | null;
|
|
5
|
-
export default ToolStubApp;
|
|
6
|
-
export declare function runToolStub(props?: ToolStubProps): Promise<void>;
|
|
@@ -1,376 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { spawn } from "node:child_process";
|
|
3
|
-
import { copyFileSync, existsSync, readFileSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
4
|
-
import { tmpdir } from "node:os";
|
|
5
|
-
import { join } from "node:path";
|
|
6
|
-
import { getAgentPath, listAgents } from "@townco/agent/storage";
|
|
7
|
-
import { MultiSelect } from "@townco/ui/tui";
|
|
8
|
-
import { Box, render, Text, useApp, useInput } from "ink";
|
|
9
|
-
import TextInput from "ink-text-input";
|
|
10
|
-
import { useEffect, useState } from "react";
|
|
11
|
-
import { ensureToolsDir, getToolsDir, saveToolConfig, toolConfigExists, } from "../lib/tool-storage";
|
|
12
|
-
// ============================================================================
|
|
13
|
-
// Helper Functions
|
|
14
|
-
// ============================================================================
|
|
15
|
-
/**
|
|
16
|
-
* Normalize a tool name to kebab-case for use as a filename
|
|
17
|
-
*/
|
|
18
|
-
function normalizeToolName(name) {
|
|
19
|
-
return name
|
|
20
|
-
.trim()
|
|
21
|
-
.toLowerCase()
|
|
22
|
-
.replace(/[^a-z0-9]+/g, "-")
|
|
23
|
-
.replace(/^-+|-+$/g, "");
|
|
24
|
-
}
|
|
25
|
-
/**
|
|
26
|
-
* Convert kebab-case or snake_case to camelCase for function names
|
|
27
|
-
*/
|
|
28
|
-
function toCamelCase(str) {
|
|
29
|
-
return str.replace(/[-_](.)/g, (_, char) => char.toUpperCase());
|
|
30
|
-
}
|
|
31
|
-
/**
|
|
32
|
-
* Convert a name to snake_case for the tool name export
|
|
33
|
-
*/
|
|
34
|
-
function toSnakeCase(str) {
|
|
35
|
-
return str
|
|
36
|
-
.trim()
|
|
37
|
-
.toLowerCase()
|
|
38
|
-
.replace(/[^a-z0-9]+/g, "_")
|
|
39
|
-
.replace(/^_+|_+$/g, "");
|
|
40
|
-
}
|
|
41
|
-
/**
|
|
42
|
-
* Generate the stub template content
|
|
43
|
-
*/
|
|
44
|
-
function generateStubTemplate(toolName) {
|
|
45
|
-
const snakeCaseName = toolName ? toSnakeCase(toolName) : "tool_name";
|
|
46
|
-
const camelCaseName = toolName ? toCamelCase(toolName) : "toolName";
|
|
47
|
-
const description = toolName
|
|
48
|
-
? `Describe what ${toolName} does here`
|
|
49
|
-
: "Describe what your tool does here";
|
|
50
|
-
return `// biome-ignore lint/suspicious/noExplicitAny: .
|
|
51
|
-
export const schema = (z: any) =>
|
|
52
|
-
z.object({
|
|
53
|
-
// Define your input parameters here
|
|
54
|
-
// Example: message: z.string().describe("The message to process"),
|
|
55
|
-
input: z.string().optional(),
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
export const name = "${snakeCaseName}";
|
|
59
|
-
export const description = "${description}";
|
|
60
|
-
|
|
61
|
-
export default function ${camelCaseName}(_input: unknown) {
|
|
62
|
-
// TODO: Implement your tool logic here
|
|
63
|
-
|
|
64
|
-
return "result";
|
|
65
|
-
}
|
|
66
|
-
`;
|
|
67
|
-
}
|
|
68
|
-
/**
|
|
69
|
-
* Open a file in the user's editor and wait for it to close
|
|
70
|
-
*/
|
|
71
|
-
async function openInEditor(filePath) {
|
|
72
|
-
return new Promise((resolve, reject) => {
|
|
73
|
-
// Get editor from environment, fallback to vi
|
|
74
|
-
const editor = process.env.EDITOR || process.env.VISUAL || "vi";
|
|
75
|
-
// Parse editor command (might include args like "code --wait")
|
|
76
|
-
const editorParts = editor.split(" ");
|
|
77
|
-
const editorCommand = editorParts[0] || "vi";
|
|
78
|
-
const editorArgs = [...editorParts.slice(1), filePath];
|
|
79
|
-
const child = spawn(editorCommand, editorArgs, {
|
|
80
|
-
stdio: "inherit",
|
|
81
|
-
});
|
|
82
|
-
child.on("exit", (code) => {
|
|
83
|
-
if (code === 0 || code === null) {
|
|
84
|
-
resolve();
|
|
85
|
-
}
|
|
86
|
-
else {
|
|
87
|
-
reject(new Error(`Editor exited with code ${code}`));
|
|
88
|
-
}
|
|
89
|
-
});
|
|
90
|
-
child.on("error", (error) => {
|
|
91
|
-
reject(error);
|
|
92
|
-
});
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
function TextInputStage({ title, value, onChange, onSubmit, onCancel, placeholder, }) {
|
|
96
|
-
useInput((_input, key) => {
|
|
97
|
-
if (key.escape) {
|
|
98
|
-
onCancel();
|
|
99
|
-
}
|
|
100
|
-
});
|
|
101
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, children: title }) }), _jsxs(Box, { children: [_jsxs(Text, { children: [">", " "] }), _jsx(TextInput, { value: value, onChange: onChange, onSubmit: onSubmit, ...(placeholder && { placeholder }) })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: Continue \u2022 Esc: Back" }) })] }));
|
|
102
|
-
}
|
|
103
|
-
function NameInputStage({ value, onChange, onNext, onBack, }) {
|
|
104
|
-
const [localError, setLocalError] = useState(null);
|
|
105
|
-
const handleSubmit = () => {
|
|
106
|
-
const trimmed = value.trim();
|
|
107
|
-
if (!trimmed) {
|
|
108
|
-
return;
|
|
109
|
-
}
|
|
110
|
-
// Check for duplicate
|
|
111
|
-
if (toolConfigExists(trimmed)) {
|
|
112
|
-
setLocalError(`Tool "${trimmed}" already exists`);
|
|
113
|
-
return;
|
|
114
|
-
}
|
|
115
|
-
onNext(trimmed);
|
|
116
|
-
};
|
|
117
|
-
const handleChange = (newValue) => {
|
|
118
|
-
if (localError) {
|
|
119
|
-
setLocalError(null);
|
|
120
|
-
}
|
|
121
|
-
onChange(newValue);
|
|
122
|
-
};
|
|
123
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(TextInputStage, { title: "Enter tool name:", value: value, onChange: handleChange, onSubmit: handleSubmit, onCancel: onBack, placeholder: "my-custom-tool" }), localError && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "red", children: ["\u274C ", localError] }) }))] }));
|
|
124
|
-
}
|
|
125
|
-
function NoAgentsMessage({ onNext }) {
|
|
126
|
-
useEffect(() => {
|
|
127
|
-
const timer = setTimeout(() => {
|
|
128
|
-
onNext();
|
|
129
|
-
}, 1000);
|
|
130
|
-
return () => clearTimeout(timer);
|
|
131
|
-
}, [onNext]);
|
|
132
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "No agents found. Tool will be saved globally." }), _jsx(Text, { dimColor: true, children: "You can attach it to agents later." })] }));
|
|
133
|
-
}
|
|
134
|
-
function AgentSelectionStage({ selectedAgents, onSelectedAgentsChange, onNext, onBack, }) {
|
|
135
|
-
const [availableAgents, setAvailableAgents] = useState([]);
|
|
136
|
-
const [isLoading, setIsLoading] = useState(true);
|
|
137
|
-
useEffect(() => {
|
|
138
|
-
// Fetch available agents
|
|
139
|
-
const fetchAgents = async () => {
|
|
140
|
-
try {
|
|
141
|
-
const agents = await listAgents();
|
|
142
|
-
setAvailableAgents(agents);
|
|
143
|
-
}
|
|
144
|
-
catch (error) {
|
|
145
|
-
console.error("Error fetching agents:", error);
|
|
146
|
-
setAvailableAgents([]);
|
|
147
|
-
}
|
|
148
|
-
finally {
|
|
149
|
-
setIsLoading(false);
|
|
150
|
-
}
|
|
151
|
-
};
|
|
152
|
-
fetchAgents();
|
|
153
|
-
}, []);
|
|
154
|
-
// If still loading, show loading message
|
|
155
|
-
if (isLoading) {
|
|
156
|
-
return (_jsx(Box, { flexDirection: "column", children: _jsx(Text, { children: "Loading agents..." }) }));
|
|
157
|
-
}
|
|
158
|
-
// If no agents available, show info message and auto-proceed
|
|
159
|
-
if (availableAgents.length === 0) {
|
|
160
|
-
return _jsx(NoAgentsMessage, { onNext: onNext });
|
|
161
|
-
}
|
|
162
|
-
// Show agent selection UI
|
|
163
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, children: "Register tool with agents (optional):" }) }), _jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { dimColor: true, children: "Select which agents should have access to this tool." }), _jsx(Text, { dimColor: true, children: "You can skip this step and register agents later." })] }), _jsx(MultiSelect, { options: availableAgents.map((agent) => ({
|
|
164
|
-
label: agent,
|
|
165
|
-
value: agent,
|
|
166
|
-
})), selected: selectedAgents, onChange: onSelectedAgentsChange, onSubmit: onNext, onCancel: onBack })] }));
|
|
167
|
-
}
|
|
168
|
-
function DoneStage({ config, status, error, attachedAgents }) {
|
|
169
|
-
const { exit } = useApp();
|
|
170
|
-
useEffect(() => {
|
|
171
|
-
if (status === "done") {
|
|
172
|
-
setImmediate(() => {
|
|
173
|
-
exit();
|
|
174
|
-
});
|
|
175
|
-
}
|
|
176
|
-
}, [status, exit]);
|
|
177
|
-
if (status === "saving") {
|
|
178
|
-
return (_jsx(Box, { flexDirection: "column", children: _jsx(Text, { children: "\u23F3 Saving tool configuration..." }) }));
|
|
179
|
-
}
|
|
180
|
-
if (status === "error") {
|
|
181
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "red", children: "\u274C Error saving tool" }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { children: error }) })] }));
|
|
182
|
-
}
|
|
183
|
-
if (status === "done") {
|
|
184
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "green", children: "\u2705 Tool saved successfully!" }), _jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { dimColor: true, children: ["Name: ", config.name] }), _jsxs(Text, { dimColor: true, children: ["Path: ", config.path] })] }), attachedAgents.length > 0 && (_jsx(Box, { marginTop: 1, flexDirection: "column", children: _jsxs(Text, { dimColor: true, children: ["Registered with agents: ", attachedAgents.join(", ")] }) }))] }));
|
|
185
|
-
}
|
|
186
|
-
return null;
|
|
187
|
-
}
|
|
188
|
-
// ============================================================================
|
|
189
|
-
// Main Component
|
|
190
|
-
// ============================================================================
|
|
191
|
-
function ToolStubApp({ name: initialName }) {
|
|
192
|
-
const { exit } = useApp();
|
|
193
|
-
const [stage, setStage] = useState("name");
|
|
194
|
-
const [config, setConfig] = useState({});
|
|
195
|
-
const [nameInput, setNameInput] = useState(initialName || "");
|
|
196
|
-
const [saveStatus, setSaveStatus] = useState("pending");
|
|
197
|
-
const [saveError, setSaveError] = useState(null);
|
|
198
|
-
const [selectedAgents, setSelectedAgents] = useState([]);
|
|
199
|
-
const [tempFilePath, setTempFilePath] = useState(null);
|
|
200
|
-
const [editorOpened, setEditorOpened] = useState(false);
|
|
201
|
-
// On mount, create temp file and open editor
|
|
202
|
-
useEffect(() => {
|
|
203
|
-
if (editorOpened)
|
|
204
|
-
return;
|
|
205
|
-
const setupAndOpenEditor = async () => {
|
|
206
|
-
try {
|
|
207
|
-
// Generate temp file path
|
|
208
|
-
const tempFile = join(tmpdir(), `town-tool-${Date.now()}.ts`);
|
|
209
|
-
setTempFilePath(tempFile);
|
|
210
|
-
// Generate and write stub template
|
|
211
|
-
const stubContent = generateStubTemplate(initialName);
|
|
212
|
-
writeFileSync(tempFile, stubContent, "utf-8");
|
|
213
|
-
// Open in editor
|
|
214
|
-
await openInEditor(tempFile);
|
|
215
|
-
// After editor closes, move to name input stage
|
|
216
|
-
setEditorOpened(true);
|
|
217
|
-
}
|
|
218
|
-
catch (error) {
|
|
219
|
-
setSaveStatus("error");
|
|
220
|
-
setSaveError(`Failed to open editor: ${error instanceof Error ? error.message : String(error)}`);
|
|
221
|
-
setStage("done");
|
|
222
|
-
}
|
|
223
|
-
};
|
|
224
|
-
setupAndOpenEditor();
|
|
225
|
-
}, [initialName, editorOpened]);
|
|
226
|
-
const handleSave = (toolName, agentsToAttach) => {
|
|
227
|
-
setSaveStatus("saving");
|
|
228
|
-
try {
|
|
229
|
-
if (!tempFilePath) {
|
|
230
|
-
setSaveStatus("error");
|
|
231
|
-
setSaveError("Temp file path not found");
|
|
232
|
-
return;
|
|
233
|
-
}
|
|
234
|
-
// Check if temp file still exists
|
|
235
|
-
if (!existsSync(tempFilePath)) {
|
|
236
|
-
setSaveStatus("error");
|
|
237
|
-
setSaveError("Temp file was deleted");
|
|
238
|
-
return;
|
|
239
|
-
}
|
|
240
|
-
// Read the edited content
|
|
241
|
-
const editedContent = readFileSync(tempFilePath, "utf-8");
|
|
242
|
-
// Ensure tools directory exists
|
|
243
|
-
ensureToolsDir();
|
|
244
|
-
// Normalize the tool name for the filename
|
|
245
|
-
const normalizedName = normalizeToolName(toolName);
|
|
246
|
-
const toolsDir = getToolsDir();
|
|
247
|
-
const targetPath = join(toolsDir, `${normalizedName}.ts`);
|
|
248
|
-
// Write the edited content to the target path
|
|
249
|
-
try {
|
|
250
|
-
writeFileSync(targetPath, editedContent, "utf-8");
|
|
251
|
-
}
|
|
252
|
-
catch (error) {
|
|
253
|
-
setSaveStatus("error");
|
|
254
|
-
setSaveError(`Failed to save file: ${error instanceof Error ? error.message : String(error)}`);
|
|
255
|
-
return;
|
|
256
|
-
}
|
|
257
|
-
// Create tool config
|
|
258
|
-
const toolConfig = {
|
|
259
|
-
name: toolName,
|
|
260
|
-
path: targetPath,
|
|
261
|
-
};
|
|
262
|
-
// Save to global tool storage
|
|
263
|
-
saveToolConfig(toolConfig);
|
|
264
|
-
// Update selected agents
|
|
265
|
-
for (const agentName of agentsToAttach) {
|
|
266
|
-
try {
|
|
267
|
-
const agentPath = getAgentPath(agentName);
|
|
268
|
-
const agentJsonPath = join(agentPath, "agent.json");
|
|
269
|
-
// Read existing agent.json
|
|
270
|
-
const agentJsonContent = readFileSync(agentJsonPath, "utf-8");
|
|
271
|
-
const agentDef = JSON.parse(agentJsonContent);
|
|
272
|
-
// Add tool to tools array (create array if doesn't exist)
|
|
273
|
-
if (!agentDef.tools) {
|
|
274
|
-
agentDef.tools = [];
|
|
275
|
-
}
|
|
276
|
-
// Create custom tool object
|
|
277
|
-
const customTool = {
|
|
278
|
-
type: "custom",
|
|
279
|
-
modulePath: targetPath,
|
|
280
|
-
};
|
|
281
|
-
// Check if this tool is already in the agent's config
|
|
282
|
-
const existingIndex = agentDef.tools.findIndex((tool) => typeof tool === "object" &&
|
|
283
|
-
tool !== null &&
|
|
284
|
-
"type" in tool &&
|
|
285
|
-
tool.type === "custom" &&
|
|
286
|
-
"modulePath" in tool &&
|
|
287
|
-
tool.modulePath === targetPath);
|
|
288
|
-
if (existingIndex >= 0) {
|
|
289
|
-
// Update existing config
|
|
290
|
-
agentDef.tools[existingIndex] = customTool;
|
|
291
|
-
}
|
|
292
|
-
else {
|
|
293
|
-
// Add new config
|
|
294
|
-
agentDef.tools.push(customTool);
|
|
295
|
-
}
|
|
296
|
-
// Write back to agent.json
|
|
297
|
-
writeFileSync(agentJsonPath, JSON.stringify(agentDef, null, 2));
|
|
298
|
-
}
|
|
299
|
-
catch (error) {
|
|
300
|
-
console.error(`Error updating agent ${agentName}:`, error);
|
|
301
|
-
// Continue with other agents even if one fails
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
// Clean up temp file
|
|
305
|
-
try {
|
|
306
|
-
unlinkSync(tempFilePath);
|
|
307
|
-
}
|
|
308
|
-
catch (error) {
|
|
309
|
-
// Ignore cleanup errors
|
|
310
|
-
}
|
|
311
|
-
setConfig(toolConfig);
|
|
312
|
-
setSaveStatus("done");
|
|
313
|
-
}
|
|
314
|
-
catch (error) {
|
|
315
|
-
setSaveStatus("error");
|
|
316
|
-
setSaveError(error instanceof Error ? error.message : "Unknown error occurred");
|
|
317
|
-
}
|
|
318
|
-
};
|
|
319
|
-
// Wait for editor to open and close before showing UI
|
|
320
|
-
if (!editorOpened) {
|
|
321
|
-
return (_jsx(Box, { flexDirection: "column", children: _jsx(Text, { children: "Opening editor..." }) }));
|
|
322
|
-
}
|
|
323
|
-
// If there was an error during editor setup, show done stage with error
|
|
324
|
-
if (saveStatus === "error" && stage === "done") {
|
|
325
|
-
return (_jsx(DoneStage, { config: config, status: saveStatus, error: saveError, attachedAgents: selectedAgents }));
|
|
326
|
-
}
|
|
327
|
-
// Name input stage
|
|
328
|
-
if (stage === "name") {
|
|
329
|
-
return (_jsx(NameInputStage, { value: nameInput, onChange: setNameInput, onNext: (name) => {
|
|
330
|
-
setConfig({ ...config, name });
|
|
331
|
-
setStage("agentSelection");
|
|
332
|
-
}, onBack: () => {
|
|
333
|
-
// Clean up temp file
|
|
334
|
-
if (tempFilePath) {
|
|
335
|
-
try {
|
|
336
|
-
unlinkSync(tempFilePath);
|
|
337
|
-
}
|
|
338
|
-
catch (error) {
|
|
339
|
-
// Ignore cleanup errors
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
exit();
|
|
343
|
-
} }));
|
|
344
|
-
}
|
|
345
|
-
// Agent selection stage
|
|
346
|
-
if (stage === "agentSelection") {
|
|
347
|
-
return (_jsx(AgentSelectionStage, { selectedAgents: selectedAgents, onSelectedAgentsChange: setSelectedAgents, onNext: () => {
|
|
348
|
-
if (!config.name) {
|
|
349
|
-
setSaveStatus("error");
|
|
350
|
-
setSaveError("Tool name is required");
|
|
351
|
-
setStage("done");
|
|
352
|
-
return;
|
|
353
|
-
}
|
|
354
|
-
handleSave(config.name, selectedAgents);
|
|
355
|
-
setStage("done");
|
|
356
|
-
}, onBack: () => setStage("name") }));
|
|
357
|
-
}
|
|
358
|
-
// Done stage
|
|
359
|
-
if (stage === "done") {
|
|
360
|
-
return (_jsx(DoneStage, { config: config, status: saveStatus, error: saveError, attachedAgents: selectedAgents }));
|
|
361
|
-
}
|
|
362
|
-
return null;
|
|
363
|
-
}
|
|
364
|
-
// ============================================================================
|
|
365
|
-
// Export and Runner
|
|
366
|
-
// ============================================================================
|
|
367
|
-
export default ToolStubApp;
|
|
368
|
-
export async function runToolStub(props = {}) {
|
|
369
|
-
// Set stdin to raw mode to capture input
|
|
370
|
-
if (process.stdin.isTTY) {
|
|
371
|
-
process.stdin.setRawMode(true);
|
|
372
|
-
}
|
|
373
|
-
const { waitUntilExit } = render(_jsx(ToolStubApp, { ...props }));
|
|
374
|
-
// Wait for the app to exit before returning
|
|
375
|
-
await waitUntilExit();
|
|
376
|
-
}
|