@townco/cli 0.1.113 → 0.1.115

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.
@@ -7,6 +7,7 @@ declare const _default: {
7
7
  readonly systemPrompt: string | undefined;
8
8
  readonly init: string | undefined;
9
9
  readonly claude: true | undefined;
10
+ readonly yes: true | undefined;
10
11
  }, ["matched", string] | ["parsing", {
11
12
  readonly command: "create";
12
13
  readonly name: [import("@optique/core").ValueParserResult<string> | undefined] | undefined;
@@ -15,6 +16,7 @@ declare const _default: {
15
16
  readonly systemPrompt: [import("@optique/core").ValueParserResult<string> | undefined] | undefined;
16
17
  readonly init: [import("@optique/core").ValueParserResult<string> | undefined] | undefined;
17
18
  readonly claude: [import("@optique/core").ValueParserResult<true> | undefined] | undefined;
19
+ readonly yes: [import("@optique/core").ValueParserResult<true> | undefined] | undefined;
18
20
  }] | undefined>;
19
21
  impl: (def: {
20
22
  readonly command: "create";
@@ -24,6 +26,7 @@ declare const _default: {
24
26
  readonly systemPrompt: string | undefined;
25
27
  readonly init: string | undefined;
26
28
  readonly claude: true | undefined;
29
+ readonly yes: true | undefined;
27
30
  }) => unknown;
28
31
  };
29
32
  export default _default;
@@ -15,10 +15,11 @@ export default createCommand({
15
15
  systemPrompt: optional(option("-p", "--prompt", string())),
16
16
  init: optional(option("--init", string())),
17
17
  claude: optional(flag("--claude")),
18
+ yes: optional(flag("-y", "--yes")),
18
19
  }), {
19
- brief: message `Create a new agent or project (with --init <path>). Use --claude to add Claude Code integration.`,
20
+ brief: message `Create a new agent or project (with --init <path>). Use --claude to add Claude Code integration. Use -y/--yes to skip confirmation prompts.`,
20
21
  }),
21
- impl: async ({ name, model, tools, systemPrompt, init, claude }) => {
22
+ impl: async ({ name, model, tools, systemPrompt, init, claude, yes }) => {
22
23
  // Handle --claude flag (initialize .claude in existing project)
23
24
  if (claude === true) {
24
25
  if (init !== null && init !== undefined) {
@@ -65,6 +66,7 @@ export default createCommand({
65
66
  ...(model !== undefined && { model }),
66
67
  ...(tools.length > 0 && { tools }),
67
68
  ...(systemPrompt !== undefined && { systemPrompt }),
69
+ ...(yes === true && { yes: true }),
68
70
  agentsDir: join(process.cwd(), "agents"),
69
71
  });
70
72
  }
@@ -84,6 +86,7 @@ export default createCommand({
84
86
  ...(model !== undefined && { model }),
85
87
  ...(tools.length > 0 && { tools }),
86
88
  ...(systemPrompt !== undefined && { systemPrompt }),
89
+ ...(yes === true && { yes: true }),
87
90
  agentsDir: join(projectRoot, "agents"),
88
91
  });
89
92
  }
@@ -4,6 +4,7 @@ interface CreateCommandProps {
4
4
  tools?: readonly string[];
5
5
  systemPrompt?: string;
6
6
  overwrite?: boolean;
7
+ yes?: boolean;
7
8
  agentsDir: string;
8
9
  }
9
10
  export declare function createCommand(props: CreateCommandProps): Promise<void>;
@@ -70,9 +70,13 @@ function NameInput({ nameInput, setNameInput, onSubmit }) {
70
70
  ] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: Continue \u2022 Esc: Cancel" }) })
71
71
  ] }));
72
72
  }
