lonny-agent 0.2.4 → 0.2.6

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 (102) hide show
  1. package/README.md +326 -326
  2. package/dist/agent/llm.d.ts +4 -0
  3. package/dist/agent/llm.d.ts.map +1 -1
  4. package/dist/agent/prompt-builder.d.ts.map +1 -1
  5. package/dist/agent/prompt-builder.js +13 -9
  6. package/dist/agent/prompt-builder.js.map +1 -1
  7. package/dist/agent/providers/google.d.ts.map +1 -1
  8. package/dist/agent/providers/google.js.map +1 -1
  9. package/dist/agent/providers/openai.d.ts.map +1 -1
  10. package/dist/agent/providers/openai.js +43 -3
  11. package/dist/agent/providers/openai.js.map +1 -1
  12. package/dist/agent/session.d.ts +4 -0
  13. package/dist/agent/session.d.ts.map +1 -1
  14. package/dist/agent/session.js +45 -7
  15. package/dist/agent/session.js.map +1 -1
  16. package/dist/config/index.d.ts.map +1 -1
  17. package/dist/config/index.js +7 -1
  18. package/dist/config/index.js.map +1 -1
  19. package/dist/tools/__tests__/bash.test.js +1 -1
  20. package/dist/tools/__tests__/bash.test.js.map +1 -1
  21. package/dist/tools/__tests__/edit.test.js +875 -371
  22. package/dist/tools/__tests__/edit.test.js.map +1 -1
  23. package/dist/tools/__tests__/grep.test.js +32 -0
  24. package/dist/tools/__tests__/grep.test.js.map +1 -1
  25. package/dist/tools/__tests__/sed.test.d.ts +2 -0
  26. package/dist/tools/__tests__/sed.test.d.ts.map +1 -0
  27. package/dist/tools/__tests__/sed.test.js +228 -0
  28. package/dist/tools/__tests__/sed.test.js.map +1 -0
  29. package/dist/tools/bash.d.ts.map +1 -1
  30. package/dist/tools/bash.js +83 -10
  31. package/dist/tools/bash.js.map +1 -1
  32. package/dist/tools/edit.d.ts +50 -9
  33. package/dist/tools/edit.d.ts.map +1 -1
  34. package/dist/tools/edit.js +328 -158
  35. package/dist/tools/edit.js.map +1 -1
  36. package/dist/tools/grep.d.ts.map +1 -1
  37. package/dist/tools/grep.js +54 -30
  38. package/dist/tools/grep.js.map +1 -1
  39. package/dist/tools/registry.d.ts +0 -6
  40. package/dist/tools/registry.d.ts.map +1 -1
  41. package/dist/tools/registry.js +2 -29
  42. package/dist/tools/registry.js.map +1 -1
  43. package/dist/tools/sed.d.ts +4 -0
  44. package/dist/tools/sed.d.ts.map +1 -0
  45. package/dist/tools/sed.js +121 -0
  46. package/dist/tools/sed.js.map +1 -0
  47. package/dist/web/index.d.ts.map +1 -1
  48. package/dist/web/index.js +26 -3
  49. package/dist/web/index.js.map +1 -1
  50. package/dist/web/public/app.js +5 -855
  51. package/dist/web/public/confirm.js +70 -0
  52. package/dist/web/public/index.html +121 -120
  53. package/dist/web/public/input.js +107 -0
  54. package/dist/web/public/messages.js +395 -0
  55. package/dist/web/public/sidebar.js +82 -0
  56. package/dist/web/public/state.js +72 -0
  57. package/dist/web/public/style.css +996 -949
  58. package/dist/web/public/utils.js +37 -0
  59. package/dist/web/public/websocket.js +267 -0
  60. package/dist/web/public/ws.js +19 -0
  61. package/dist/web/session-bridge.d.ts.map +1 -1
  62. package/dist/web/session-bridge.js +3 -7
  63. package/dist/web/session-bridge.js.map +1 -1
  64. package/package.json +55 -54
  65. package/scripts/copy-native.mjs +24 -0
  66. package/scripts/copy-web.mjs +15 -0
  67. package/src/agent/index.ts +25 -25
  68. package/src/agent/llm.ts +4 -0
  69. package/src/agent/prompt-builder.ts +13 -9
  70. package/src/agent/providers/anthropic.ts +179 -179
  71. package/src/agent/providers/google.ts +210 -211
  72. package/src/agent/providers/ollama.ts +186 -186
  73. package/src/agent/providers/openai.ts +61 -5
  74. package/src/agent/session.ts +59 -3
  75. package/src/config/index.ts +342 -335
  76. package/src/tools/__tests__/bash.test.ts +1 -1
  77. package/src/tools/__tests__/edit.test.ts +981 -385
  78. package/src/tools/__tests__/grep.test.ts +35 -0
  79. package/src/tools/bash.ts +97 -11
  80. package/src/tools/edit.ts +362 -169
  81. package/src/tools/git.ts +76 -76
  82. package/src/tools/grep.ts +57 -30
  83. package/src/tools/registry.ts +2 -31
  84. package/src/tui/index.ts +786 -786
  85. package/src/web/index.ts +25 -3
  86. package/src/web/public/app.js +5 -855
  87. package/src/web/public/confirm.js +70 -0
  88. package/src/web/public/index.html +121 -120
  89. package/src/web/public/input.js +107 -0
  90. package/src/web/public/messages.js +395 -0
  91. package/src/web/public/sidebar.js +82 -0
  92. package/src/web/public/state.js +72 -0
  93. package/src/web/public/style.css +996 -949
  94. package/src/web/public/utils.js +37 -0
  95. package/src/web/public/websocket.js +267 -0
  96. package/src/web/public/ws.js +19 -0
  97. package/src/web/session-bridge.ts +193 -194
  98. package/.claude/settings.local.json +0 -14
  99. package/.kilo/plans/1780064105789-mighty-pixel.md +0 -171
  100. package/.kilo/plans/1780293725888-quick-wizard.md +0 -62
  101. package/.lonny/plan-web-cwd-status.md +0 -38
  102. package/src/tools/exec.ts +0 -348
