@towles/tool 0.0.96 → 0.0.103

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.
@@ -2,10 +2,10 @@ import { rmSync, writeFileSync, mkdirSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { tmpdir } from "node:os";
4
4
 
5
- import { Args, Flags } from "@oclif/core";
5
+ import { defineCommand } from "citty";
6
6
  import consola from "consola";
7
7
 
8
- import { BaseCommand } from "../base.js";
8
+ import { debugArg } from "../shared.js";
9
9
  import {
10
10
  STEP_NAMES,
11
11
  fetchIssue,
@@ -21,95 +21,75 @@ import {
21
21
  } from "../../lib/auto-claude/index.js";
22
22
  import type { IssueContext, StepName } from "../../lib/auto-claude/index.js";
23
23
 
24
- export default class AutoClaude extends BaseCommand {
25
- static override aliases = ["ac"];
26
-
27
- static override description = "Automated issue-to-PR pipeline using Claude Code";
28
-
29
- static override args = {
30
- prompt: Args.string({
31
- description: "Run a single prompt (skips issue pipeline)",
24
+ export default defineCommand({
25
+ meta: { name: "auto-claude", description: "Automated issue-to-PR pipeline using Claude Code" },
26
+ args: {
27
+ prompt: {
28
+ type: "positional" as const,
32
29
  required: false,
33
- }),
34
- };
35
-
36
- static override examples = [
37
- {
38
- description: "Run a single prompt",
39
- command: '<%= config.bin %> auto-claude "Fix the login bug in auth.ts"',
40
- },
41
- {
42
- description: "Process a specific issue",
43
- command: "<%= config.bin %> auto-claude --issue 42",
44
- },
45
- {
46
- description: "Run until plan step",
47
- command: "<%= config.bin %> auto-claude --issue 42 --until plan",
48
- },
49
- {
50
- description: "Reset local state for an issue",
51
- command: "<%= config.bin %> auto-claude --reset 42",
52
- },
53
- {
54
- description: "Loop mode: poll for labeled issues",
55
- command: "<%= config.bin %> auto-claude --loop",
56
- },
57
- {
58
- description: "Loop with custom interval",
59
- command: "<%= config.bin %> auto-claude --loop --interval 45",
30
+ description: "Run a single prompt (skips issue pipeline)",
60
31
  },
61
- ];
62
-
63
- static override flags = {
64
- ...BaseCommand.baseFlags,
65
- "max-turns": Flags.integer({
32
+ debug: debugArg,
33
+ "max-turns": {
34
+ type: "string" as const,
66
35
  description: "Maximum conversation turns for prompt mode (default: 10)",
67
- default: 10,
68
- }),
69
- issue: Flags.integer({
70
- char: "i",
36
+ default: "10",
37
+ },
38
+ issue: {
39
+ type: "string" as const,
40
+ alias: "i",
71
41
  description: "Process a specific issue number",
72
- }),
73
- until: Flags.string({
74
- char: "u",
42
+ },
43
+ until: {
44
+ type: "string" as const,
45
+ alias: "u",
75
46
  description: `Stop after this step (${STEP_NAMES.join(", ")})`,
76
- options: [...STEP_NAMES],
77
- }),
78
- reset: Flags.integer({
47
+ },
48
+ reset: {
49
+ type: "string" as const,
79
50
  description: "Delete local state for an issue (force restart)",
80
- }),
81
- model: Flags.string({
51
+ },
52
+ model: {
53
+ type: "string" as const,
82
54
  description: "Claude model to use (default: opus)",
83
55
  default: "opus",
84
- }),
85
- loop: Flags.boolean({
56
+ },
57
+ loop: {
58
+ type: "boolean" as const,
86
59
  description: "Poll for labeled issues continuously",
87
60
  default: false,
88
- }),
89
- interval: Flags.integer({
61
+ },
62
+ interval: {
63
+ type: "string" as const,
90
64
  description: "Poll interval in minutes (default: 30)",
91
- }),
92
- limit: Flags.integer({
65
+ },
66
+ limit: {
67
+ type: "string" as const,
93
68
  description: "Max issues per iteration (default: 1)",
94
- default: 1,
95
- }),
96
- label: Flags.string({
69
+ default: "1",
70
+ },
71
+ label: {
72
+ type: "string" as const,
97
73
  description: "Trigger label (default: auto-claude)",
98
- }),
99
- "main-branch": Flags.string({
74
+ },
75
+ "main-branch": {
76
+ type: "string" as const,
100
77
  description: "Override main branch detection",
101
- }),
102
- "scope-path": Flags.string({
78
+ },
79
+ "scope-path": {
80
+ type: "string" as const,
103
81
  description: "Path within repo to scope work (default: .)",
104
- }),
105
- };
106
-
107
- async run(): Promise<void> {
108
- const { args, flags } = await this.parse(AutoClaude);
109
-
82
+ },
83
+ },
84
+ subCommands: {
85
+ list: () => import("./list.js").then((m) => m.default),
86
+ status: () => import("./status.js").then((m) => m.default),
87
+ retry: () => import("./retry.js").then((m) => m.default),
88
+ },
89
+ async run({ args }) {
110
90
  // Prompt mode: run a single prompt with structured output, skip issue pipeline
111
91
  if (args.prompt) {
112
- await initConfig({ model: flags.model });
92
+ await initConfig({ model: args.model });
113
93
 
114
94
  const promptDir = join(tmpdir(), "tt-auto-claude");
115
95
  mkdirSync(promptDir, { recursive: true });
@@ -118,41 +98,44 @@ export default class AutoClaude extends BaseCommand {
118
98
 
119
99
  const result = await runClaude({
120
100
  promptFile,
121
- maxTurns: flags["max-turns"],
101
+ maxTurns: Number(args["max-turns"]),
122
102
  });
123
103
 
124
104
  if (result.is_error) {
125
- this.error("Claude reported an error", { exit: 1 });
105
+ consola.error("Claude reported an error");
106
+ process.exit(1);
126
107
  }
127
108
  return;
128
109
  }
129
110
 
130
111
  // Issue pipeline mode
131
112
  const cfg = await initConfig({
132
- triggerLabel: flags.label,
133
- mainBranch: flags["main-branch"],
134
- scopePath: flags["scope-path"],
135
- model: flags.model,
113
+ triggerLabel: args.label,
114
+ mainBranch: args["main-branch"],
115
+ scopePath: args["scope-path"],
116
+ model: args.model,
136
117
  });
137
118
 
138
- if (flags.reset) {
139
- const issueDir = join(process.cwd(), `.auto-claude/issue-${flags.reset}`);
140
- log(`Resetting state for issue-${flags.reset}...`);
119
+ const resetIssue = args.reset ? Number(args.reset) : undefined;
120
+ if (resetIssue) {
121
+ const issueDir = join(process.cwd(), `.auto-claude/issue-${resetIssue}`);
122
+ log(`Resetting state for issue-${resetIssue}...`);
141
123
  rmSync(issueDir, { recursive: true, force: true });
142
124
  log(`Cleaned ${issueDir}`);
143
125
  return;
144
126
  }
145
127
 
146
- const untilStep = flags.until as StepName | undefined;
147
- const loopMode = flags.loop;
148
- const intervalMs = (flags.interval ?? cfg.loopIntervalMinutes) * 60_000;
149
- const limit = flags.limit ?? 1;
128
+ const untilStep = args.until as StepName | undefined;
129
+ const loopMode = args.loop as boolean;
130
+ const intervalMs = (args.interval ? Number(args.interval) : cfg.loopIntervalMinutes) * 60_000;
131
+ const limit = Number(args.limit);
150
132
 
151
133
  if (loopMode) {
152
134
  registerShutdownHandlers();
153
135
  log(`Loop mode — interval: ${intervalMs / 60_000}min, limit: ${limit}`);
154
136
  }
155
137
 
138
+ const issueNumber = args.issue ? Number(args.issue) : undefined;
156
139
  let iteration = 0;
157
140
 
158
141
  do {
@@ -177,8 +160,8 @@ export default class AutoClaude extends BaseCommand {
177
160
 
178
161
  log("Fetching labeled issues…");
179
162
  let contexts: IssueContext[];
180
- if (flags.issue) {
181
- const ctx = await fetchIssue(flags.issue);
163
+ if (issueNumber) {
164
+ const ctx = await fetchIssue(issueNumber);
182
165
  contexts = ctx ? [ctx] : [];
183
166
  } else {
184
167
  contexts = await fetchIssues(limit);
@@ -212,8 +195,8 @@ export default class AutoClaude extends BaseCommand {
212
195
  } while (loopMode);
213
196
 
214
197
  log("Done.");
215
- }
216
- }
198
+ },
199
+ });
217
200
 
218
201
  async function syncWithRemote(): Promise<void> {
219
202
  const cfg = getConfig();
@@ -1,61 +1,50 @@
1
- import { Flags } from "@oclif/core";
1
+ import { defineCommand } from "citty";
2
2
  import consola from "consola";
3
3
  import { colors } from "consola/utils";
4
4
  import { Fzf } from "fzf";
5
5
  import prompts from "prompts";
6
6
  import type { Choice } from "prompts";
7
7
 
8
- import { BaseCommand } from "../base.js";
8
+ import { debugArg } from "../shared.js";
9
9
  import { buildIssueChoices, computeColumnLayout } from "../gh/branch.js";
10
10
  import { STEP_NAMES, fetchIssue, initConfig, runPipeline } from "../../lib/auto-claude/index.js";
11
11
  import type { StepName } from "../../lib/auto-claude/index.js";
12
12
  import { getIssues, isGithubCliInstalled } from "../../utils/git/gh-cli-wrapper.js";
13
13
  import { getTerminalColumns } from "../../utils/render.js";
14
14
 
15
- export default class AutoClaudeList extends BaseCommand {
16
- static override description = "Interactively pick an auto-claude issue to process";
17
-
18
- static override examples = [
19
- {
20
- description: "Browse auto-claude labeled issues",
21
- command: "<%= config.bin %> auto-claude list",
22
- },
23
- {
24
- description: "Pick an issue and run until plan step",
25
- command: "<%= config.bin %> auto-claude list --until plan",
26
- },
27
- ];
28
-
29
- static override flags = {
30
- ...BaseCommand.baseFlags,
31
- until: Flags.string({
32
- char: "u",
15
+ export default defineCommand({
16
+ meta: { name: "list", description: "Interactively pick an auto-claude issue to process" },
17
+ args: {
18
+ debug: debugArg,
19
+ until: {
20
+ type: "string" as const,
21
+ alias: "u",
33
22
  description: `Stop after this step (${STEP_NAMES.join(", ")})`,
34
- options: [...STEP_NAMES],
35
- }),
36
- label: Flags.string({
23
+ },
24
+ label: {
25
+ type: "string" as const,
37
26
  description: "Trigger label (default: auto-claude)",
38
- }),
39
- "main-branch": Flags.string({
27
+ },
28
+ "main-branch": {
29
+ type: "string" as const,
40
30
  description: "Override main branch detection",
41
- }),
42
- "scope-path": Flags.string({
31
+ },
32
+ "scope-path": {
33
+ type: "string" as const,
43
34
  description: "Path within repo to scope work (default: .)",
44
- }),
45
- };
46
-
47
- async run(): Promise<void> {
48
- const { flags } = await this.parse(AutoClaudeList);
49
-
35
+ },
36
+ },
37
+ async run({ args }) {
50
38
  const cfg = await initConfig({
51
- triggerLabel: flags.label,
52
- mainBranch: flags["main-branch"],
53
- scopePath: flags["scope-path"],
39
+ triggerLabel: args.label,
40
+ mainBranch: args["main-branch"],
41
+ scopePath: args["scope-path"],
54
42
  });
55
43
 
56
44
  const cliInstalled = await isGithubCliInstalled();
57
45
  if (!cliInstalled) {
58
- this.error("GitHub CLI (gh) is not installed");
46
+ consola.error("GitHub CLI (gh) is not installed");
47
+ process.exit(1);
59
48
  }
60
49
 
61
50
  const issues = await getIssues({ cwd: process.cwd(), label: cfg.triggerLabel });
@@ -89,7 +78,7 @@ export default class AutoClaudeList extends BaseCommand {
89
78
  {
90
79
  onCancel: () => {
91
80
  consola.info(colors.dim("Canceled"));
92
- this.exit(0);
81
+ process.exit(0);
93
82
  },
94
83
  },
95
84
  );
@@ -101,14 +90,15 @@ export default class AutoClaudeList extends BaseCommand {
101
90
 
102
91
  const ctx = await fetchIssue(result.issueNumber);
103
92
  if (!ctx) {
104
- this.error(`Could not fetch issue #${result.issueNumber}`);
93
+ consola.error(`Could not fetch issue #${result.issueNumber}`);
94
+ process.exit(1);
105
95
  }
106
96
 
107
- const untilStep = flags.until as StepName | undefined;
97
+ const untilStep = args.until as StepName | undefined;
108
98
  await runPipeline(ctx, untilStep);
109
99
  } catch (e) {
110
100
  consola.error(e);
111
- this.exit(1);
101
+ process.exit(1);
112
102
  }
113
- }
114
- }
103
+ },
104
+ });
@@ -5,6 +5,8 @@ import { tmpdir } from "node:os";
5
5
  import consola from "consola";
6
6
  import { describe, it, expect, vi, beforeEach } from "vitest";
7
7
 
8
+ import type { Mock } from "vitest";
9
+
8
10
  import type { ExecSafeFn } from "../../lib/auto-claude/labels.js";
9
11
  import { LABELS } from "../../lib/auto-claude/labels.js";
10
12
  import { retryIssues } from "./retry.js";
@@ -12,15 +14,17 @@ import { retryIssues } from "./retry.js";
12
14
  // Suppress consola output during tests
13
15
  consola.level = -999;
14
16
 
15
- const mockExecSafe: ExecSafeFn = vi.fn().mockResolvedValue({ stdout: "", ok: true });
17
+ const mockExecSafe = vi.fn().mockResolvedValue({ stdout: "", ok: true }) as Mock & ExecSafeFn;
16
18
 
17
19
  function getGhEditCalls() {
18
- return vi
19
- .mocked(mockExecSafe)
20
- .mock.calls.filter(
21
- ([cmd, args]) => cmd === "gh" && args?.[0] === "issue" && args?.[1] === "edit",
20
+ return mockExecSafe.mock.calls
21
+ .filter(
22
+ (call: unknown[]) =>
23
+ call[0] === "gh" &&
24
+ (call[1] as string[])?.[0] === "issue" &&
25
+ (call[1] as string[])?.[1] === "edit",
22
26
  )
23
- .map(([, args]) => args as string[]);
27
+ .map((call: unknown[]) => call[1] as string[]);
24
28
  }
25
29
 
26
30
  describe("retryIssues", () => {
@@ -1,12 +1,12 @@
1
1
  import { rmSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
 
4
- import { Flags } from "@oclif/core";
4
+ import { defineCommand } from "citty";
5
5
  import consola from "consola";
6
6
  import { colors } from "consola/utils";
7
7
  import prompts from "prompts";
8
8
 
9
- import { BaseCommand } from "../base.js";
9
+ import { debugArg } from "../shared.js";
10
10
  import { initConfig } from "../../lib/auto-claude/index.js";
11
11
  import { LABELS, removeLabel, setLabel } from "../../lib/auto-claude/labels.js";
12
12
  import type { ExecSafeFn } from "../../lib/auto-claude/labels.js";
@@ -47,54 +47,41 @@ export async function retryIssues(
47
47
  return selected.length;
48
48
  }
49
49
 
50
- export default class AutoClaudeRetry extends BaseCommand {
51
- static override description = "Retry failed auto-claude issues by swapping labels";
52
-
53
- static override examples = [
54
- {
55
- description: "Interactively pick failed issues to retry",
56
- command: "<%= config.bin %> auto-claude retry",
57
- },
58
- {
59
- description: "Retry a specific issue",
60
- command: "<%= config.bin %> auto-claude retry --issue 42",
61
- },
62
- {
63
- description: "Retry and clean local artifacts",
64
- command: "<%= config.bin %> auto-claude retry --issue 42 --clean",
65
- },
66
- ];
67
-
68
- static override flags = {
69
- ...BaseCommand.baseFlags,
70
- issue: Flags.integer({
71
- char: "i",
50
+ export default defineCommand({
51
+ meta: { name: "retry", description: "Retry failed auto-claude issues by swapping labels" },
52
+ args: {
53
+ debug: debugArg,
54
+ issue: {
55
+ type: "string" as const,
56
+ alias: "i",
72
57
  description: "Issue number to retry",
73
- }),
74
- clean: Flags.boolean({
58
+ },
59
+ clean: {
60
+ type: "boolean" as const,
75
61
  description: "Delete local .auto-claude/issue-{N}/ artifacts",
76
62
  default: false,
77
- }),
78
- };
79
-
80
- async run(): Promise<void> {
81
- const { flags } = await this.parse(AutoClaudeRetry);
63
+ },
64
+ },
65
+ async run({ args }) {
66
+ const issueNumber = args.issue ? Number(args.issue) : undefined;
82
67
 
83
68
  const cfg = await initConfig();
84
69
 
85
70
  const cliInstalled = await isGithubCliInstalled();
86
71
  if (!cliInstalled) {
87
- this.error("GitHub CLI (gh) is not installed");
72
+ consola.error("GitHub CLI (gh) is not installed");
73
+ process.exit(1);
88
74
  }
89
75
 
90
76
  const failedIssues = await getIssues({ cwd: process.cwd(), label: LABELS.failed });
91
77
 
92
78
  let selected: Issue[];
93
79
 
94
- if (flags.issue) {
95
- const match = failedIssues.find((i) => i.number === flags.issue);
80
+ if (issueNumber) {
81
+ const match = failedIssues.find((i) => i.number === issueNumber);
96
82
  if (!match) {
97
- this.error(`Issue #${flags.issue} not found with '${LABELS.failed}' label`);
83
+ consola.error(`Issue #${issueNumber} not found with '${LABELS.failed}' label`);
84
+ process.exit(1);
98
85
  }
99
86
  selected = [match];
100
87
  } else {
@@ -122,7 +109,7 @@ export default class AutoClaudeRetry extends BaseCommand {
122
109
  {
123
110
  onCancel: () => {
124
111
  consola.info(colors.dim("Canceled"));
125
- this.exit(0);
112
+ process.exit(0);
126
113
  },
127
114
  },
128
115
  );
@@ -135,7 +122,7 @@ export default class AutoClaudeRetry extends BaseCommand {
135
122
  selected = failedIssues.filter((i) => result.selected.includes(i.number));
136
123
  }
137
124
 
138
- const count = await retryIssues(cfg.repo, cfg.triggerLabel, selected, flags.clean);
125
+ const count = await retryIssues(cfg.repo, cfg.triggerLabel, selected, args.clean as boolean);
139
126
  consola.box(`Retried ${count} issue(s)`);
140
- }
141
- }
127
+ },
128
+ });
@@ -1,6 +1,7 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
 
4
+ import { defineCommand } from "citty";
4
5
  import consola from "consola";
5
6
  import { colors } from "consola/utils";
6
7
 
@@ -8,7 +9,7 @@ import type { Issue } from "../../utils/git/gh-cli-wrapper.js";
8
9
  import { getIssues, isGithubCliInstalled } from "../../utils/git/gh-cli-wrapper.js";
9
10
  import { ARTIFACTS } from "../../lib/auto-claude/prompt-templates/index.js";
10
11
  import { LABELS } from "../../lib/auto-claude/labels.js";
11
- import { BaseCommand } from "../base.js";
12
+ import { debugArg } from "../shared.js";
12
13
 
13
14
  /** All labels that indicate an issue is part of the auto-claude pipeline. */
14
15
  const ALL_AC_LABELS = ["auto-claude", ...Object.values(LABELS)] as const;
@@ -86,22 +87,14 @@ export async function fetchAllAcIssues(cwd: string): Promise<Issue[]> {
86
87
  return [...issueMap.values()].sort((a, b) => a.number - b.number);
87
88
  }
88
89
 
89
- export default class AutoClaudeStatus extends BaseCommand {
90
- static override description = "Show pipeline status for auto-claude issues";
91
-
92
- static override aliases = ["ac:status"];
93
-
94
- static override examples = [
95
- {
96
- description: "Show status of all auto-claude issues",
97
- command: "<%= config.bin %> auto-claude status",
98
- },
99
- ];
100
-
101
- async run(): Promise<void> {
90
+ export default defineCommand({
91
+ meta: { name: "status", description: "Show pipeline status for auto-claude issues" },
92
+ args: { debug: debugArg },
93
+ async run() {
102
94
  const cliInstalled = await isGithubCliInstalled();
103
95
  if (!cliInstalled) {
104
- this.error("GitHub CLI (gh) is not installed");
96
+ consola.error("GitHub CLI (gh) is not installed");
97
+ process.exit(1);
105
98
  }
106
99
 
107
100
  const cwd = process.cwd();
@@ -119,5 +112,5 @@ export default class AutoClaudeStatus extends BaseCommand {
119
112
  const artifacts = checkArtifacts(issue.number, cwd);
120
113
  consola.log(formatIssueStatus(issue, artifacts));
121
114
  }
122
- }
123
- }
115
+ },
116
+ });
@@ -1,15 +1,9 @@
1
- /**
2
- * Integration tests for oclif config command
3
- * Note: consola outputs to stderr with different log levels
4
- */
5
1
  import { describe, it, expect } from "vitest";
6
- import { runCommand } from "@oclif/test";
2
+ import { runCommand } from "citty";
7
3
 
8
4
  describe("config command", () => {
9
- it("runs config and outputs settings info", async () => {
10
- const { stderr } = await runCommand(["config"]);
11
- // consola.warn outputs captured in stderr
12
- expect(stderr).toContain("User Config");
13
- expect(stderr).toContain("Working Directory");
5
+ it("runs config without throwing", async () => {
6
+ const { default: configCmd } = await import("./config.js");
7
+ await expect(runCommand(configCmd, { rawArgs: [] })).resolves.toBeDefined();
14
8
  });
15
9
  });
@@ -1,42 +1,28 @@
1
+ import { defineCommand } from "citty";
1
2
  import consola from "consola";
2
- import { BaseCommand } from "./base.js";
3
+ import { withSettings, debugArg } from "./shared.js";
3
4
 
4
- /**
5
- * Display current configuration settings
6
- */
7
- export default class Config extends BaseCommand {
8
- static override description = "Display current configuration settings";
9
-
10
- static override examples = [
11
- { description: "Display configuration", command: "<%= config.bin %> <%= command.id %>" },
12
- { description: "Use alias", command: "<%= config.bin %> cfg" },
13
- ];
14
-
15
- async run(): Promise<void> {
16
- await this.parse(Config);
5
+ export default defineCommand({
6
+ meta: { name: "config", description: "Display current configuration settings" },
7
+ args: { debug: debugArg },
8
+ async run({ args }) {
9
+ const { settingsFile, settings } = await withSettings(args.debug);
17
10
 
18
11
  consola.info("Configuration");
19
12
  consola.log("");
20
13
 
21
- consola.info(`Settings File: ${this.settingsFile.path}`);
14
+ consola.info(`Settings File: ${settingsFile.path}`);
22
15
  consola.log("");
23
16
 
24
17
  consola.warn("User Config:");
25
- consola.log(` Daily Path Template: ${this.userSettings.journalSettings.dailyPathTemplate}`);
26
- consola.log(
27
- ` Meeting Path Template: ${this.userSettings.journalSettings.meetingPathTemplate}`,
28
- );
29
- consola.log(` Note Path Template: ${this.userSettings.journalSettings.notePathTemplate}`);
30
- consola.log(` Editor: ${this.userSettings.preferredEditor}`);
18
+ consola.log(` Daily Path Template: ${settings.journalSettings.dailyPathTemplate}`);
19
+ consola.log(` Meeting Path Template: ${settings.journalSettings.meetingPathTemplate}`);
20
+ consola.log(` Note Path Template: ${settings.journalSettings.notePathTemplate}`);
21
+ consola.log(` Editor: ${settings.preferredEditor}`);
31
22
  consola.log("");
32
23
 
33
24
  consola.warn("Working Directory:");
34
25
  consola.log(` ${process.cwd()}`);
35
26
  consola.log("");
36
-
37
- consola.info("Shell Completions:");
38
- consola.log(" Run `tt completion` to generate shell completions");
39
- consola.log(" Bash/Zsh: tt completion >> ~/.bashrc (or ~/.zshrc)");
40
- consola.log(" Fish: tt completion > ~/.config/fish/completions/tt.fish");
41
- }
42
- }
27
+ },
28
+ });