thincoder 0.12.2 → 0.12.4

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 (85) hide show
  1. package/README.md +29 -6
  2. package/package.json +3 -3
  3. package/src/advisor/history.mjs +112 -0
  4. package/src/advisor/messages.mjs +182 -0
  5. package/src/advisor/repos.mjs +133 -0
  6. package/src/advisor/run.mjs +346 -0
  7. package/src/advisor.mjs +109 -509
  8. package/src/agent/completion.mjs +134 -0
  9. package/src/agent/dispatch.mjs +54 -7
  10. package/src/agent/post-turn.mjs +70 -0
  11. package/src/agent/setup.mjs +95 -6
  12. package/src/agent-tools/advisor.mjs +159 -12
  13. package/src/agent-tools/eng.mjs +64 -0
  14. package/src/agent-tools/subagent.mjs +73 -3
  15. package/src/agent-tools/task.mjs +45 -6
  16. package/src/agent-tools/verify.mjs +18 -0
  17. package/src/agent-tools.mjs +1 -0
  18. package/src/agent.mjs +152 -161
  19. package/src/cli/make-agent.mjs +1 -0
  20. package/src/cli/setup-wizard.mjs +1 -0
  21. package/src/config.mjs +34 -4
  22. package/src/context.mjs +47 -13
  23. package/src/generate-title.mjs +44 -0
  24. package/src/prompts/advisor-design.md +43 -0
  25. package/src/prompts/advisor-round1.md +11 -4
  26. package/src/prompts/advisor-round2.md +12 -7
  27. package/src/prompts/advisor-round3.md +11 -6
  28. package/src/prompts/coder.md +9 -3
  29. package/src/prompts/discipline.md +12 -96
  30. package/src/prompts/eng-coder.md +34 -0
  31. package/src/prompts/engineering-sub.md +12 -0
  32. package/src/prompts/engineering.md +96 -0
  33. package/src/prompts/main.md +1 -1
  34. package/src/prompts/methodology-template.md +39 -0
  35. package/src/prompts/plan.md +2 -2
  36. package/src/prompts/system.md +43 -61
  37. package/src/provider/core.mjs +58 -2
  38. package/src/session.mjs +291 -94
  39. package/src/skills.mjs +48 -15
  40. package/src/tools/apply_patch.md +1 -1
  41. package/src/tools/checklist.mjs +4 -3
  42. package/src/tools/codemode.mjs +23 -11
  43. package/src/tools/delete.md +1 -0
  44. package/src/tools/edit.md +1 -1
  45. package/src/tools/execute.md +5 -0
  46. package/src/tools/file.mjs +4 -0
  47. package/src/tools/git.md +15 -0
  48. package/src/tools/git.mjs +1 -6
  49. package/src/tools/lint.md +8 -0
  50. package/src/tools/linter.mjs +1 -5
  51. package/src/tools/lsp.md +7 -0
  52. package/src/tools/lsp.mjs +8 -9
  53. package/src/tools/patch.mjs +1 -29
  54. package/src/tools/read_image.md +5 -1
  55. package/src/tools/system.mjs +1 -1
  56. package/src/tools/web.mjs +3 -3
  57. package/src/tui/agent-turn.mjs +184 -66
  58. package/src/tui/ansi.mjs +4 -0
  59. package/src/tui/clipboard.mjs +9 -0
  60. package/src/tui/cmd-config.mjs +14 -26
  61. package/src/tui/cmd-eng.mjs +44 -0
  62. package/src/tui/cmd-exit.mjs +1 -1
  63. package/src/tui/cmd-fold.mjs +3 -4
  64. package/src/tui/cmd-model.mjs +11 -6
  65. package/src/tui/cmd-new.mjs +5 -5
  66. package/src/tui/cmd-session.mjs +21 -11
  67. package/src/tui/cmd-think.mjs +1 -0
  68. package/src/tui/index.mjs +20 -9
  69. package/src/tui/key-handler.mjs +177 -9
  70. package/src/tui/layout.mjs +5 -5
  71. package/src/tui/markdown.mjs +52 -0
  72. package/src/tui/pickers.mjs +190 -45
  73. package/src/tui/render-conversation.mjs +54 -13
  74. package/src/tui/render-frame.mjs +39 -12
  75. package/src/tui/render-loop.mjs +2 -1
  76. package/src/tui/render.mjs +13 -7
  77. package/src/tui/slash-commands.mjs +11 -7
  78. package/src/tui/startup.mjs +4 -3
  79. package/src/tui/wizard.mjs +3 -0
  80. package/src/tools/checkpoint.md +0 -15
  81. package/src/tools/git_diff.md +0 -11
  82. package/src/tools/git_log.md +0 -10
  83. package/src/tools/git_status.md +0 -8
  84. package/src/tools/linter.md +0 -13
  85. package/src/tools/syntax_check.md +0 -10