package/src/tools/git.ts CHANGED
@@ -1,76 +1,76 @@
1
- import { execSync } from 'node:child_process'
2
- import type { Tool, ToolDefinition } from './types.js'
3
-
4
- export const createGitTool = (cwd: string): Tool => {
5
- const definition: ToolDefinition = {
6
- name: 'git',
7
- description:
8
- 'Run git commands for repository operations. Supports status, diff, log, show, branch, and other read-only git operations.',
9
- parameters: {
10
- command: {
11
- type: 'string',
12
- description: 'Git subcommand and arguments (e.g. "status", "diff", "log --oneline -5")',
13
- required: true,
14
- },
15
- path: { type: 'string', description: 'Repository path (default: cwd)', required: false },
16
- },
17
- }
18
-
19
- const execute = async (
20
- input: Record<string, unknown>,
21
- ): Promise<{ success: boolean; output: string; error?: string }> => {
22
- const gitCmd = String(input.command || '').trim()
23
- const repoPath = String(input.path || cwd)
24
-
25
- if (!gitCmd) {
26
- return { success: false, output: '', error: 'command is required' }
27
- }
28
-
29
- // Safety: block destructive commands
30
- const destructivePatterns = [
31
- /^reset\s+(--hard|--soft|--mixed|--merge|--keep)/i,
32
- /^push\s+.*(--force|-f)/i,
33
- /^(rebase|merge|cherry-pick|revert)\b/i,
34
- /^branch\s+-[dD]/i,
35
- /^tag\s+-[dD]/i,
36
- /^(clean|gc|prune)\b/i,
37
- /^update-ref\b/i,
38
- /^rm\b/i,
39
- /^commit\s+--amend/i,
40
- /^checkout\s+.*-B/i,
41
- ]
42
-
43
- for (const pattern of destructivePatterns) {
44
- if (pattern.test(gitCmd)) {
45
- return {
46
- success: false,
47
- output: '',
48
- error: `Destructive git command not allowed: "${gitCmd}". Only read-only operations are permitted.`,
49
- }
50
- }
51
- }
52
-
53
- try {
54
- const result = execSync(`git ${gitCmd}`, {
55
- cwd: repoPath,
56
- encoding: 'utf-8',
57
- timeout: 30000,
58
- maxBuffer: 10 * 1024 * 1024, // 10MB
59
- })
60
-
61
- return { success: true, output: result.trim() || '(empty output)' }
62
- } catch (err) {
63
- const error = err as { stderr?: string; stdout?: string; message?: string }
64
- const stderr = error.stderr || ''
65
- const stdout = error.stdout || ''
66
- const msg = error.message || String(err)
67
- return {
68
- success: false,
69
- output: stdout.trim(),
70
- error: stderr.trim() || msg,
71
- }
72
- }
73
- }
74
-
75
- return { definition, execute }
76
- }
1
+ import { execSync } from 'node:child_process'
2
+ import type { Tool, ToolDefinition } from './types.js'
3
+
4
+ export const createGitTool = (cwd: string): Tool => {
5
+ const definition: ToolDefinition = {
6
+ name: 'git',
7
+ description:
8
+ 'Run git commands for repository operations. Supports status, diff, log, show, branch, and other read-only git operations.',
9
+ parameters: {
10
+ command: {
11
+ type: 'string',
12
+ description: 'Git subcommand and arguments (e.g. "status", "diff", "log --oneline -5")',
13
+ required: true,
14
+ },
15
+ path: { type: 'string', description: 'Repository path (default: cwd)', required: false },
16
+ },
17
+ }
18
+
19
+ const execute = async (
20
+ input: Record<string, unknown>,
21
+ ): Promise<{ success: boolean; output: string; error?: string }> => {
22
+ const gitCmd = String(input.command || '').trim()
23
+ const repoPath = String(input.path || cwd)
24
+
25
+ if (!gitCmd) {
26
+ return { success: false, output: '', error: 'command is required' }
27
+ }
28
+
29
+ // Safety: block destructive commands
30
+ const destructivePatterns = [
31
+ /^reset\s+(--hard|--soft|--mixed|--merge|--keep)/i,
32
+ /^push\s+.*(--force|-f)/i,
33
+ /^(rebase|merge|cherry-pick|revert)\b/i,
34
+ /^branch\s+-[dD]/i,
35
+ /^tag\s+-[dD]/i,
36
+ /^(clean|gc|prune)\b/i,
37
+ /^update-ref\b/i,
38
+ /^rm\b/i,
39
+ /^commit\s+--amend/i,
40
+ /^checkout\s+.*-B/i,
41
+ ]
42
+
43
+ for (const pattern of destructivePatterns) {
44
+ if (pattern.test(gitCmd)) {
45
+ return {
46
+ success: false,
47
+ output: '',
48
+ error: `Destructive git command not allowed: "${gitCmd}". Only read-only operations are permitted.`,
49
+ }
50
+ }
51
+ }
52
+
53
+ try {
54
+ const result = execSync(`git ${gitCmd}`, {
55
+ cwd: repoPath,
56
+ encoding: 'utf-8',
57
+ timeout: 30000,
58
+ maxBuffer: 10 * 1024 * 1024, // 10MB
59
+ })
60
+
61
+ return { success: true, output: result.trim() || '(empty output)' }
62
+ } catch (err) {
63
+ const error = err as { stderr?: string; stdout?: string; message?: string }
64
+ const stderr = error.stderr || ''
65
+ const stdout = error.stdout || ''
66
+ const msg = error.message || String(err)
67
+ return {
68
+ success: false,
69
+ output: stdout.trim(),
70
+ error: stderr.trim() || msg,
71
+ }
72
+ }
73
+ }
74
+
75
+ return { definition, execute }
76
+ }
package/src/tools/grep.ts CHANGED
@@ -1,16 +1,20 @@
1
- import { execSync } from 'node:child_process'
1
+ import { execFileSync } from 'node:child_process'
2
2
  import * as fs from 'node:fs/promises'
