@wrongstack/tools 0.1.1 → 0.1.3

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.
Files changed (103) hide show
  1. package/dist/audit.d.ts +25 -0
  2. package/dist/audit.js +209 -0
  3. package/dist/audit.js.map +1 -0
  4. package/dist/bash.d.ts +16 -0
  5. package/dist/bash.js +180 -0
  6. package/dist/bash.js.map +1 -0
  7. package/dist/batch-tool-use.d.ts +26 -0
  8. package/dist/batch-tool-use.js +106 -0
  9. package/dist/batch-tool-use.js.map +1 -0
  10. package/dist/builtin.d.ts +5 -0
  11. package/dist/builtin.js +3735 -0
  12. package/dist/builtin.js.map +1 -0
  13. package/dist/diff.d.ts +20 -0
  14. package/dist/diff.js +142 -0
  15. package/dist/diff.js.map +1 -0
  16. package/dist/document.d.ts +27 -0
  17. package/dist/document.js +148 -0
  18. package/dist/document.js.map +1 -0
  19. package/dist/edit.d.ts +22 -0
  20. package/dist/edit.js +138 -0
  21. package/dist/edit.js.map +1 -0
  22. package/dist/exec.d.ts +21 -0
  23. package/dist/exec.js +159 -0
  24. package/dist/exec.js.map +1 -0
  25. package/dist/fetch.d.ts +15 -0
  26. package/dist/fetch.js +213 -0
  27. package/dist/fetch.js.map +1 -0
  28. package/dist/format.d.ts +18 -0
  29. package/dist/format.js +194 -0
  30. package/dist/format.js.map +1 -0
  31. package/dist/git.d.ts +27 -0
  32. package/dist/git.js +174 -0
  33. package/dist/git.js.map +1 -0
  34. package/dist/glob.d.ts +14 -0
  35. package/dist/glob.js +101 -0
  36. package/dist/glob.js.map +1 -0
  37. package/dist/grep.d.ts +20 -0
  38. package/dist/grep.js +264 -0
  39. package/dist/grep.js.map +1 -0
  40. package/dist/index.d.ts +34 -563
  41. package/dist/index.js +717 -442
  42. package/dist/index.js.map +1 -1
  43. package/dist/install.d.ts +19 -0
  44. package/dist/install.js +186 -0
  45. package/dist/install.js.map +1 -0
  46. package/dist/json.d.ts +20 -0
  47. package/dist/json.js +124 -0
  48. package/dist/json.js.map +1 -0
  49. package/dist/lint.d.ts +20 -0
  50. package/dist/lint.js +191 -0
  51. package/dist/lint.js.map +1 -0
  52. package/dist/logs.d.ts +27 -0
  53. package/dist/logs.js +180 -0
  54. package/dist/logs.js.map +1 -0
  55. package/dist/memory.d.ts +22 -0
  56. package/dist/memory.js +53 -0
  57. package/dist/memory.js.map +1 -0
  58. package/dist/mode.d.ts +20 -0
  59. package/dist/mode.js +81 -0
  60. package/dist/mode.js.map +1 -0
  61. package/dist/outdated.d.ts +26 -0
  62. package/dist/outdated.js +138 -0
  63. package/dist/outdated.js.map +1 -0
  64. package/dist/patch.d.ts +18 -0
  65. package/dist/patch.js +101 -0
  66. package/dist/patch.js.map +1 -0
  67. package/dist/read.d.ts +16 -0
  68. package/dist/read.js +81 -0
  69. package/dist/read.js.map +1 -0
  70. package/dist/replace.d.ts +23 -0
  71. package/dist/replace.js +196 -0
  72. package/dist/replace.js.map +1 -0
  73. package/dist/scaffold.d.ts +20 -0
  74. package/dist/scaffold.js +185 -0
  75. package/dist/scaffold.js.map +1 -0
  76. package/dist/search.d.ts +20 -0
  77. package/dist/search.js +212 -0
  78. package/dist/search.js.map +1 -0
  79. package/dist/test.d.ts +24 -0
  80. package/dist/test.js +247 -0
  81. package/dist/test.js.map +1 -0
  82. package/dist/todo.d.ts +12 -0
  83. package/dist/todo.js +53 -0
  84. package/dist/todo.js.map +1 -0
  85. package/dist/tool-help.d.ts +23 -0
  86. package/dist/tool-help.js +122 -0
  87. package/dist/tool-help.js.map +1 -0
  88. package/dist/tool-search.d.ts +22 -0
  89. package/dist/tool-search.js +70 -0
  90. package/dist/tool-search.js.map +1 -0
  91. package/dist/tool-use.d.ts +16 -0
  92. package/dist/tool-use.js +79 -0
  93. package/dist/tool-use.js.map +1 -0
  94. package/dist/tree.d.ts +21 -0
  95. package/dist/tree.js +176 -0
  96. package/dist/tree.js.map +1 -0
  97. package/dist/typecheck.d.ts +19 -0
  98. package/dist/typecheck.js +181 -0
  99. package/dist/typecheck.js.map +1 -0
  100. package/dist/write.d.ts +15 -0
  101. package/dist/write.js +77 -0
  102. package/dist/write.js.map +1 -0
  103. package/package.json +137 -4