@@ -24,14 +24,17 @@
24
24
  import { Script, createContext } from "node:vm"
25
25
  import { readFileSync, writeFileSync, existsSync, mkdirSync, statSync, readdirSync } from "node:fs"
26
26
  import { join, dirname, relative, resolve } from "node:path"
27
- import { globToRegex, normalizeEOL, isPrivateHost } from "./shared.mjs"
27
+ import { DESC, globToRegex, normalizeEOL, isPrivateHost } from "./shared.mjs"
28
28
 
29
29
  const MAX_OUTPUT = 50_000
30
30
  const MAX_SCRIPT = 50_000
31
31
  const DEFAULT_TIMEOUT = 30_000
32
32
 
33
- /** SSRF-safe fetch: only http/https, private IP rejection, 10s timeout. */
34
- async function sandboxFetch(url) {
33
+ /** SSRF-safe fetch: only http/https, private IP rejection, 10s timeout.
34
+ * Validation throws SYNCHRONOUSLY so the vm sandbox's try/catch can catch it —
35
+ * an async throw here would become an unhandled rejection and crash the host process,
36
+ * and the rejection message would never reach the model. */
37
+ function sandboxFetch(url) {
35
38
  const parsed = new URL(url)
36
39
  if (!["http:", "https:"].includes(parsed.protocol)) {
37
40
  throw new Error(`CodeMode fetch: protocol not allowed: ${parsed.protocol}`)
@@ -40,6 +43,10 @@ async function sandboxFetch(url) {
40
43
  if (isPrivateHost(parsed.hostname)) {
41
44
  throw new Error(`CodeMode fetch: private/internal host not allowed: ${parsed.hostname}`)
42
45
  }
46
+ return doFetch(url)
47
+ }
48
+
49
+ async function doFetch(url) {
43
50
  const ctrl = new AbortController()
44
51
  const timer = setTimeout(() => ctrl.abort(), 10_000)
45
52
  try {
@@ -53,9 +60,7 @@ async function sandboxFetch(url) {
53
60
 
54
61
  export const codeModeTool = {
55
62
  name: "execute",
56
- description:
57
- "Execute sandboxed JavaScript code. Use this to compose multiple file operations into one call — " +
58
- "read, write, glob, grep, and log results. No network or system access. Max 30s timeout, 50KB output.",
63
+ description: DESC("execute"),
59
64
  parameters: {
60
65
  type: "object",
61
66
  properties: {
@@ -96,6 +101,11 @@ export const codeModeTool = {
96
101
  return abs
97
102
  }
98
103
 
104
+ // Block dynamic imports: check for import() syntax before execution
105
+ if (/\bimport\s*\(/.test(code)) {
106
+ return "Error: dynamic import() is not allowed in CodeMode sandbox. Use the provided readFile/writeFile/glob/grep/fetch functions instead."
107
+ }
108
+
99
109
  const sandbox = createContext({
100
110
  readFile: (p) => {
101
111
  const abs = safePath(p)
@@ -149,14 +159,16 @@ export const codeModeTool = {
149
159
  }
150
160
  },
151
161
  fetch: sandboxFetch,
162
+ // Block process and require access
163
+ require: () => { throw new Error("require() is not available in CodeMode sandbox") },
164
+ process: undefined,
152
165
  })
153
166
 
154
167
  try {
155
- const script = new Script(code, {
156
- filename: "codemode.js",
157
- timeout: timeoutMs,
158
- })
159
- script.runInContext(sandbox)
168
+ const script = new Script(code, { filename: "codemode.js" })
169
+ // timeout belongs on runInContext — the Script constructor ignores it,
170
+ // so passing it there let runaway scripts (while(true)) hang the process forever.
171
+ script.runInContext(sandbox, { timeout: timeoutMs })
160
172
  return output.join("\n") || "(no output)"
161
173
  } catch (err) {
162
174
  const out = output.join("\n")
@@ -2,6 +2,7 @@ Delete a file. Use when the agent created a temporary or junk file that should b
2
2
 
3
3
  Parameters:
4
4
  - path (required): File path, relative to cwd or absolute
5
+ - force: Allow deleting git-tracked files (default false)
5
6
 
6
7
  Notes:
7
8
  - Untracked or non-git files are deleted immediately
package/src/tools/edit.md CHANGED
@@ -15,6 +15,6 @@ Parameters:
15
15
  - replace_all: Replace all occurrences instead of just one (default false)
16
16
 
17
17
  Notes:
18
- - Prefer this over write for targeted edits — it's safer and keeps diffs small
18
+ - Prefer this over write for targeted edits — it's safer and keeps changes targeted
19
19
  - If old_string matches zero times: error. If it matches multiple times without replace_all: error — add more surrounding context to make it unique
20
20
  - Never fabricate the old_string — copy it verbatim from the actual file using read first
@@ -0,0 +1,5 @@
1
+ Execute sandboxed JavaScript code. Use this to compose multiple file operations into one call — read, write, glob, grep, and log results. No network or system access. Max 30s timeout, 50KB output.
2
+
3
+ Parameters:
4
+ - code (required): JavaScript code to execute in the sandbox. Use provided functions: readFile(path), writeFile(path, content), glob(pattern), grep(pattern, file), log(...args).
5
+ - timeoutMs: Timeout in milliseconds (default 30000, max 60000)
@@ -122,6 +122,7 @@ export const writeTool = {
122
122
  required: ["path", "content"],
123
123
  },
124
124
  readonly: false,
125
+ touchedPaths(args) { return args.path ? [args.path] : [] },
125
126
  async execute(args, ctx) {
126
127
  const abs = resolveInCwd(ctx, args.path)
127
128
  await mkdir(dirname(abs), { recursive: true })
@@ -149,6 +150,7 @@ export const editTool = {
149
150
  required: ["path", "old_string", "new_string"],
150
151
  },
151
152
  readonly: false,
153
+ touchedPaths(args) { return args.path ? [args.path] : [] },
152
154
  async execute(args, ctx) {
153
155
  const abs = resolveInCwd(ctx, args.path)
154
156
  if (!args.old_string) {
@@ -194,6 +196,7 @@ export const insertAfterTool = {
194
196
  required: ["path", "content"],
195
197
  },
196
198
  readonly: false,
199
+ touchedPaths(args) { return args.path ? [args.path] : [] },
197
200
  async execute(args, ctx) {
198
201
  const abs = resolveInCwd(ctx, args.path)
199
202
  const text = normalizeEOL(await readFile(abs, "utf8"))
@@ -257,6 +260,7 @@ export const hashlineEditTool = {
257
260
  required: ["path", "old_hashes", "new_content"],
258
261
  },
259
262
  readonly: false,
263
+ touchedPaths(args) { return args.path ? [args.path] : [] },
260
264
  async execute(args, ctx) {
261
265
  const abs = resolveInCwd(ctx, args.path)
262
266
  if (!args.old_hashes?.length) throw new Error("old_hashes must not be empty — read the file with hashes=true to get line hashes")
@@ -0,0 +1,15 @@
1
+ Run a git command. Use this to see uncommitted changes, staged changes, diff against a ref, recent commits, or manage checkpoints. Only works inside a git repository.
2
+ - action='diff': Show unified diff — what changed since last commit. Set staged=true for staged-only diff, ref=<ref> to compare against a specific commit/branch, path=<dir> to scope to a file or directory.
3
+ - action='status': Show working tree state — staged, unstaged, untracked files, and conflicts. Returns categorized lists.
4
+ - action='log': Show recent commit history. Set count to limit, oneline=true for compact format, path=<file> to see history of one file.
5
+ - action='checkpoint': Manage git-based snapshots. Use checkpointAction to choose: list (overview), create (snapshot now), rewind (restore snapshot by id), cat (read a file from a snapshot).
6
+
7
+ Parameters:
8
+ - action (required): diff / status / log / checkpoint
9
+ - staged: (diff) Show staged changes instead of working tree
10
+ - path: (diff/log/checkpoint:cat/checkpoint:rewind) File or directory to scope to
11
+ - ref: (diff) Compare against this ref (default HEAD)
12
+ - count: (log) Number of commits (default 10)
13
+ - oneline: (log) One-line-per-commit format
14
+ - checkpointAction: (checkpoint) list snapshots / create one / restore by id / read file from snapshot
15
+ - checkpointId: (checkpoint) Snapshot id — required for rewind and cat; optional for list (shows file tree)
package/src/tools/git.mjs CHANGED
@@ -9,12 +9,7 @@ import { join } from "node:path";
9
9
 
10
10
  export const gitTool = {
11
11
  name: "git",
12
- description:
13
- "Run a git command. Use this to see uncommitted changes, staged changes, diff against a ref, recent commits, or manage checkpoints. Only works inside a git repository.\n" +
14
- "- action='diff': Show unified diff — what changed since last commit. Set staged=true for staged-only diff, ref=<ref> to compare against a specific commit/branch, path=<dir> to scope to a file or directory.\n" +
15
- "- action='status': Show working tree state — staged, unstaged, untracked files, and conflicts. Returns categorized lists.\n" +
16
- "- action='log': Show recent commit history. Set count to limit, oneline=true for compact format, path=<file> to see history of one file.\n" +
17
- "- action='checkpoint': Manage git-based snapshots. Use checkpointAction to choose: list (overview), create (snapshot now), rewind (restore snapshot by id), cat (read a file from a snapshot).",
12
+ description: DESC("git"),
18
13
  parameters: {
19
14
  type: "object",
20
15
  properties: {
@@ -0,0 +1,8 @@
1
+ Run the appropriate linter/checker for a file. Auto-detects based on file extension and project config.
2
+ Without 'full', runs a fast node --check (JS/TS syntax only, catches parse errors in milliseconds).
3
+ With 'full', runs the language-aware cascade: eslint → tsc –noEmit → node --check (JS/TS/TSX); ruff (Python); cargo check (Rust); go vet (Go).
4
+ Use the fast default after every write/edit; use 'full' before declaring a task complete.
5
+
6
+ Parameters:
7
+ - path: File to check (default: most recently modified file)
8
+ - full: Run the full language-aware cascade instead of just node --check (default false)
@@ -5,11 +5,7 @@ import { join, relative } from "node:path"
5
5
 
6
6
  export const lintTool = {
7
7
  name: "lint",
8
- description:
9
- "Run the appropriate linter/checker for a file. Auto-detects based on file extension and project config.\n" +
10
- "Without 'full', runs a fast node --check (JS/TS syntax only, catches parse errors in milliseconds).\n" +
11
- "With 'full', runs the language-aware cascade: eslint → tsc –noEmit → node --check (JS/TS/TSX); ruff (Python); cargo check (Rust); go vet (Go).\n" +
12
- "Use the fast default after every write/edit; use 'full' before declaring a task complete.",
8
+ description: DESC("lint"),
13
9
  parameters: {
14
10
  type: "object",
15
11
  properties: {
@@ -0,0 +1,7 @@
1
+ LSP code intelligence: go to definition, find references, hover info, document symbols, diagnostics. Use this to understand code structure without grep-guessing function locations or type shapes.
2
+
3
+ Parameters:
4
+ - subcommand (required): LSP operation — "definition" | "references" | "hover" | "symbols" | "diagnostics"
5
+ - uri (required): Target file path (relative to project root)
6
+ - line: 1-based line number (for definition/references/hover)
7
+ - character: 1-based character offset (for definition/references/hover)
package/src/tools/lsp.mjs CHANGED
@@ -17,6 +17,7 @@
17
17
  import { spawn } from "node:child_process"
18
18
  import { existsSync, readFileSync } from "node:fs"
19
19
  import { join, extname } from "node:path"
20
+ import { DESC } from "./shared.mjs"
20
21
 
21
22
  // ---- JSON-RPC transport over stdio ----
22
23
 
@@ -184,9 +185,7 @@ async function ensureOpen(proc, uri, ext) {
184
185
 
185
186
  export const lspTool = {
186
187
  name: "lsp",
187
- description:
188
- "LSP code intelligence: go to definition, find references, hover info, document symbols, diagnostics. " +
189
- "Use this to understand code structure without grep-guessing function locations or type shapes.",
188
+ description: DESC("lsp"),
190
189
  parameters: {
191
190
  type: "object",
192
191
  properties: {
@@ -240,7 +239,7 @@ export const lspTool = {
240
239
  textDocument: { uri },
241
240
  position: { line: args.line - 1, character: args.character - 1 },
242
241
  }, 10)
243
- if (!res?.result) return "No definition found."
242
+ if (!res?.result) return "(no definition found)"
244
243
  const locs = Array.isArray(res.result) ? res.result : [res.result]
245
244
  return locs.map((l) => {
246
245
  const path = l.uri.replace(/^file:\/\/\//, "").replace(/%3A/, ":")
@@ -255,7 +254,7 @@ export const lspTool = {
255
254
  position: { line: args.line - 1, character: args.character - 1 },
256
255
  context: { includeDeclaration: false },
257
256
  }, 10)
258
- if (!res?.result?.length) return "No references found."
257
+ if (!res?.result?.length) return "(no references found)"
259
258
  return res.result.slice(0, 50).map((l) => {
260
259
  const path = l.uri.replace(/^file:\/\/\//, "").replace(/%3A/, ":")
261
260
  return `${path}:${l.range.start.line + 1}:${l.range.start.character + 1}`
@@ -268,7 +267,7 @@ export const lspTool = {
268
267
  textDocument: { uri },
269
268
  position: { line: args.line - 1, character: args.character - 1 },
270
269
  }, 10)
271
- if (!res?.result?.contents) return "No hover info available."
270
+ if (!res?.result?.contents) return "(no hover info)"
272
271
  const contents = res.result.contents
273
272
  if (typeof contents === "string") return contents
274
273
  if (Array.isArray(contents)) return contents.map((c) => typeof c === "string" ? c : c.value).join("\n")
@@ -280,7 +279,7 @@ export const lspTool = {
280
279
  const res = await request(proc, "textDocument/documentSymbol", {
281
280
  textDocument: { uri },
282
281
  }, 10)
283
- if (!res?.result?.length) return "No symbols found."
282
+ if (!res?.result?.length) return "(no symbols found)"
284
283
  function render(nodes, depth) {
285
284
  const lines = []
286
285
  for (const n of nodes) {
@@ -295,7 +294,7 @@ export const lspTool = {
295
294
 
296
295
  case "diagnostics": {
297
296
  const diags = proc._diagnostics?.[uri]
298
- if (!diags?.length) return "No diagnostics."
297
+ if (!diags?.length) return "(no diagnostics)"
299
298
  return diags.slice(0, 30).map((d) => {
300
299
  const sev = { 1: "ERROR", 2: "WARN", 3: "INFO", 4: "HINT" }[d.severity] || "?"
301
300
  return `L${d.range.start.line + 1}: ${sev}: ${d.message}${d.code ? ` [${d.code}]` : ""}`
@@ -306,7 +305,7 @@ export const lspTool = {
306
305
  return `Unknown subcommand: ${args.subcommand}`
307
306
  }
308
307
  } catch (err) {
309
- return `LSP error: ${err.message}`
308
+ return `lsp error: ${err.message}`
310
309
  }
311
310
  },
312
311
  }
@@ -159,36 +159,7 @@ export const applyPatchTool = {
159
159
  },
160
160
  }
161
161
 
162
- // ---------------------------------------------------------------- syntax_check
163
162
 
164
- export const syntaxCheckTool = {
165
- name: "syntax_check",
166
- description: DESC("syntax_check"),
167
- parameters: {
168
- type: "object",
169
- properties: {
170
- path: { type: "string", description: "File path (.js/.mjs/.cjs only)" },
171
- },
172
- required: ["path"],
173
- },
174
- readonly: true,
175
- execute(args, ctx) {
176
- const abs = resolveInCwd(ctx, args.path)
177
- if (!/\.(?:[mc]?js)$/.test(abs)) {
178
- return `syntax_check only supports .js/.mjs/.cjs files; ${args.path} skipped.`
179
- }
180
- try {
181
- execFileSync(process.execPath, ["--check", abs], {
182
- cwd: ctx.cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"],
183
- })
184
- return `Syntax OK: ${args.path}`
185
- } catch (e) {
186
- // node --check writes errors to stderr
187
- const msg = (e.stderr || e.stdout || e.message || "").trim()
188
- return `Syntax error in ${args.path}:\n${msg || "(unknown)"}`
189
- }
190
- },
191
- }
192
163
 
193
164
  // ---------------------------------------------------------------- bash
194
165
 
@@ -204,6 +175,7 @@ export const deleteTool = {
204
175
  required: ["path"],
205
176
  },
206
177
  readonly: false,
178
+ touchedPaths(args) { return args.path ? [args.path] : [] },
207
179
  async execute(args, ctx) {
208
180
  const abs = resolveInCwd(ctx, args.path)
209
181
  let s
@@ -1,3 +1,7 @@
1
1
  Read an image file and return it as multimodal content visible to the model. Use this to view screenshots, UI mockups, diagrams, or any visual content. The model only sees images through this tool — it cannot "see" files directly. Supports png, jpg, gif, webp, bmp, svg. The image is base64-encoded and included in the response. Large images (>20MB) are rejected.
2
2
 
3
- Note: this tool only works with models that support vision/image input (Kimi K3, Qwen3.7, MiniMax M3). Pure text models (DeepSeek V4, GLM-5) will receive an error.
3
+ Parameters:
4
+ - path (required): Path to image file (relative to cwd or absolute). Supports png, jpg, gif, webp, bmp, svg.
5
+
6
+ Notes:
7
+ - This tool only works with models that support vision/image input (Kimi K3, Qwen3.7, MiniMax M3). Pure text models (DeepSeek V4, GLM-5) will receive an error.
@@ -378,7 +378,7 @@ export const lsTool = {
378
378
  }),
379
379
  )
380
380
  rows.sort((a, b) => (a.dir === b.dir ? a.name.localeCompare(b.name) : a.dir ? -1 : 1))
381
- if (rows.length === 0) return "(empty directory)"
381
+ if (rows.length === 0) return "(no entries)"
382
382
  const out = rows.map((r) => `${r.dir ? "d" : "-"} ${r.name.padEnd(40)} ${String(r.size).padStart(10)} ${r.mtime}`)
383
383
  return truncate(out.join("\n"))
384
384
  },
package/src/tools/web.mjs CHANGED
@@ -77,12 +77,12 @@ export const websearchTool = {
77
77
  const engine = ENGINES.find(e => e.name === args.engine)
78
78
  if (!engine) return `Unknown engine '${args.engine}'. Available: ${ENGINE_NAMES.join(", ")}`
79
79
  const fetched = await fetchEngine(engine, args.query, page, ctx)
80
- if (!fetched || fetched.results.length === 0) return `(no results from ${engine.label})`
81
- return truncate(fetched.results.slice(0, limit).map((r, i) => `${i + 1}. ${r.title}\n ${r.href}\n ${r.snippet}`).join("\n\n"))
80
+ if (!fetched || fetched.results.length === 0) return "(no results)"
81
+ return truncate(fetched.results.slice(0, limit).map((r, i) => `${i + 1}. [${engine.label}] ${r.title}\n ${r.href}\n ${r.snippet}`).join("\n\n"))
82
82
  }
83
83
  const promises = ENGINES.map(e => fetchEngine(e, args.query, 1, ctx))
84
84
  const fetched = (await Promise.all(promises)).filter(Boolean)
85
- if (fetched.length === 0) return "(no results — all search engines failed)"
85
+ if (fetched.length === 0) return "(no results)"
86
86
  const merged = [], indexes = fetched.map(() => 0)
87
87
  let done = false
88
88
  while (!done && merged.length < limit) {