73
- function CreateApp({ name: initialName, model: initialModel, tools: initialTools, systemPrompt: initialSystemPrompt, overwrite = false, agentsDir, }) {
73
+ function CreateApp({ name: initialName, model: initialModel, tools: initialTools, systemPrompt: initialSystemPrompt, overwrite = false, yes = false, agentsDir, }) {
74
74
  // Determine the starting stage based on what's provided
75
75
  const determineInitialStage = () => {
76
+ // If --yes flag is set and we have name and model, skip directly to done
77
+ if (yes && initialName && initialModel) {
78
+ return "done";
79
+ }
76
80
  if (!initialName)
77
81
  return "name";
78
82
  if (!initialModel)
@@ -0,0 +1,10 @@
1
+ type CheckStatus = "pass" | "fail" | "warn";
2
+ type CheckCategory = "model" | "mcp";
3
+ export interface CheckResult {
4
+ category: CheckCategory;
5
+ name: string;
6
+ status: CheckStatus;
7
+ message: string;
8
+ details?: string;
9
+ }
10
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,18 @@
1
+ import type { AgentDefinition } from "@townco/agent/definition";
2
+ import type { CheckResult } from "./checks";
3
+ import { type Provider } from "./model-checks";
4
+ interface DoctorResult {
5
+ agent: string;
6
+ model: string;
7
+ provider: Provider;
8
+ checks: CheckResult[];
9
+ success: boolean;
10
+ subagents?: DoctorResult[];
11
+ }
12
+ interface DoctorCommandOptions {
13
+ name: string;
14
+ json?: boolean;
15
+ }
16
+ export declare function doctorCommand({ name, json }: DoctorCommandOptions): Promise<void>;
17
+ export declare function runDoctorChecks(agentName: string, definition: AgentDefinition, projectRoot: string, visited?: Set<string>): Promise<DoctorResult>;
18
+ export {};
@@ -0,0 +1,140 @@
1
+ import { join } from "node:path";
2
+ import { checkAllMcpHealth } from "@townco/agent/mcp";
3
+ import { isInsideTownProject } from "@townco/agent/storage";
4
+ import { checkModel } from "./model-checks";
5
+ export async function doctorCommand({ name, json = false, }) {
6
+ const projectRoot = await isInsideTownProject();
7
+ if (!projectRoot) {
8
+ console.error("Not inside a Town project");
9
+ process.exit(1);
10
+ }
11
+ const agentIndexPath = join(projectRoot, "agents", name, "index.ts");
12
+ try {
13
+ const agentModule = (await import(agentIndexPath));
14
+ const definition = agentModule.default;
15
+ const result = await runDoctorChecks(name, definition, projectRoot);
16
+ if (json) {
17
+ console.log(JSON.stringify(result, null, 2));
18
+ }
19
+ else {
20
+ printDoctorResult(result);
21
+ }
22
+ if (!result.success) {
23
+ process.exit(1);
24
+ }
25
+ }
26
+ catch (error) {
27
+ console.error(`Could not load agent definition at ${agentIndexPath}`);
28
+ if (error instanceof Error) {
29
+ console.error(` ${error.message}`);
30
+ }
31
+ process.exit(1);
32
+ }
33
+ }
34
+ export async function runDoctorChecks(agentName, definition, projectRoot, visited = new Set()) {
35
+ // Add current agent to visited set for cycle detection
36
+ visited.add(agentName);
37
+ const modelString = definition.model;
38
+ const { provider, checks: modelChecks } = await checkModel(modelString);
39
+ const mcpResults = await checkAllMcpHealth(definition.mcps ?? []);
40
+ const mcpChecks = mcpResults.map((r) => {
41
+ const base = {
42
+ category: "mcp",
43
+ name: r.name,
44
+ status: r.status,
45
+ message: r.toolCount ? `${r.message} (${r.toolCount} tools)` : r.message,
46
+ };
47
+ return r.details ? { ...base, details: r.details } : base;
48
+ });
49
+ const checks = [...modelChecks, ...mcpChecks];
50
+ const subagentResults = await checkSubagents(definition, projectRoot, visited);
51
+ const ownSuccess = checks.every((c) => c.status !== "fail");
52
+ const subagentsSuccess = subagentResults.every((s) => s.success);
53
+ const result = {
54
+ agent: agentName,
55
+ model: modelString,
56
+ provider,
57
+ checks,
58
+ success: ownSuccess && subagentsSuccess,
59
+ };
60
+ if (subagentResults.length > 0) {
61
+ result.subagents = subagentResults;
62
+ }
63
+ return result;
64
+ }
65
+ const SUBAGENT_TOOL_NAME = "subagent";
66
+ function extractSubagentNames(definition) {
67
+ const taskTool = definition.tools?.find((t) => typeof t === "object" &&
68
+ "type" in t &&
69
+ t.type === "direct" &&
70
+ "name" in t &&
71
+ t.name === SUBAGENT_TOOL_NAME);
72
+ const configs = taskTool
73
+ ?.subagentConfigs;
74
+ return configs?.map((c) => c.agentName) ?? [];
75
+ }
76
+ async function checkSubagents(definition, projectRoot, visited) {
77
+ const subagentNames = extractSubagentNames(definition);
78
+ const results = [];
79
+ for (const name of subagentNames) {
80
+ if (visited.has(name)) {
81
+ continue;
82
+ }
83
+ const subagentPath = join(projectRoot, "agents", name, "index.ts");
84
+ try {
85
+ const subagentModule = (await import(subagentPath));
86
+ const result = await runDoctorChecks(name, subagentModule.default, projectRoot, visited);
87
+ results.push(result);
88
+ }
89
+ catch (error) {
90
+ results.push({
91
+ agent: name,
92
+ model: "unknown",
93
+ provider: "unknown",
94
+ checks: [
95
+ {
96
+ category: "model",
97
+ name: "load",
98
+ status: "fail",
99
+ message: "Could not load subagent",
100
+ details: error instanceof Error ? error.message : "Unknown error",
101
+ },
102
+ ],
103
+ success: false,
104
+ });
105
+ }
106
+ }
107
+ return results;
108
+ }
109
+ function formatCheckResult(check) {
110
+ const icon = check.status === "pass" ? "✓" : check.status === "warn" ? "⚠" : "✗";
111
+ let line = `${icon} ${check.message}`;
112
+ if (check.details) {
113
+ line += ` (${check.details})`;
114
+ }
115
+ return line;
116
+ }
117
+ function printDoctorResult(result, indent = 0) {
118
+ const pad = " ".repeat(indent);
119
+ console.log(`${pad}Model: ${result.model}`);
120
+ const modelChecks = result.checks.filter((c) => c.category === "model");
121
+ const mcpChecks = result.checks.filter((c) => c.category === "mcp");
122
+ for (const check of modelChecks) {
123
+ console.log(`${pad}${formatCheckResult(check)}`);
124
+ }
125
+ if (mcpChecks.length > 0) {
126
+ console.log("");
127
+ console.log(`${pad}MCPs:`);
128
+ for (const check of mcpChecks) {
129
+ console.log(`${pad}${formatCheckResult(check)}`);
130
+ }
131
+ }
132
+ if (result.subagents && result.subagents.length > 0) {
133
+ console.log("");
134
+ console.log(`${pad}Subagents:`);
135
+ for (const subagent of result.subagents) {
136
+ console.log(`${pad} ${subagent.agent}:`);
137
+ printDoctorResult(subagent, indent + 4);
138
+ }
139
+ }
140
+ }
@@ -0,0 +1,8 @@
1
+ import type { CheckResult } from "./checks";
2
+ export type Provider = "town" | "anthropic" | "vertex" | "gemini" | "unknown";
3
+ interface ModelCheckResult {
4
+ provider: Provider;
5
+ checks: CheckResult[];
6
+ }
7
+ export declare function checkModel(modelString: string): Promise<ModelCheckResult>;
8
+ export {};
@@ -0,0 +1,205 @@
1
+ import { getShedAuth } from "@townco/core/auth";
2
+ const townModelRegexp = /^town-/;
3
+ const vertexModelRegexp = /^vertex-/;
4
+ const geminiModelRegexp = /^gemini-/;
5
+ const anthropicModelRegexp = /^claude-/;
6
+ const FETCH_TIMEOUT_MS = 5000;
7
+ async function fetchWithTimeout(url, options = {}) {
8
+ const controller = new AbortController();
9
+ const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
10
+ try {
11
+ return await fetch(url, { ...options, signal: controller.signal });
12
+ }
13
+ finally {
14
+ clearTimeout(timeout);
15
+ }
16
+ }
17
+ function detectProvider(modelString) {
18
+ if (townModelRegexp.test(modelString))
19
+ return "town";
20
+ if (vertexModelRegexp.test(modelString))
21
+ return "vertex";
22
+ if (geminiModelRegexp.test(modelString))
23
+ return "gemini";
24
+ if (anthropicModelRegexp.test(modelString))
25
+ return "anthropic";
26
+ return "unknown";
27
+ }
28
+ async function checkTownProxiedModel(modelString) {
29
+ // Remove "town-" prefix to get the underlying model name
30
+ const actualModel = modelString.slice(5);
31
+ const shedAuth = await getShedAuth();
32
+ if (!shedAuth) {
33
+ return {
34
+ category: "model",
35
+ name: "town-auth",
36
+ status: "fail",
37
+ message: "Not logged in",
38
+ details: "Run 'town login' or set SHED_API_KEY",
39
+ };
40
+ }
41
+ try {
42
+ const response = await fetchWithTimeout(`${shedAuth.shedUrl}/api/anthropic/v1/models/${actualModel}`, {
43
+ headers: { "x-api-key": shedAuth.accessToken },
44
+ });
45
+ if (response.ok) {
46
+ return {
47
+ category: "model",
48
+ name: "town-api",
49
+ status: "pass",
50
+ message: "Model API connected successfully",
51
+ };
52
+ }
53
+ return {
54
+ category: "model",
55
+ name: "town-api",
56
+ status: "fail",
57
+ message: `Model API returned ${response.status}`,
58
+ details: `Check shed authentication (${shedAuth.shedUrl})`,
59
+ };
60
+ }
61
+ catch {
62
+ return {
63
+ category: "model",
64
+ name: "town-api",
65
+ status: "fail",
66
+ message: "Could not connect to shed",
67
+ details: shedAuth.shedUrl,
68
+ };
69
+ }
70
+ }
71
+ async function checkAnthropicModel(modelString) {
72
+ const apiKey = process.env.ANTHROPIC_API_KEY;
73
+ if (!apiKey) {
74
+ return {
75
+ category: "model",
76
+ name: "anthropic-api",
77
+ status: "fail",
78
+ message: "ANTHROPIC_API_KEY not set",
79
+ };
80
+ }
81
+ try {
82
+ const response = await fetchWithTimeout(`https://api.anthropic.com/v1/models/${modelString}`, {
83
+ headers: {
84
+ "x-api-key": apiKey,
85
+ "anthropic-version": "2023-06-01",
86
+ },
87
+ });
88
+ if (response.ok) {
89
+ return {
90
+ category: "model",
91
+ name: "anthropic-api",
92
+ status: "pass",
93
+ message: "Anthropic API connected successfully",
94
+ };
95
+ }
96
+ return {
97
+ category: "model",
98
+ name: "anthropic-api",
99
+ status: "fail",
100
+ message: `Anthropic API returned ${response.status}`,
101
+ details: "Check ANTHROPIC_API_KEY",
102
+ };
103
+ }
104
+ catch {
105
+ return {
106
+ category: "model",
107
+ name: "anthropic-api",
108
+ status: "fail",
109
+ message: "Could not connect to Anthropic API",
110
+ };
111
+ }
112
+ }
113
+ function checkVertexModel() {
114
+ if (!process.env.VERTEX_CREDENTIALS) {
115
+ return {
116
+ category: "model",
117
+ name: "vertex-credentials",
118
+ status: "fail",
119
+ message: "VERTEX_CREDENTIALS not set",
120
+ };
121
+ }
122
+ try {
123
+ JSON.parse(process.env.VERTEX_CREDENTIALS);
124
+ return {
125
+ category: "model",
126
+ name: "vertex-credentials",
127
+ status: "pass",
128
+ message: "VERTEX_CREDENTIALS is valid JSON",
129
+ };
130
+ }
131
+ catch {
132
+ return {
133
+ category: "model",
134
+ name: "vertex-credentials",
135
+ status: "fail",
136
+ message: "VERTEX_CREDENTIALS is invalid JSON",
137
+ };
138
+ }
139
+ }
140
+ async function checkGeminiModel() {
141
+ const apiKey = process.env.GEMINI_API_KEY;
142
+ if (!apiKey) {
143
+ return {
144
+ category: "model",
145
+ name: "gemini-api",
146
+ status: "fail",
147
+ message: "GEMINI_API_KEY not set",
148
+ };
149
+ }
150
+ try {
151
+ const response = await fetchWithTimeout(`https://generativelanguage.googleapis.com/v1beta/models?key=${apiKey}`);
152
+ if (response.ok) {
153
+ return {
154
+ category: "model",
155
+ name: "gemini-api",
156
+ status: "pass",
157
+ message: "Gemini API connected successfully",
158
+ };
159
+ }
160
+ return {
161
+ category: "model",
162
+ name: "gemini-api",
163
+ status: "fail",
164
+ message: `Gemini API returned ${response.status}`,
165
+ details: "Check GEMINI_API_KEY",
166
+ };
167
+ }
168
+ catch {
169
+ return {
170
+ category: "model",
171
+ name: "gemini-api",
172
+ status: "fail",
173
+ message: "Could not connect to Gemini API",
174
+ };
175
+ }
176
+ }
177
+ export async function checkModel(modelString) {
178
+ const provider = detectProvider(modelString);
179
+ let checks;
180
+ switch (provider) {
181
+ case "town":
182
+ checks = [await checkTownProxiedModel(modelString)];
183
+ break;
184
+ case "anthropic":
185
+ checks = [await checkAnthropicModel(modelString)];
186
+ break;
187
+ case "vertex":
188
+ checks = [checkVertexModel()];
189
+ break;
190
+ case "gemini":
191
+ checks = [await checkGeminiModel()];
192
+ break;
193
+ case "unknown":
194
+ checks = [
195
+ {
196
+ category: "model",
197
+ name: "unknown",
198
+ status: "fail",
199
+ message: `Unknown model format: ${modelString}`,
200
+ },
201
+ ];
202
+ break;
203
+ }
204
+ return { provider, checks };
205
+ }
@@ -0,0 +1,17 @@
1
+ declare const _default: {
2
+ def: import("@optique/core").Parser<{
3
+ readonly command: "doctor";
4
+ readonly name: string;
5
+ readonly json: true | undefined;
6
+ }, ["matched", string] | ["parsing", {
7
+ readonly command: "doctor";
8
+ readonly name: import("@optique/core").ValueParserResult<string> | undefined;
9
+ readonly json: [import("@optique/core").ValueParserResult<true> | undefined] | undefined;
10
+ }] | undefined>;
11
+ impl: (def: {
12
+ readonly command: "doctor";
13
+ readonly name: string;
14
+ readonly json: true | undefined;
15
+ }) => unknown;
16
+ };
17
+ export default _default;
@@ -0,0 +1,17 @@
1
+ import { argument, command, constant, flag, message, object, optional, string, } from "@optique/core";
2
+ import { createCommand } from "../lib/command";
3
+ import { doctorCommand } from "./doctor";
4
+ export default createCommand({
5
+ def: command("doctor", object({
6
+ command: constant("doctor"),
7
+ name: argument(string({ metavar: "NAME" }), {
8
+ description: message `Name of the agent to diagnose`,
9
+ }),
10
+ json: optional(flag("--json", {
11
+ description: message `Output results as JSON`,
12
+ })),
13
+ }), { brief: message `Run diagnostics on an agent.` }),
14
+ impl: async ({ name, json }) => {
15
+ await doctorCommand({ name, json: json ?? false });
16
+ },
17
+ });
package/dist/index.js CHANGED
@@ -7,6 +7,7 @@ import batchCmd from "./commands/batch-wrapper";
7
7
  import { configureCommand } from "./commands/configure";
8
8
  import createCmd from "./commands/create-wrapper";
9
9
  import deployCmd from "./commands/deploy";
10
+ import doctorCmd from "./commands/doctor-wrapper";
10
11
  import { loginCommand } from "./commands/login";
11
12
  import runCmd from "./commands/run-wrapper";
12
13
  import secretCmd from "./commands/secret";
@@ -14,7 +15,7 @@ import { upgradeCommand } from "./commands/upgrade";
14
15
  import { whoamiCommand } from "./commands/whoami";
15
16
  const parser = or(batchCmd.def, command("configure", constant("configure"), {
16
17
  brief: message `Configure environment variables.`,
17
- }), command("login", constant("login"), { brief: message `Log in to Town Shed.` }), command("upgrade", constant("upgrade"), {
18
+ }), doctorCmd.def, command("login", constant("login"), { brief: message `Log in to Town Shed.` }), command("upgrade", constant("upgrade"), {
18
19
  brief: message `Upgrade dependencies by cleaning and reinstalling.`,
19
20
  }), command("whoami", constant("whoami"), {
20
21
  brief: message `Show current login status.`,
@@ -35,6 +36,7 @@ const main = async (parser, meta) => await match(run(parser, meta))
35
36
  .with({ command: "batch" }, batchCmd.impl)
36
37
  .with({ command: "create" }, createCmd.impl)
37
38
  .with({ command: "deploy" }, deployCmd.impl)
39
+ .with({ command: "doctor" }, doctorCmd.impl)
38
40
  .with({ command: "run" }, runCmd.impl)
39
41
  .with({ command: "secret" }, secretCmd.impl)
40
42
  .exhaustive();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@townco/cli",
3
- "version": "0.1.113",
3
+ "version": "0.1.115",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "town": "./dist/index.js"
@@ -15,7 +15,7 @@
15
15
  "build": "tsgo"
16
16
  },
17
17
  "devDependencies": {
18
- "@townco/tsconfig": "0.1.105",
18
+ "@townco/tsconfig": "0.1.107",
19
19
  "@types/archiver": "^7.0.0",
20
20
  "@types/bun": "^1.3.1",
21
21
  "@types/ignore-walk": "^4.0.3",
@@ -24,13 +24,13 @@
24
24
  "dependencies": {
25
25
  "@optique/core": "^0.6.2",
26
26
  "@optique/run": "^0.6.2",
27
- "@townco/agent": "0.1.116",
28
- "@townco/apiclient": "0.0.28",
29
- "@townco/core": "0.0.86",
30
- "@townco/debugger": "0.1.64",
31
- "@townco/env": "0.1.58",
32
- "@townco/secret": "0.1.108",
33
- "@townco/ui": "0.1.108",
27
+ "@townco/agent": "0.1.118",
28
+ "@townco/apiclient": "0.0.30",
29
+ "@townco/core": "0.0.88",
30
+ "@townco/debugger": "0.1.66",
31
+ "@townco/env": "0.1.60",
32
+ "@townco/secret": "0.1.110",
33
+ "@townco/ui": "0.1.110",
34
34
  "@trpc/client": "^11.7.2",
35
35
  "archiver": "^7.0.1",
36
36
  "eventsource": "^4.1.0",