3
3
  import * as path from 'node:path'
4
4
  import { fmtErr } from './errors.js'
5
5
  import type { Tool, ToolResult } from './types.js'
6
6
 
7
+ let _hasRg: boolean | undefined
8
+
7
9
  function hasRg(): boolean {
10
+ if (_hasRg !== undefined) return _hasRg
8
11
  try {
9
- execSync('rg --version', { stdio: 'pipe' })
10
- return true
12
+ execFileSync('rg', ['--version'], { stdio: 'pipe' })
13
+ _hasRg = true
11
14
  } catch {
12
- return false
15
+ _hasRg = false
13
16
  }
17
+ return _hasRg
14
18
  }
15
19
 
16
20
  /** Simple glob-to-regex for the `include` parameter (e.g. "*.ts", "*.{ts,tsx}"). */
@@ -25,9 +29,11 @@ function includeRe(include: string): RegExp {
25
29
  continue
26
30
  }
27
31
  if (ch === '*') {
32
+ // Handle ** (match across dirs) and * (match within a single dir segment)
28
33
  if (include[i + 1] === '*') {
29
34
  re += '.*'
30
- i += 2
35
+ // Skip all consecutive asterisks so `***` doesn't leave a trailing literal `*`
36
+ while (i < include.length && include[i] === '*') i++
31
37
  continue
32
38
  }
33
39
  re += '[^/]*'
@@ -48,6 +54,12 @@ function includeRe(include: string): RegExp {
48
54
  i++
49
55
  continue
50
56
  }
57
+ // Escape special regex characters (except those we handle above)
58
+ if (/[.+^${}()|[\]\\]/.test(ch)) {
59
+ re += '\\' + ch
60
+ i++
61
+ continue
62
+ }
51
63
  re += ch
52
64
  i++
53
65
  }
