@townco/cli 0.1.11 → 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/README.md +66 -0
- package/dist/commands/create.d.ts +6 -5
- package/dist/commands/create.js +12 -78
- package/dist/commands/mcp-add.d.ts +5 -10
- package/dist/commands/mcp-list.js +47 -174
- package/dist/commands/run.d.ts +4 -4
- package/dist/commands/tool-add.d.ts +6 -0
- package/dist/commands/tool-add.js +349 -0
- package/dist/commands/tool-list.d.ts +3 -0
- package/dist/commands/tool-list.js +61 -0
- package/dist/commands/tool-register.d.ts +7 -0
- package/dist/commands/tool-register.js +291 -0
- package/dist/commands/tool-remove.d.ts +3 -0
- package/dist/commands/tool-remove.js +202 -0
- package/dist/index.js +37 -2
- package/dist/lib/editor-utils.d.ts +15 -0
- package/dist/lib/editor-utils.js +112 -0
- package/dist/lib/mcp-storage.d.ts +5 -5
- package/dist/lib/mcp-storage.js +48 -50
- package/dist/lib/tool-storage.d.ts +32 -0
- package/dist/lib/tool-storage.js +107 -0
- package/package.json +6 -6
|
@@ -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
|
+
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
export type MCPConfig = {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
name: string;
|
|
3
|
+
transport: "stdio" | "http";
|
|
4
|
+
command?: string;
|
|
5
|
+
args?: string[];
|
|
6
|
+
url?: string;
|
|
7
7
|
};
|
|
8
8
|
/**
|
|
9
9
|
* Save an MCP config to the store
|
package/dist/lib/mcp-storage.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
|
|
5
4
|
// ============================================================================
|
|
6
5
|
// Constants
|
|
7
6
|
// ============================================================================
|
|
@@ -14,40 +13,38 @@ const MCPS_FILE = join(TOWN_CONFIG_DIR, "mcps.json");
|
|
|
14
13
|
* Ensure the config directory exists
|
|
15
14
|
*/
|
|
16
15
|
function ensureConfigDir() {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
if (!existsSync(TOWN_CONFIG_DIR)) {
|
|
17
|
+
mkdirSync(TOWN_CONFIG_DIR, { recursive: true });
|
|
18
|
+
}
|
|
20
19
|
}
|
|
21
20
|
/**
|
|
22
21
|
* Load all MCP configs from the JSON file
|
|
23
22
|
*/
|
|
24
23
|
function loadStore() {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
24
|
+
ensureConfigDir();
|
|
25
|
+
if (!existsSync(MCPS_FILE)) {
|
|
26
|
+
return {};
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
const content = readFileSync(MCPS_FILE, "utf-8");
|
|
30
|
+
return JSON.parse(content);
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
throw new Error(`Failed to load MCP configs: ${error instanceof Error ? error.message : String(error)}`);
|
|
34
|
+
}
|
|
37
35
|
}
|
|
38
36
|
/**
|
|
39
37
|
* Save all MCP configs to the JSON file
|
|
40
38
|
*/
|
|
41
39
|
function saveStore(store) {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}
|
|
40
|
+
ensureConfigDir();
|
|
41
|
+
try {
|
|
42
|
+
const content = JSON.stringify(store, null, 2);
|
|
43
|
+
writeFileSync(MCPS_FILE, content, "utf-8");
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
throw new Error(`Failed to save MCP configs: ${error instanceof Error ? error.message : String(error)}`);
|
|
47
|
+
}
|
|
51
48
|
}
|
|
52
49
|
// ============================================================================
|
|
53
50
|
// Public API
|
|
@@ -56,54 +53,55 @@ function saveStore(store) {
|
|
|
56
53
|
* Save an MCP config to the store
|
|
57
54
|
*/
|
|
58
55
|
export function saveMCPConfig(config) {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
56
|
+
const store = loadStore();
|
|
57
|
+
store[config.name] = config;
|
|
58
|
+
saveStore(store);
|
|
62
59
|
}
|
|
63
60
|
/**
|
|
64
61
|
* Load an MCP config by name
|
|
65
62
|
*/
|
|
66
63
|
export function loadMCPConfig(name) {
|
|
67
|
-
|
|
68
|
-
|
|
64
|
+
const store = loadStore();
|
|
65
|
+
return store[name] || null;
|
|
69
66
|
}
|
|
70
67
|
/**
|
|
71
68
|
* Delete an MCP config by name
|
|
72
69
|
*/
|
|
73
70
|
export function deleteMCPConfig(name) {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
71
|
+
const store = loadStore();
|
|
72
|
+
if (store[name]) {
|
|
73
|
+
delete store[name];
|
|
74
|
+
saveStore(store);
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
return false;
|
|
81
78
|
}
|
|
82
79
|
/**
|
|
83
80
|
* List all MCP configs
|
|
84
81
|
*/
|
|
85
82
|
export function listMCPConfigs() {
|
|
86
|
-
|
|
87
|
-
|
|
83
|
+
const store = loadStore();
|
|
84
|
+
return Object.values(store).sort((a, b) => a.name.localeCompare(b.name));
|
|
88
85
|
}
|
|
89
86
|
/**
|
|
90
87
|
* Check if an MCP config exists
|
|
91
88
|
*/
|
|
92
89
|
export function mcpConfigExists(name) {
|
|
93
|
-
|
|
94
|
-
|
|
90
|
+
const store = loadStore();
|
|
91
|
+
return name in store;
|
|
95
92
|
}
|
|
96
93
|
/**
|
|
97
94
|
* Get a summary of an MCP config for display
|
|
98
95
|
*/
|
|
99
96
|
export function getMCPSummary(config) {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
97
|
+
if (config.transport === "http") {
|
|
98
|
+
return `HTTP: ${config.url}`;
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
const parts = [`Stdio: ${config.command}`];
|
|
102
|
+
if (config.args && config.args.length > 0) {
|
|
103
|
+
parts.push(`[${config.args.join(" ")}]`);
|
|
104
|
+
}
|
|
105
|
+
return parts.join(" ");
|
|
106
|
+
}
|
|
109
107
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export type ToolConfig = {
|
|
2
|
+
name: string;
|
|
3
|
+
path: string;
|
|
4
|
+
};
|
|
5
|
+
/**
|
|
6
|
+
* Ensure the tools directory exists
|
|
7
|
+
*/
|
|
8
|
+
export declare function ensureToolsDir(): void;
|
|
9
|
+
/**
|
|
10
|
+
* Get the tools directory path
|
|
11
|
+
*/
|
|
12
|
+
export declare function getToolsDir(): string;
|
|
13
|
+
/**
|
|
14
|
+
* Save a tool config to the store
|
|
15
|
+
*/
|
|
16
|
+
export declare function saveToolConfig(config: ToolConfig): void;
|
|
17
|
+
/**
|
|
18
|
+
* Load a tool config by name
|
|
19
|
+
*/
|
|
20
|
+
export declare function loadToolConfig(name: string): ToolConfig | null;
|
|
21
|
+
/**
|
|
22
|
+
* Delete a tool config by name
|
|
23
|
+
*/
|
|
24
|
+
export declare function deleteToolConfig(name: string): boolean;
|
|
25
|
+
/**
|
|
26
|
+
* List all tool configs
|
|
27
|
+
*/
|
|
28
|
+
export declare function listToolConfigs(): ToolConfig[];
|
|
29
|
+
/**
|
|
30
|
+
* Check if a tool config exists
|
|
31
|
+
*/
|
|
32
|
+
export declare function toolConfigExists(name: string): boolean;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
// ============================================================================
|
|
5
|
+
// Constants
|
|
6
|
+
// ============================================================================
|
|
7
|
+
const TOWN_CONFIG_DIR = join(homedir(), ".config", "town");
|
|
8
|
+
const TOOLS_FILE = join(TOWN_CONFIG_DIR, "tools.json");
|
|
9
|
+
const TOOLS_DIR = join(TOWN_CONFIG_DIR, "tools");
|
|
10
|
+
// ============================================================================
|
|
11
|
+
// Helper Functions
|
|
12
|
+
// ============================================================================
|
|
13
|
+
/**
|
|
14
|
+
* Ensure the config directory exists
|
|
15
|
+
*/
|
|
16
|
+
function ensureConfigDir() {
|
|
17
|
+
if (!existsSync(TOWN_CONFIG_DIR)) {
|
|
18
|
+
mkdirSync(TOWN_CONFIG_DIR, { recursive: true });
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Ensure the tools directory exists
|
|
23
|
+
*/
|
|
24
|
+
export function ensureToolsDir() {
|
|
25
|
+
if (!existsSync(TOOLS_DIR)) {
|
|
26
|
+
mkdirSync(TOOLS_DIR, { recursive: true });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Get the tools directory path
|
|
31
|
+
*/
|
|
32
|
+
export function getToolsDir() {
|
|
33
|
+
return TOOLS_DIR;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Load all tool configs from the JSON file
|
|
37
|
+
*/
|
|
38
|
+
function loadStore() {
|
|
39
|
+
ensureConfigDir();
|
|
40
|
+
if (!existsSync(TOOLS_FILE)) {
|
|
41
|
+
return {};
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
const content = readFileSync(TOOLS_FILE, "utf-8");
|
|
45
|
+
return JSON.parse(content);
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
throw new Error(`Failed to load tool configs: ${error instanceof Error ? error.message : String(error)}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Save all tool configs to the JSON file
|
|
53
|
+
*/
|
|
54
|
+
function saveStore(store) {
|
|
55
|
+
ensureConfigDir();
|
|
56
|
+
try {
|
|
57
|
+
const content = JSON.stringify(store, null, 2);
|
|
58
|
+
writeFileSync(TOOLS_FILE, content, "utf-8");
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
throw new Error(`Failed to save tool configs: ${error instanceof Error ? error.message : String(error)}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// ============================================================================
|
|
65
|
+
// Public API
|
|
66
|
+
// ============================================================================
|
|
67
|
+
/**
|
|
68
|
+
* Save a tool config to the store
|
|
69
|
+
*/
|
|
70
|
+
export function saveToolConfig(config) {
|
|
71
|
+
const store = loadStore();
|
|
72
|
+
store[config.name] = config;
|
|
73
|
+
saveStore(store);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Load a tool config by name
|
|
77
|
+
*/
|
|
78
|
+
export function loadToolConfig(name) {
|
|
79
|
+
const store = loadStore();
|
|
80
|
+
return store[name] || null;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Delete a tool config by name
|
|
84
|
+
*/
|
|
85
|
+
export function deleteToolConfig(name) {
|
|
86
|
+
const store = loadStore();
|
|
87
|
+
if (store[name]) {
|
|
88
|
+
delete store[name];
|
|
89
|
+
saveStore(store);
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* List all tool configs
|
|
96
|
+
*/
|
|
97
|
+
export function listToolConfigs() {
|
|
98
|
+
const store = loadStore();
|
|
99
|
+
return Object.values(store).sort((a, b) => a.name.localeCompare(b.name));
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Check if a tool config exists
|
|
103
|
+
*/
|
|
104
|
+
export function toolConfigExists(name) {
|
|
105
|
+
const store = loadStore();
|
|
106
|
+
return name in store;
|
|
107
|
+
}
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
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"
|
|
7
7
|
},
|
|
8
8
|
"files": [
|
|
9
9
|
"dist",
|
|
@@ -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",
|