@@ -0,0 +1,185 @@
1
+ import * as path from 'path';
2
+ import * as fsSync from 'fs';
3
+ import 'child_process';
4
+
5
+ // src/scaffold.ts
6
+ function resolvePath(input, ctx) {
7
+ return path.isAbsolute(input) ? path.normalize(input) : path.resolve(ctx.cwd, input);
8
+ }
9
+ function ensureInsideRoot(absPath, ctx) {
10
+ const root = path.resolve(ctx.projectRoot);
11
+ const target = path.resolve(absPath);
12
+ const rel = path.relative(root, target);
13
+ if (rel.startsWith("..") || path.isAbsolute(rel)) {
14
+ throw new Error(`Path "${absPath}" is outside project root "${root}"`);
15
+ }
16
+ return target;
17
+ }
18
+ function safeResolve(input, ctx) {
19
+ return ensureInsideRoot(resolvePath(input, ctx), ctx);
20
+ }
21
+
22
+ // src/scaffold.ts
23
+ var BUILT_IN_TEMPLATES = {
24
+ "npm-package": {
25
+ description: "Basic npm package with ESM",
26
+ files: {
27
+ "package.json": JSON.stringify({
28
+ name: "{{name}}",
29
+ version: "0.1.1",
30
+ type: "module",
31
+ main: "./dist/index.js",
32
+ scripts: { build: "tsc", test: "vitest run" },
33
+ devDependencies: { typescript: "^5.0.0" }
34
+ }, null, 2),
35
+ "tsconfig.json": JSON.stringify({
36
+ compilerOptions: { target: "ES2022", module: "ESNext", strict: true },
37
+ include: ["src"]
38
+ }, null, 2),
39
+ "src/index.ts": `export function hello() {
40
+ return 'Hello from {{name}}';
41
+ }
42
+ `,
43
+ "src/index.test.ts": `import { hello } from './index';
44
+ import { describe, it, expect } from 'vitest';
45
+
46
+ describe('hello', () => {
47
+ it('returns greeting', () => {
48
+ expect(hello()).toBe('Hello from {{name}}');
49
+ });
50
+ });
51
+ `
52
+ }
53
+ },
54
+ "cli-tool": {
55
+ description: "CLI tool with argparse",
56
+ files: {
57
+ "package.json": JSON.stringify({
58
+ name: "{{name}}",
59
+ version: "0.1.1",
60
+ type: "module",
61
+ bin: { "{{name}}": "./src/index.js" },
62
+ scripts: { build: "tsc", start: "node dist/index.js" }
63
+ }, null, 2),
64
+ "src/index.ts": `#!/usr/bin/env node
65
+
66
+ async function main() {
67
+ console.log('Hello from {{name}}');
68
+ }
69
+
70
+ main();
71
+ `
72
+ }
73
+ },
74
+ "react-component": {
75
+ description: "React component with TypeScript",
76
+ files: {
77
+ "{{name}}.tsx": `interface {{Name}}Props {
78
+ className?: string;
79
+ }
80
+
81
+ export function {{Name}}({ className }: {{Name}}Props) {
82
+ return (
83
+ <div className={className}>
84
+ {{Name}} Component
85
+ </div>
86
+ );
87
+ }
88
+ `,
89
+ "{{name}}.test.tsx": `import { render, screen } from '@testing-library/react';
90
+ import { {{Name}} } from './{{Name}}';
91
+
92
+ describe('{{Name}}', () => {
93
+ it('renders', () => {
94
+ render(<{{Name}} />);
95
+ expect(screen.getByText('{{Name}} Component')).toBeInTheDocument();
96
+ });
97
+ });
98
+ `
99
+ }
100
+ }
101
+ };
102
+ var scaffoldTool = {
103
+ name: "scaffold",
104
+ description: "Generate boilerplate code from built-in templates or paths. Creates package.json, source files, tests.",
105
+ usageHint: "Set `template` (npm-package, cli-tool, react-component) and `name`. `vars` for template variables. `dry_run` preview.",
106
+ permission: "confirm",
107
+ mutating: true,
108
+ timeoutMs: 3e4,
109
+ inputSchema: {
110
+ type: "object",
111
+ properties: {
112
+ template: {
113
+ type: "string",
114
+ description: "Template name (npm-package, cli-tool, react-component) or path to template directory"
115
+ },
116
+ name: {
117
+ type: "string",
118
+ description: "Project/component name (used in generated files)"
119
+ },
120
+ cwd: { type: "string", description: "Working directory (default: cwd)" },
121
+ vars: {
122
+ type: "object",
123
+ additionalProperties: { type: "string" },
124
+ description: "Template variables for custom templates"
125
+ },
126
+ dry_run: {
127
+ type: "boolean",
128
+ description: "Preview generated files without creating (default: false)"
129
+ }
130
+ },
131
+ required: ["template", "name"]
132
+ },
133
+ async execute(input, ctx) {
134
+ const cwd = input.cwd ? safeResolve(input.cwd, ctx) : ctx.cwd;
135
+ const name = input.name;
136
+ const vars = { name, ...input.vars };
137
+ const builtIn = BUILT_IN_TEMPLATES[input.template];
138
+ if (builtIn) {
139
+ return handleBuiltIn(name, builtIn.files, cwd, input.dry_run ?? false, vars);
140
+ }
141
+ return {
142
+ template: input.template,
143
+ name,
144
+ files_created: 0,
145
+ files: [],
146
+ dry_run: input.dry_run ?? false,
147
+ output: `Template "${input.template}" not found. Available: ${Object.keys(BUILT_IN_TEMPLATES).join(", ")}`
148
+ };
149
+ }
150
+ };
151
+ function handleBuiltIn(name, templateFiles, cwd, dryRun, vars) {
152
+ const files = [];
153
+ let filesCreated = 0;
154
+ for (const [filePath, content] of Object.entries(templateFiles)) {
155
+ const resolvedPath = substituteVars(filePath, name, vars);
156
+ const fullPath = path.join(cwd, resolvedPath);
157
+ if (!dryRun) {
158
+ fsSync.mkdirSync(path.dirname(fullPath), { recursive: true });
159
+ fsSync.writeFileSync(fullPath, substituteVars(content, name, vars), "utf8");
160
+ }
161
+ files.push(resolvedPath);
162
+ filesCreated++;
163
+ }
164
+ return {
165
+ template: "built-in",
166
+ name,
167
+ files_created: filesCreated,
168
+ files,
169
+ dry_run: dryRun,
170
+ output: dryRun ? `Would create ${filesCreated} files: ${files.join(", ")}` : `Created ${filesCreated} files: ${files.join(", ")}`
171
+ };
172
+ }
173
+ function substituteVars(content, name, vars) {
174
+ let result = content;
175
+ result = result.replace(/\{\{name\}\}/g, name.toLowerCase().replace(/\s+/g, "-"));
176
+ result = result.replace(/\{\{Name\}\}/g, name.replace(/(?:^|[-_\s]+)([a-z])/g, (_, c) => c.toUpperCase()));
177
+ for (const [k, v] of Object.entries(vars)) {
178
+ result = result.replace(new RegExp(`\\{\\{${k}\\}\\}`, "g"), v);
179
+ }
180
+ return result;
181
+ }
182
+
183
+ export { scaffoldTool };
184
+ //# sourceMappingURL=scaffold.js.map
185
+ //# sourceMappingURL=scaffold.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/_util.ts","../src/scaffold.ts"],"names":["path2"],"mappings":"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","file":"scaffold.js","sourcesContent":["import * as path from 'node:path';\r\nimport { spawn } from 'node:child_process';\r\nimport type { Context, ToolProgressEvent } from '@wrongstack/core';\r\n\r\nexport function resolvePath(input: string, ctx: Context): string {\r\n return path.isAbsolute(input) ? path.normalize(input) : path.resolve(ctx.cwd, input);\r\n}\r\n\r\nexport function ensureInsideRoot(absPath: string, ctx: Context): string {\r\n const root = path.resolve(ctx.projectRoot);\r\n const target = path.resolve(absPath);\r\n const rel = path.relative(root, target);\r\n if (rel.startsWith('..') || path.isAbsolute(rel)) {\r\n throw new Error(`Path \"${absPath}\" is outside project root \"${root}\"`);\r\n }\r\n return target;\r\n}\r\n\r\nexport function safeResolve(input: string, ctx: Context): string {\r\n return ensureInsideRoot(resolvePath(input, ctx), ctx);\r\n}\r\n\r\nexport function truncateMiddle(s: string, max: number): string {\r\n if (Buffer.byteLength(s, 'utf8') <= max) return s;\r\n const half = Math.floor(max / 2);\r\n return (\r\n s.slice(0, half) +\r\n `\\n…[truncated ${Buffer.byteLength(s, 'utf8') - max} bytes from middle]…\\n` +\r\n s.slice(-half)\r\n );\r\n}\r\n\r\nexport function isBinaryBuffer(buf: Buffer): boolean {\r\n const len = Math.min(buf.length, 8192);\r\n for (let i = 0; i < len; i++) {\r\n if (buf[i] === 0) return true;\r\n }\r\n return false;\r\n}\r\n\r\nexport interface SpawnStreamResult {\r\n stdout: string;\r\n stderr: string;\r\n exitCode: number;\r\n truncated: boolean;\r\n error?: string;\r\n}\r\n\r\nexport interface SpawnStreamOptions {\r\n cmd: string;\r\n args: string[];\r\n cwd: string;\r\n signal: AbortSignal;\r\n maxBytes?: number;\r\n /** Bytes of new stdout/stderr to accumulate before yielding a `partial_output` event. */\r\n flushBytes?: number;\r\n}\r\n\r\n/**\r\n * Spawn a child process and yield `partial_output` progress events as\r\n * stdout/stderr arrive (batched by byte threshold), then return the full\r\n * buffered result. Shared between install/lint/format/typecheck/test/audit\r\n * so the TUI live tail sees consistent progress regardless of which tool\r\n * is running.\r\n */\r\nexport async function* spawnStream(\r\n opts: SpawnStreamOptions,\r\n): AsyncGenerator<ToolProgressEvent, SpawnStreamResult> {\r\n const max = opts.maxBytes ?? 200_000;\r\n const flushAt = opts.flushBytes ?? 4 * 1024;\r\n let stdout = '';\r\n let stderr = '';\r\n let pending = '';\r\n let error: string | undefined;\r\n\r\n const child = spawn(opts.cmd, opts.args, {\r\n cwd: opts.cwd,\r\n signal: opts.signal,\r\n stdio: ['ignore', 'pipe', 'pipe'],\r\n });\r\n\r\n type Chunk = { kind: 'out' | 'err' | 'close' | 'error'; data: string; code?: number };\r\n const queue: Chunk[] = [];\r\n let waiter: (() => void) | undefined;\r\n const wake = () => {\r\n if (waiter) {\r\n const w = waiter;\r\n waiter = undefined;\r\n w();\r\n }\r\n };\r\n\r\n child.stdout?.on('data', (c) => {\r\n const s = c.toString();\r\n if (stdout.length < max) stdout += s;\r\n queue.push({ kind: 'out', data: s });\r\n wake();\r\n });\r\n child.stderr?.on('data', (c) => {\r\n const s = c.toString();\r\n if (stderr.length < max) stderr += s;\r\n queue.push({ kind: 'err', data: s });\r\n wake();\r\n });\r\n child.on('error', (e) => {\r\n error = e.message;\r\n queue.push({ kind: 'error', data: e.message });\r\n wake();\r\n });\r\n child.on('close', (code) => {\r\n queue.push({ kind: 'close', data: '', code: code ?? 0 });\r\n wake();\r\n });\r\n\r\n let exitCode = 0;\r\n let spawnFailed = false;\r\n for (;;) {\r\n while (queue.length === 0) {\r\n await new Promise<void>((resolve) => {\r\n waiter = resolve;\r\n });\r\n }\r\n const chunk = queue.shift()!;\r\n if (chunk.kind === 'close') {\r\n // If we already saw a spawn error (ENOENT etc.), keep exitCode=1\r\n // rather than the negative platform code Node fabricates.\r\n if (!spawnFailed) exitCode = chunk.code ?? 0;\r\n break;\r\n }\r\n if (chunk.kind === 'error') {\r\n spawnFailed = true;\r\n exitCode = 1;\r\n // close usually follows\r\n continue;\r\n }\r\n pending += chunk.data;\r\n if (pending.length >= flushAt) {\r\n yield { type: 'partial_output', text: pending };\r\n pending = '';\r\n }\r\n }\r\n if (pending.length > 0) {\r\n yield { type: 'partial_output', text: pending };\r\n }\r\n\r\n return {\r\n stdout,\r\n stderr,\r\n exitCode,\r\n truncated: stdout.length >= max || stderr.length >= max,\r\n error,\r\n };\r\n}\r\n","import * as fs from 'node:fs/promises';\nimport * as path from 'node:path';\nimport * as fsSync from 'node:fs';\nimport type { Tool } from '@wrongstack/core';\nimport { safeResolve } from './_util.js';\n\ninterface ScaffoldInput {\n template: string;\n name: string;\n cwd?: string;\n vars?: Record<string, string>;\n dry_run?: boolean;\n}\n\ninterface ScaffoldOutput {\n template: string;\n name: string;\n files_created: number;\n files: string[];\n dry_run: boolean;\n output: string;\n}\n\nconst BUILT_IN_TEMPLATES: Record<string, { description: string; files: Record<string, string> }> = {\n 'npm-package': {\n description: 'Basic npm package with ESM',\n files: {\n 'package.json': JSON.stringify({\n name: '{{name}}',\n version: '0.1.1',\n type: 'module',\n main: './dist/index.js',\n scripts: { build: 'tsc', test: 'vitest run' },\n devDependencies: { typescript: '^5.0.0' },\n }, null, 2),\n 'tsconfig.json': JSON.stringify({\n compilerOptions: { target: 'ES2022', module: 'ESNext', strict: true },\n include: ['src'],\n }, null, 2),\n 'src/index.ts': `export function hello() {\\n return 'Hello from {{name}}';\\n}\\n`,\n 'src/index.test.ts': `import { hello } from './index';\\nimport { describe, it, expect } from 'vitest';\\n\\ndescribe('hello', () => {\\n it('returns greeting', () => {\\n expect(hello()).toBe('Hello from {{name}}');\\n });\\n});\\n`,\n },\n },\n 'cli-tool': {\n description: 'CLI tool with argparse',\n files: {\n 'package.json': JSON.stringify({\n name: '{{name}}',\n version: '0.1.1',\n type: 'module',\n bin: { '{{name}}': './src/index.js' },\n scripts: { build: 'tsc', start: 'node dist/index.js' },\n }, null, 2),\n 'src/index.ts': `#!/usr/bin/env node\\n\\nasync function main() {\\n console.log('Hello from {{name}}');\\n}\\n\\nmain();\\n`,\n },\n },\n 'react-component': {\n description: 'React component with TypeScript',\n files: {\n '{{name}}.tsx': `interface {{Name}}Props {\\n className?: string;\\n}\\n\\nexport function {{Name}}({ className }: {{Name}}Props) {\\n return (\\n <div className={className}>\\n {{Name}} Component\\n </div>\\n );\\n}\\n`,\n '{{name}}.test.tsx': `import { render, screen } from '@testing-library/react';\\nimport { {{Name}} } from './{{Name}}';\\n\\ndescribe('{{Name}}', () => {\\n it('renders', () => {\\n render(<{{Name}} />);\\n expect(screen.getByText('{{Name}} Component')).toBeInTheDocument();\\n });\\n});\\n`,\n },\n },\n};\n\nexport const scaffoldTool: Tool<ScaffoldInput, ScaffoldOutput> = {\n name: 'scaffold',\n description:\n 'Generate boilerplate code from built-in templates or paths. Creates package.json, source files, tests.',\n usageHint:\n 'Set `template` (npm-package, cli-tool, react-component) and `name`. `vars` for template variables. `dry_run` preview.',\n permission: 'confirm',\n mutating: true,\n timeoutMs: 30_000,\n inputSchema: {\n type: 'object',\n properties: {\n template: {\n type: 'string',\n description: 'Template name (npm-package, cli-tool, react-component) or path to template directory',\n },\n name: {\n type: 'string',\n description: 'Project/component name (used in generated files)',\n },\n cwd: { type: 'string', description: 'Working directory (default: cwd)' },\n vars: {\n type: 'object',\n additionalProperties: { type: 'string' },\n description: 'Template variables for custom templates',\n },\n dry_run: {\n type: 'boolean',\n description: 'Preview generated files without creating (default: false)',\n },\n },\n required: ['template', 'name'],\n },\n async execute(input, ctx) {\n const cwd = input.cwd ? safeResolve(input.cwd, ctx) : ctx.cwd;\n const name = input.name;\n const vars = { name, ...input.vars };\n\n const builtIn = BUILT_IN_TEMPLATES[input.template];\n if (builtIn) {\n return handleBuiltIn(name, builtIn.files, cwd, input.dry_run ?? false, vars);\n }\n\n return {\n template: input.template,\n name,\n files_created: 0,\n files: [],\n dry_run: input.dry_run ?? false,\n output: `Template \"${input.template}\" not found. Available: ${Object.keys(BUILT_IN_TEMPLATES).join(', ')}`,\n };\n },\n};\n\nfunction handleBuiltIn(\n name: string,\n templateFiles: Record<string, string>,\n cwd: string,\n dryRun: boolean,\n vars: Record<string, string>,\n): ScaffoldOutput {\n const files: string[] = [];\n let filesCreated = 0;\n\n for (const [filePath, content] of Object.entries(templateFiles)) {\n const resolvedPath = substituteVars(filePath, name, vars);\n const fullPath = path.join(cwd, resolvedPath);\n\n if (!dryRun) {\n fsSync.mkdirSync(path.dirname(fullPath), { recursive: true });\n fsSync.writeFileSync(fullPath, substituteVars(content, name, vars), 'utf8');\n }\n files.push(resolvedPath);\n filesCreated++;\n }\n\n return {\n template: 'built-in',\n name,\n files_created: filesCreated,\n files,\n dry_run: dryRun,\n output: dryRun\n ? `Would create ${filesCreated} files: ${files.join(', ')}`\n : `Created ${filesCreated} files: ${files.join(', ')}`,\n };\n}\n\nfunction substituteVars(content: string, name: string, vars: Record<string, string>): string {\n let result = content;\n result = result.replace(/\\{\\{name\\}\\}/g, name.toLowerCase().replace(/\\s+/g, '-'));\n result = result.replace(/\\{\\{Name\\}\\}/g, name.replace(/(?:^|[-_\\s]+)([a-z])/g, (_, c) => c.toUpperCase()));\n for (const [k, v] of Object.entries(vars)) {\n result = result.replace(new RegExp(`\\\\{\\\\{${k}\\\\}\\\\}`, 'g'), v);\n }\n return result;\n}"]}
@@ -0,0 +1,20 @@
1
+ import { Tool } from '@wrongstack/core';
2
+
3
+ interface SearchInput {
4
+ query: string;
5
+ num_results?: number;
6
+ source?: 'duckduckgo' | 'google' | 'bing';
7
+ }
8
+ interface SearchOutput {
9
+ query: string;
10
+ results: {
11
+ title: string;
12
+ url: string;
13
+ snippet: string;
14
+ }[];
15
+ source: string;
16
+ truncated: boolean;
17
+ }
18
+ declare const searchTool: Tool<SearchInput, SearchOutput>;
19
+
20
+ export { searchTool };
package/dist/search.js ADDED
@@ -0,0 +1,212 @@
1
+ // src/search.ts
2
+ var DEFAULT_NUM = 10;
3
+ var MAX_RESULTS = 50;
4
+ var TIMEOUT_MS = 15e3;
5
+ var searchTool = {
6
+ name: "search",
7
+ description: "Search the web for information. Returns title, URL, and snippet for each result.",
8
+ usageHint: "Set `num_results` (1-50, default 10). Use `source` to pick engine: duckduckgo (default), google, bing.",
9
+ permission: "confirm",
10
+ mutating: false,
11
+ timeoutMs: TIMEOUT_MS,
12
+ inputSchema: {
13
+ type: "object",
14
+ properties: {
15
+ query: { type: "string", description: "Search query" },
16
+ num_results: {
17
+ type: "integer",
18
+ description: "Number of results (1-50, default 10)",
19
+ minimum: 1,
20
+ maximum: MAX_RESULTS
21
+ },
22
+ source: {
23
+ type: "string",
24
+ enum: ["duckduckgo", "google", "bing"],
25
+ description: "Search engine to use (default: duckduckgo)"
26
+ }
27
+ },
28
+ required: ["query"]
29
+ },
30
+ async execute(input, ctx, opts) {
31
+ let final;
32
+ for await (const ev of searchTool.executeStream(input, ctx, opts)) {
33
+ if (ev.type === "final") final = ev.output;
34
+ }
35
+ if (!final) throw new Error("search: stream ended without final event");
36
+ return final;
37
+ },
38
+ async *executeStream(input, _ctx, opts) {
39
+ if (!input?.query) throw new Error("search: query is required");
40
+ const num = Math.max(1, Math.min(input.num_results ?? DEFAULT_NUM, MAX_RESULTS));
41
+ const source = input.source ?? "duckduckgo";
42
+ yield { type: "log", text: `Querying ${source} for "${input.query}"\u2026`, data: { source, query: input.query } };
43
+ let output;
44
+ switch (source) {
45
+ case "duckduckgo":
46
+ output = await duckduckgoSearch(input.query, num, opts.signal);
47
+ break;
48
+ case "google":
49
+ output = await googleSearch(input.query, num, opts.signal);
50
+ break;
51
+ case "bing":
52
+ output = await bingSearch(input.query, num, opts.signal);
53
+ break;
54
+ default:
55
+ throw new Error(`search: unknown source "${source}"`);
56
+ }
57
+ yield {
58
+ type: "partial_output",
59
+ text: `${output.results.length} results from ${output.source}`,
60
+ data: { count: output.results.length }
61
+ };
62
+ yield { type: "final", output };
63
+ }
64
+ };
65
+ async function duckduckgoSearch(query, num, signal) {
66
+ const encoded = encodeURIComponent(query);
67
+ const url = `https://lite.duckduckgo.com/lite/?q=${encoded}&kd=-1&kl=wt-wt`;
68
+ const results = await fetchWithTimeout(url, signal, TIMEOUT_MS).then((r) => r.text()).then((html) => parseDuckDuckGo(html, num)).catch(() => [{ title: "Search unavailable", url: "", snippet: "Could not reach DuckDuckGo" }]);
69
+ return {
70
+ query,
71
+ results,
72
+ source: "duckduckgo",
73
+ truncated: results.length >= num
74
+ };
75
+ }
76
+ function takeFrom(iter, max) {
77
+ const out = [];
78
+ for (const item of iter) {
79
+ if (out.length >= max) break;
80
+ out.push(item);
81
+ }
82
+ return out;
83
+ }
84
+ function parseDuckDuckGo(html, num) {
85
+ const results = [];
86
+ const snippetRegex = /<a class="result-link"[^>]+href="([^"]+)"[^>]*>([^<]+)<\/a>/gi;
87
+ const snippet2Regex = /<a class="result-snippet"[^>]*>([^<]+)<\/a>/gi;
88
+ const linkMatches = takeFrom(
89
+ [...html.matchAll(snippetRegex)].filter((m) => m[1] && m[2]).map((m) => ({ url: m[1], title: stripTags(m[2]) })),
90
+ num
91
+ );
92
+ const snippetMatches = takeFrom(
93
+ [...html.matchAll(snippet2Regex)].filter((m) => m[1]).map((m) => stripTags(m[1])),
94
+ num
95
+ );
96
+ for (let i = 0; i < linkMatches.length && i < num; i++) {
97
+ const entry = linkMatches[i];
98
+ results.push({
99
+ title: entry?.title ?? "",
100
+ url: entry?.url ?? "",
101
+ snippet: snippetMatches[i] ?? ""
102
+ });
103
+ }
104
+ return results;
105
+ }
106
+ async function googleSearch(query, num, signal) {
107
+ const encoded = encodeURIComponent(query);
108
+ const url = `https://www.google.com/search?q=${encoded}&hl=en`;
109
+ const html = await fetchWithTimeout(url, signal, TIMEOUT_MS).then((r) => r.text()).catch(() => "");
110
+ const results = parseGoogleResults(html, num);
111
+ return {
112
+ query,
113
+ results,
114
+ source: "google",
115
+ truncated: results.length >= num
116
+ };
117
+ }
118
+ function parseGoogleResults(html, num) {
119
+ const results = [];
120
+ const titleRegex = /<h3[^>]*class="[^"]*DKV84"[^>]*>([^<]+)<\/h3>/gi;
121
+ const urlRegex = /<cite[^>]*>([^<]+)<\/cite>/gi;
122
+ const snippetRegex = /<span[^>]*class="[^"]*aXCZ0b[^>]*>([^<]+)<\/span>/gi;
123
+ const titles = takeFrom(
124
+ [...html.matchAll(titleRegex)].filter((m) => m[1]).map((m) => stripTags(m[1])),
125
+ num
126
+ );
127
+ const urls = takeFrom(
128
+ [...html.matchAll(urlRegex)].filter((m) => m[1]).map((m) => stripTags(m[1]).replace(/^\*(https?:\/\/[^\s]+).*$/, "$1")).filter((u) => u.startsWith("http")),
129
+ num
130
+ );
131
+ const snippets = takeFrom(
132
+ [...html.matchAll(snippetRegex)].filter((m) => m[1]).map((m) => stripTags(m[1])),
133
+ num
134
+ );
135
+ for (let i = 0; i < Math.min(titles.length, num); i++) {
136
+ results.push({
137
+ title: titles[i] ?? "",
138
+ url: urls[i] ?? "",
139
+ snippet: snippets[i] ?? ""
140
+ });
141
+ }
142
+ return results;
143
+ }
144
+ async function bingSearch(query, num, signal) {
145
+ const encoded = encodeURIComponent(query);
146
+ const url = `https://www.bing.com/search?q=${encoded}`;
147
+ const html = await fetchWithTimeout(url, signal, TIMEOUT_MS).then((r) => r.text()).catch(() => "");
148
+ const results = parseBingResults(html, num);
149
+ return {
150
+ query,
151
+ results,
152
+ source: "bing",
153
+ truncated: results.length >= num
154
+ };
155
+ }
156
+ function parseBingResults(html, num) {
157
+ const results = [];
158
+ const titleRegex = /<h2[^>]*>\s*<a[^>]+href="([^"]+)"[^>]*>([^<]+)<\/a>\s*<\/h2>/gi;
159
+ const snippetRegex = /<p[^>]*class="[^"]*b_paractl[^"]*"[^>]*>([^<]+)<\/p>/gi;
160
+ const entries = takeFrom(
161
+ [...html.matchAll(titleRegex)].filter((m) => m[1] && m[2]).map((m) => ({ url: m[1], title: stripTags(m[2]) })),
162
+ num
163
+ );
164
+ const snippets = takeFrom(
165
+ [...html.matchAll(snippetRegex)].filter((m) => m[1]).map((m) => stripTags(m[1])),
166
+ num
167
+ );
168
+ for (let i = 0; i < entries.length; i++) {
169
+ results.push({
170
+ title: entries[i]?.title ?? "",
171
+ url: entries[i]?.url ?? "",
172
+ snippet: snippets[i] ?? ""
173
+ });
174
+ }
175
+ return results;
176
+ }
177
+ async function fetchWithTimeout(url, signal, timeoutMs) {
178
+ const controller = new AbortController();
179
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
180
+ const fetchSignal = anySignal(signal, controller.signal);
181
+ try {
182
+ const res = await fetch(url, {
183
+ headers: {
184
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
185
+ },
186
+ signal: fetchSignal
187
+ });
188
+ clearTimeout(timer);
189
+ return res;
190
+ } catch (e) {
191
+ clearTimeout(timer);
192
+ throw e;
193
+ }
194
+ }
195
+ function anySignal(...signals) {
196
+ const controller = new AbortController();
197
+ for (const s of signals) {
198
+ if (s.aborted) {
199
+ controller.abort();
200
+ break;
201
+ }
202
+ s.addEventListener("abort", () => controller.abort());
203
+ }
204
+ return controller.signal;
205
+ }
206
+ function stripTags(html) {
207
+ return html.replace(/<[^>]+>/g, "").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'").trim();
208
+ }
209
+
210
+ export { searchTool };
211
+ //# sourceMappingURL=search.js.map
212
+ //# sourceMappingURL=search.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/search.ts"],"names":[],"mappings":"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","file":"search.js","sourcesContent":["import type { Tool, ToolStreamEvent } from '@wrongstack/core';\r\n\r\ninterface SearchInput {\r\n query: string;\r\n num_results?: number;\r\n source?: 'duckduckgo' | 'google' | 'bing';\r\n}\r\n\r\ninterface SearchOutput {\r\n query: string;\r\n results: { title: string; url: string; snippet: string }[];\r\n source: string;\r\n truncated: boolean;\r\n}\r\n\r\nconst DEFAULT_NUM = 10;\r\nconst MAX_RESULTS = 50;\r\nconst TIMEOUT_MS = 15_000;\r\n\r\nexport const searchTool: Tool<SearchInput, SearchOutput> = {\r\n name: 'search',\r\n description:\r\n 'Search the web for information. Returns title, URL, and snippet for each result.',\r\n usageHint:\r\n 'Set `num_results` (1-50, default 10). Use `source` to pick engine: duckduckgo (default), google, bing.',\r\n permission: 'confirm',\r\n mutating: false,\r\n timeoutMs: TIMEOUT_MS,\r\n inputSchema: {\r\n type: 'object',\r\n properties: {\r\n query: { type: 'string', description: 'Search query' },\r\n num_results: {\r\n type: 'integer',\r\n description: 'Number of results (1-50, default 10)',\r\n minimum: 1,\r\n maximum: MAX_RESULTS,\r\n },\r\n source: {\r\n type: 'string',\r\n enum: ['duckduckgo', 'google', 'bing'],\r\n description: 'Search engine to use (default: duckduckgo)',\r\n },\r\n },\r\n required: ['query'],\r\n },\r\n async execute(input, ctx, opts) {\r\n let final: SearchOutput | undefined;\r\n for await (const ev of searchTool.executeStream!(input, ctx, opts)) {\r\n if (ev.type === 'final') final = ev.output;\r\n }\r\n if (!final) throw new Error('search: stream ended without final event');\r\n return final;\r\n },\r\n async *executeStream(input, _ctx, opts): AsyncGenerator<ToolStreamEvent<SearchOutput>> {\r\n if (!input?.query) throw new Error('search: query is required');\r\n\r\n const num = Math.max(1, Math.min(input.num_results ?? DEFAULT_NUM, MAX_RESULTS));\r\n const source = input.source ?? 'duckduckgo';\r\n\r\n yield { type: 'log', text: `Querying ${source} for \"${input.query}\"…`, data: { source, query: input.query } };\r\n\r\n let output: SearchOutput;\r\n switch (source) {\r\n case 'duckduckgo':\r\n output = await duckduckgoSearch(input.query, num, opts.signal);\r\n break;\r\n case 'google':\r\n output = await googleSearch(input.query, num, opts.signal);\r\n break;\r\n case 'bing':\r\n output = await bingSearch(input.query, num, opts.signal);\r\n break;\r\n default:\r\n throw new Error(`search: unknown source \"${source}\"`);\r\n }\r\n\r\n yield {\r\n type: 'partial_output',\r\n text: `${output.results.length} results from ${output.source}`,\r\n data: { count: output.results.length },\r\n };\r\n yield { type: 'final', output };\r\n },\r\n};\r\n\r\nasync function duckduckgoSearch(\r\n query: string,\r\n num: number,\r\n signal: AbortSignal,\r\n): Promise<SearchOutput> {\r\n const encoded = encodeURIComponent(query);\r\n const url = `https://lite.duckduckgo.com/lite/?q=${encoded}&kd=-1&kl=wt-wt`;\r\n\r\n const results = await fetchWithTimeout(url, signal, TIMEOUT_MS)\r\n .then((r) => r.text())\r\n .then((html) => parseDuckDuckGo(html, num))\r\n .catch(() => [{ title: 'Search unavailable', url: '', snippet: 'Could not reach DuckDuckGo' }]);\r\n\r\n return {\r\n query,\r\n results,\r\n source: 'duckduckgo',\r\n truncated: results.length >= num,\r\n };\r\n}\r\n\r\nfunction takeFrom<T>(iter: Iterable<T>, max: number): T[] {\r\n const out: T[] = [];\r\n for (const item of iter) {\r\n if (out.length >= max) break;\r\n out.push(item);\r\n }\r\n return out;\r\n}\r\n\r\nfunction parseDuckDuckGo(html: string, num: number): SearchOutput['results'] {\r\n const results: SearchOutput['results'] = [];\r\n const snippetRegex = /<a class=\"result-link\"[^>]+href=\"([^\"]+)\"[^>]*>([^<]+)<\\/a>/gi;\r\n const snippet2Regex = /<a class=\"result-snippet\"[^>]*>([^<]+)<\\/a>/gi;\r\n\r\n const linkMatches = takeFrom(\r\n [...html.matchAll(snippetRegex)]\r\n .filter((m) => m[1] && m[2])\r\n .map((m) => ({ url: m[1]!, title: stripTags(m[2]!) })),\r\n num,\r\n );\r\n\r\n const snippetMatches = takeFrom(\r\n [...html.matchAll(snippet2Regex)]\r\n .filter((m) => m[1])\r\n .map((m) => stripTags(m[1]!)),\r\n num,\r\n );\r\n\r\n for (let i = 0; i < linkMatches.length && i < num; i++) {\r\n const entry = linkMatches[i];\r\n results.push({\r\n title: entry?.title ?? '',\r\n url: entry?.url ?? '',\r\n snippet: snippetMatches[i] ?? '',\r\n });\r\n }\r\n\r\n return results;\r\n}\r\n\r\nasync function googleSearch(\r\n query: string,\r\n num: number,\r\n signal: AbortSignal,\r\n): Promise<SearchOutput> {\r\n const encoded = encodeURIComponent(query);\r\n const url = `https://www.google.com/search?q=${encoded}&hl=en`;\r\n\r\n const html = await fetchWithTimeout(url, signal, TIMEOUT_MS)\r\n .then((r) => r.text())\r\n .catch(() => '');\r\n\r\n const results = parseGoogleResults(html, num);\r\n\r\n return {\r\n query,\r\n results,\r\n source: 'google',\r\n truncated: results.length >= num,\r\n };\r\n}\r\n\r\nfunction parseGoogleResults(html: string, num: number): SearchOutput['results'] {\r\n const results: SearchOutput['results'] = [];\r\n const titleRegex = /<h3[^>]*class=\"[^\"]*DKV84\"[^>]*>([^<]+)<\\/h3>/gi;\r\n const urlRegex = /<cite[^>]*>([^<]+)<\\/cite>/gi;\r\n const snippetRegex = /<span[^>]*class=\"[^\"]*aXCZ0b[^>]*>([^<]+)<\\/span>/gi;\r\n\r\n const titles = takeFrom(\r\n [...html.matchAll(titleRegex)].filter((m) => m[1]).map((m) => stripTags(m[1]!)),\r\n num,\r\n );\r\n\r\n const urls = takeFrom(\r\n [...html.matchAll(urlRegex)]\r\n .filter((m) => m[1])\r\n .map((m) => stripTags(m[1]!).replace(/^\\*(https?:\\/\\/[^\\s]+).*$/, '$1'))\r\n .filter((u) => u.startsWith('http')),\r\n num,\r\n );\r\n\r\n const snippets = takeFrom(\r\n [...html.matchAll(snippetRegex)].filter((m) => m[1]).map((m) => stripTags(m[1]!)),\r\n num,\r\n );\r\n\r\n for (let i = 0; i < Math.min(titles.length, num); i++) {\r\n results.push({\r\n title: titles[i] ?? '',\r\n url: urls[i] ?? '',\r\n snippet: snippets[i] ?? '',\r\n });\r\n }\r\n\r\n return results;\r\n}\r\n\r\nasync function bingSearch(\r\n query: string,\r\n num: number,\r\n signal: AbortSignal,\r\n): Promise<SearchOutput> {\r\n const encoded = encodeURIComponent(query);\r\n const url = `https://www.bing.com/search?q=${encoded}`;\r\n\r\n const html = await fetchWithTimeout(url, signal, TIMEOUT_MS)\r\n .then((r) => r.text())\r\n .catch(() => '');\r\n\r\n const results = parseBingResults(html, num);\r\n\r\n return {\r\n query,\r\n results,\r\n source: 'bing',\r\n truncated: results.length >= num,\r\n };\r\n}\r\n\r\nfunction parseBingResults(html: string, num: number): SearchOutput['results'] {\r\n const results: SearchOutput['results'] = [];\r\n const titleRegex = /<h2[^>]*>\\s*<a[^>]+href=\"([^\"]+)\"[^>]*>([^<]+)<\\/a>\\s*<\\/h2>/gi;\r\n const snippetRegex = /<p[^>]*class=\"[^\"]*b_paractl[^\"]*\"[^>]*>([^<]+)<\\/p>/gi;\r\n\r\n const entries = takeFrom(\r\n [...html.matchAll(titleRegex)]\r\n .filter((m) => m[1] && m[2])\r\n .map((m) => ({ url: m[1]!, title: stripTags(m[2]!) })),\r\n num,\r\n );\r\n\r\n const snippets = takeFrom(\r\n [...html.matchAll(snippetRegex)].filter((m) => m[1]).map((m) => stripTags(m[1]!)),\r\n num,\r\n );\r\n\r\n for (let i = 0; i < entries.length; i++) {\r\n results.push({\r\n title: entries[i]?.title ?? '',\r\n url: entries[i]?.url ?? '',\r\n snippet: snippets[i] ?? '',\r\n });\r\n }\r\n\r\n return results;\r\n}\r\n\r\nasync function fetchWithTimeout(\r\n url: string,\r\n signal: AbortSignal,\r\n timeoutMs: number,\r\n): Promise<Response> {\r\n const controller = new AbortController();\r\n const timer = setTimeout(() => controller.abort(), timeoutMs);\r\n\r\n const fetchSignal = anySignal(signal, controller.signal);\r\n try {\r\n const res = await fetch(url, {\r\n headers: {\r\n 'User-Agent':\r\n 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',\r\n },\r\n signal: fetchSignal,\r\n });\r\n clearTimeout(timer);\r\n return res;\r\n } catch (e) {\r\n clearTimeout(timer);\r\n throw e;\r\n }\r\n}\r\n\r\nfunction anySignal(...signals: AbortSignal[]): AbortSignal {\r\n const controller = new AbortController();\r\n for (const s of signals) {\r\n if (s.aborted) { controller.abort(); break; }\r\n s.addEventListener('abort', () => controller.abort());\r\n }\r\n return controller.signal;\r\n}\r\n\r\nfunction stripTags(html: string): string {\r\n return html.replace(/<[^>]+>/g, '').replace(/&amp;/g, '&').replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&quot;/g, '\"').replace(/&#39;/g, \"'\").trim();\r\n}"]}
package/dist/test.d.ts ADDED
@@ -0,0 +1,24 @@
1
+ import { Tool } from '@wrongstack/core';
2
+
3
+ interface TestInput {
4
+ files?: string | string[];
5
+ runner?: 'vitest' | 'jest' | 'mocha' | 'auto';
6
+ watch?: boolean;
7
+ coverage?: boolean;
8
+ cwd?: string;
9
+ grep?: string;
10
+ timeout?: number;
11
+ }
12
+ interface TestOutput {
13
+ runner: string;
14
+ exit_code: number;
15
+ tests_run: number;
16
+ passed: number;
17
+ failed: number;
18
+ duration_ms: number;
19
+ output: string;
20
+ truncated: boolean;
21
+ }
22
+ declare const testTool: Tool<TestInput, TestOutput>;
23
+
24
+ export { testTool };