@@ -64,32 +76,38 @@ async function nodeGrep(dir: string, re: RegExp, incRe: RegExp | null): Promise<
64
76
  const results: NodeGrepMatch[] = []
65
77
  try {
66
78
  const entries = await fs.readdir(dir, { withFileTypes: true })
67
- const subResults: Promise<NodeGrepMatch[]>[] = []
79
+ const promises: Promise<void>[] = []
80
+
68
81
  for (const e of entries) {
69
82
  const full = path.join(dir, e.name)
70
83
  if (e.isDirectory()) {
71
84
  if (e.name === '.git' || e.name === 'node_modules') continue
72
- subResults.push(nodeGrep(full, re, incRe))
85
+ promises.push(
86
+ nodeGrep(full, re, incRe).then(sub => {
87
+ for (const m of sub) results.push(m)
88
+ }),
89
+ )
73
90
  } else if (e.isFile()) {
74
91
  if (incRe && !incRe.test(e.name)) continue
75
- try {
76
- const content = await fs.readFile(full, 'utf-8')
77
- const lines = content.split('\n')
78
- for (let i = 0; i < lines.length; i++) {
79
- if (re.test(lines[i])) {
80
- results.push({ file: full, line: i + 1, text: lines[i] })
92
+ promises.push(
93
+ (async () => {
94
+ try {
95
+ const content = await fs.readFile(full, 'utf-8')
96
+ const lines = content.split('\n')
97
+ for (let i = 0; i < lines.length; i++) {
98
+ if (re.test(lines[i])) {
99
+ results.push({ file: full, line: i + 1, text: lines[i] })
100
+ }
101
+ }
102
+ } catch {
103
+ /* permission denied etc */
81
104
  }
82
- }
83
- } catch {
84
- /* permission denied etc */
85
- }
105
+ })(),
106
+ )
86
107
  }
87
108
  }
88
- // Await all subdirectory results concurrently
89
- const subMatches = await Promise.all(subResults)
90
- for (const sm of subMatches) {
91
- results.push(...sm)
92
- }
109
+
110
+ await Promise.all(promises)
93
111
  } catch {
94
112
  /* permission denied etc */
95
113
  }
@@ -120,11 +138,23 @@ export function createGrepTool(cwd: string): Tool {
120
138
 
121
139
  try {
122
140
  if (useRg) {
123
- let cmd = `rg -n "${pattern.replace(/"/g, '\\"')}"`
124
- if (include) cmd += ` -g "${include}"`
125
- cmd += ` "${searchPath}"`
126
- const output = execSync(cmd, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] })
127
- return { success: true, output: output || 'No matches found.' }
141
+ const args = ['-n', '--no-heading', pattern]
142
+ if (include) args.push('-g', include)
143
+ args.push(searchPath)
144
+ try {
145
+ const output = execFileSync('rg', args, {
146
+ encoding: 'utf-8',
147
+ stdio: ['pipe', 'pipe', 'pipe'],
148
+ })
149
+ return { success: true, output: output || 'No matches found.' }
150
+ } catch (rgErr: unknown) {
151
+ const rgMsg = fmtErr(rgErr)
152
+ // ripgrep exit code 1 = no matches, exit code 2 = error
153
+ if (rgMsg.includes('exit code 1')) {
154
+ return { success: true, output: 'No matches found.' }
155
+ }
156
+ throw rgErr
157
+ }
128
158
  }
129
159
 
130
160
  const re = new RegExp(pattern)
@@ -141,9 +171,6 @@ export function createGrepTool(cwd: string): Tool {
141
171
  return { success: true, output }
142
172
  } catch (err) {
143
173
  const msg = fmtErr(err)
144
- if (useRg && msg.includes('exit code 1')) {
145
- return { success: true, output: 'No matches found.' }
146
- }
147
174
  return { success: false, output: '', error: `Grep failed: ${msg}` }
148
175
  }
149
176
  },
@@ -2,7 +2,6 @@ import type { FileReadTracker } from '../diff/apply.js'
2
2
  import { bashTool } from './bash.js'
3
3
  import { createEditTool } from './edit.js'
4
4
  import { fmtErr } from './errors.js'
5
- import { createExecTool, updateExecToolDefinition } from './exec.js'
6
5
  import { fetchTool } from './fetch.js'
7
6
  import { createFindTool } from './find.js'
8
7
  import { createGitTool } from './git.js'
@@ -50,9 +49,6 @@ function normalizeToolInput(
50
49
  if (toolName === 'write_plan') {
51
50
  return { filename: input, content: input }
52
51
  }
53
- if (toolName === 'exec') {
54
- return { code: input }
55
- }
56
52
  // For tools that take a single string param, guess from the definition
57
53
  const params = Object.keys(_definition.parameters)
58
54
  if (params.length === 1) {
@@ -97,8 +93,6 @@ export class ToolRegistry {
97
93
  private tools: Map<string, Tool> = new Map()
98
94
  private context: ToolContext
99
95
  private plugins: Map<string, ToolPlugin> = new Map()
100
- /** Reference to the exec tool for dynamic description updates */
101
- private execTool: Tool | null = null
102
96
 
103
97
  constructor(context: ToolContext) {
104
98
  this.context = context
@@ -107,7 +101,7 @@ export class ToolRegistry {
107
101
 
108
102
  /** Register all built-in tools */
109
103
  private registerBuiltins(): void {
110
- // ask mode: only fetch and search + exec (exec works with any toolset)
104
+ // ask mode: only fetch and search
111
105
  if (this.context.mode === 'ask') {
112
106
  this.register(fetchTool)
113
107
  this.register(searchTool)
@@ -128,27 +122,12 @@ export class ToolRegistry {
128
122
  this.register(createGitTool(this.context.cwd))
129
123
  this.register(createInstallSkillTool(this.context.cwd))
130
124
  this.register(createEditTool(this.context.applier, this.context.cwd))
131
- this.registerExecTool()
132
125
  } else {
133
126
  // Plan mode: read-only investigation + write_plan
134
127
  this.register(createWritePlanTool(this.context.cwd, this.context.onPlanWritten))
135
128
  }
136
129
  }
137
130
 
138
- /** Register the exec tool with dynamic type declarations for all registered tools */
139
- private registerExecTool(): void {
140
- const execTool = createExecTool(() => Array.from(this.tools.values()))
141
- this.execTool = execTool
142
- this.register(execTool)
143
- }
144
-
145
- /** Refresh the exec tool's description to include current tool type declarations */
146
- private refreshExecDescription(): void {
147
- if (this.execTool) {
148
- updateExecToolDefinition(this.execTool, this.getDefinitions())
149
- }
150
- }
151
-
152
131
  setMode(mode: 'code' | 'plan' | 'ask'): void {
153
132
  // Update context.mode FIRST so registerBuiltins() and mode-based logic
154
133
  // use the NEW mode, not the stale one (fixes bug when switching from
@@ -158,20 +137,16 @@ export class ToolRegistry {
158
137
  if (mode === 'code') {
159
138
  // Re-register all built-in tools for code mode
160
139
  this.tools.clear()
161
- this.execTool = null
162
140
  this.registerBuiltins()
163
141
  } else if (mode === 'plan') {
164
- // Keep existing tools but swap edit/write_plan, remove exec
142
+ // Keep existing tools but swap edit/write_plan
165
143
  if (!this.tools.has('write_plan')) {
166
144
  this.register(createWritePlanTool(this.context.cwd, this.context.onPlanWritten))
167
145
  }
168
146
  this.tools.delete('edit')
169
- this.tools.delete('exec')
170
- this.execTool = null
171
147
  } else if (mode === 'ask') {
172
148
  // Ask mode: only fetch and search
173
149
  this.tools.clear()
174
- this.execTool = null
175
150
  this.register(fetchTool)
176
151
  this.register(searchTool)
177
152
  }
@@ -180,10 +155,6 @@ export class ToolRegistry {
180
155
  /** Register a single tool */
181
156
  register(tool: Tool): void {
182
157
  this.tools.set(tool.definition.name, tool)
183
- // Refresh exec tool description to include the new tool's type declarations
184
- if (this.execTool && tool.definition.name !== 'exec') {
185
- this.refreshExecDescription()
186
- }
187
158
  }
188
159
 
189
160
  /** Register a tool plugin (lazy creation pattern) */