thincoder 0.8.4 → 0.8.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.
- package/bin/thincoder.mjs +7 -23
- package/package.json +1 -1
- package/src/prompts/discipline.md +2 -1
- package/src/prompts/main.md +1 -1
- package/src/tui/ansi.mjs +2 -0
- package/src/tui/cmd-upgrade.mjs +47 -0
- package/src/tui/index.mjs +63 -7
- package/src/tui/render-frame.mjs +9 -1
- package/src/tui/slash-commands.mjs +3 -0
- package/src/upgrade.mjs +41 -0
package/bin/thincoder.mjs
CHANGED
|
@@ -65,21 +65,6 @@ function exitSoon(code) {
|
|
|
65
65
|
setTimeout(() => process.exit(code), 100)
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
/** Semantic version comparison: a<b returns -1, equal 0, a>b returns 1; non-numeric segments compare as strings */
|
|
69
|
-
function compareVersions(a, b) {
|
|
70
|
-
const pa = String(a).split("."), pb = String(b).split(".")
|
|
71
|
-
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
72
|
-
const xa = pa[i] ?? "0", xb = pb[i] ?? "0"
|
|
73
|
-
const na = Number(xa), nb = Number(xb)
|
|
74
|
-
if (!Number.isNaN(na) && !Number.isNaN(nb)) {
|
|
75
|
-
if (na !== nb) return na < nb ? -1 : 1
|
|
76
|
-
} else if (xa !== xb) {
|
|
77
|
-
return xa < xb ? -1 : 1
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
return 0
|
|
81
|
-
}
|
|
82
|
-
|
|
83
68
|
switch (command) {
|
|
84
69
|
case "chat": {
|
|
85
70
|
const auto = args.includes("--auto")
|
|
@@ -270,21 +255,20 @@ switch (command) {
|
|
|
270
255
|
case "upgrade": {
|
|
271
256
|
const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"))
|
|
272
257
|
const local = pkg.version
|
|
273
|
-
const {
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
remote = execSync("npm view thincoder version", { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim()
|
|
277
|
-
} catch {
|
|
258
|
+
const { checkForUpdate } = await import("../src/upgrade.mjs")
|
|
259
|
+
const result = await checkForUpdate(local)
|
|
260
|
+
if (!result) {
|
|
278
261
|
console.error("[upgrade] Unable to query npm registry — check your network connection and that npm is installed")
|
|
279
262
|
exitSoon(1)
|
|
280
263
|
break
|
|
281
264
|
}
|
|
282
|
-
if (
|
|
265
|
+
if (!result.newer) {
|
|
283
266
|
console.log(`ThinCoder ${local} is already the latest.`)
|
|
284
267
|
} else {
|
|
285
|
-
console.log(`Upgrading: ${local} → ${
|
|
268
|
+
console.log(`Upgrading: ${local} → ${result.latest}`)
|
|
269
|
+
const { execSync } = await import("node:child_process")
|
|
286
270
|
execSync("npm install -g thincoder@latest", { stdio: "inherit" })
|
|
287
|
-
console.log(`Upgraded to ${
|
|
271
|
+
console.log(`Upgraded to ${result.latest}`)
|
|
288
272
|
}
|
|
289
273
|
break
|
|
290
274
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
Coding discipline (rigor over speed—tokens spent on verification are well spent):
|
|
2
2
|
- **Prefer built-in tools over bash for file operations**: use `ls` (not `bash ls`), `glob` (not `bash find`), `grep` (not `bash grep`). The bash tool runs the system shell — on Windows this is cmd.exe without Unix commands; on Unix it may have them but built-in tools are more reliable and platform-consistent.
|
|
3
3
|
- Spec before code: when the user describes a feature request without specifying the details (retry count? timeout? which error types? which files?), ask clarifying questions before writing code.
|
|
4
|
+
- Design docs are the spec: when the project has design documents (check with `doc_search`), read them before implementing. Their decisions represent intentional architecture — don't override them with personal habit or guesswork.
|
|
4
5
|
- Do not silently invent defaults. Do not guess the user's intent from a one-liner. A wrong assumption costs more than the round-trip to clarify.
|
|
5
6
|
- Save key design decisions to memory_put as you make them — architecture choices, API contracts, naming conventions, trade-off reasoning. Context compression may summarize earlier work into a few lines; memory entries survive compression and get re-injected so later turns don't operate on lost assumptions.
|
|
6
7
|
- Before fixing a bug, find the root cause: read the error output, reproduce it, trace the code path. Don't patch symptoms.
|
|
@@ -15,7 +16,7 @@ Coding discipline (rigor over speed—tokens spent on verification are well spen
|
|
|
15
16
|
- After changing behavior, sweep comments and docstrings that now describe the old behavior and bring them in line with the code.
|
|
16
17
|
- Before your final reply, re-read the user's latest request and confirm you are answering that one—not an earlier ask left over from a steer or compaction.
|
|
17
18
|
- After completing a batch of edits, pause and self-review:
|
|
18
|
-
1. Is
|
|
19
|
+
1. Is it correct? Does every line do exactly what it claims, with no off-by-one, no missing edge case, no silent failure?
|
|
19
20
|
2. Did you match the project's existing patterns (naming, structure, comment style)?
|
|
20
21
|
3. Did you change anything unrelated to the task? If so, explain why it was necessary.
|
|
21
22
|
4. Did the implementation match the design? Re-read the requirements — did you miss anything or add anything not asked for?
|
package/src/prompts/main.md
CHANGED
|
@@ -15,7 +15,7 @@ Load skills when relevant — project skills (.thincoder/skills/) contain reusab
|
|
|
15
15
|
**How you finish:**
|
|
16
16
|
|
|
17
17
|
After a batch of edits, pause and self-review:
|
|
18
|
-
1.
|
|
18
|
+
1. Is it correct? Every line does exactly what it claims — no off-by-one, no missing edge case, no silent failure.
|
|
19
19
|
2. Matches existing patterns?
|
|
20
20
|
3. Changed anything unrelated? If so, explain why.
|
|
21
21
|
4. Matches the design? Re-read the requirements — missed anything? Added anything not asked for?
|
package/src/tui/ansi.mjs
CHANGED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/** /upgrade command: check for updates and optionally upgrade.
|
|
2
|
+
* ctx: { agent, pushLine, pushLabel, openPicker, ansi, C } */
|
|
3
|
+
export async function handleUpgradeCommand(ctx) {
|
|
4
|
+
const { pushLine, pushLabel, openPicker, ansi, C } = ctx
|
|
5
|
+
const { checkForUpdate } = await import("../upgrade.mjs")
|
|
6
|
+
const { readFileSync } = await import("node:fs")
|
|
7
|
+
|
|
8
|
+
const pkg = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8"))
|
|
9
|
+
|
|
10
|
+
pushLabel(`❯ Upgrade`, ansi.bold + C.tool)
|
|
11
|
+
pushLine(`Checking for updates...`, C.dim)
|
|
12
|
+
const result = await checkForUpdate(pkg.version)
|
|
13
|
+
if (!result) {
|
|
14
|
+
pushLine(`Unable to query npm registry — check your network`, C.error)
|
|
15
|
+
return
|
|
16
|
+
}
|
|
17
|
+
if (!result.newer) {
|
|
18
|
+
pushLine(`✓ ThinCoder ${result.local} is already the latest.`, C.tool)
|
|
19
|
+
return
|
|
20
|
+
}
|
|
21
|
+
pushLine(`thincoder ${result.latest} is available (current: ${result.local}).`, C.tool)
|
|
22
|
+
openPicker({
|
|
23
|
+
title: `Update: ${result.local} → ${result.latest}`,
|
|
24
|
+
entries: [
|
|
25
|
+
{ type: "header", text: `New version: ${result.latest}` },
|
|
26
|
+
{ type: "item", text: "Upgrade now", action: "upgrade" },
|
|
27
|
+
{ type: "item", text: "Later", action: "later" },
|
|
28
|
+
],
|
|
29
|
+
onSelect: async (sel) => {
|
|
30
|
+
if (sel.action === "upgrade") {
|
|
31
|
+
pushLabel(`❯ Upgrade`, ansi.bold + C.tool)
|
|
32
|
+
pushLine(`Upgrading to ${result.latest}...`, C.tool)
|
|
33
|
+
const { exec } = await import("node:child_process")
|
|
34
|
+
const cp = exec("npm install -g thincoder@latest", { windowsHide: true })
|
|
35
|
+
cp.stdout?.on("data", () => {})
|
|
36
|
+
cp.stderr?.on("data", () => {})
|
|
37
|
+
cp.on("close", (code) => {
|
|
38
|
+
if (code === 0) {
|
|
39
|
+
pushLine(`✓ Upgraded to ${result.latest}. Restart to apply.`, C.tool)
|
|
40
|
+
} else {
|
|
41
|
+
pushLine(`✗ Upgrade failed (exit ${code}). Run \`thincoder upgrade\` manually.`, C.error)
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
})
|
|
47
|
+
}
|
package/src/tui/index.mjs
CHANGED
|
@@ -223,6 +223,7 @@ export async function startTUI(agent, opts = {}) {
|
|
|
223
223
|
// Frame dedup + streaming rate limit: skip re-rendering unchanged frames (prevents flicker);
|
|
224
224
|
// merge token flood to ~25fps
|
|
225
225
|
let lastFrame = ""
|
|
226
|
+
let lastCursorRow = -1, lastCursorCol = -1
|
|
226
227
|
let renderTimer = null
|
|
227
228
|
|
|
228
229
|
function scheduleRender() {
|
|
@@ -260,13 +261,23 @@ export async function startTUI(agent, opts = {}) {
|
|
|
260
261
|
})
|
|
261
262
|
if (frame !== lastFrame) {
|
|
262
263
|
lastFrame = frame
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
264
|
+
// Single write: home + hide cursor + frame + clear-tail + cursor position — prevents flicker
|
|
265
|
+
let out = ansi.home + ansi.hideCursor + frame + ansi.clearToEnd
|
|
266
|
+
if (state.permission || state.question || state.picker || state.wizard?.step === "provider") {
|
|
267
|
+
out += ansi.hideCursor
|
|
268
|
+
} else {
|
|
269
|
+
out += `\x1b[${cursorRow};${cursorCol}H${ansi.showCursor}`
|
|
270
|
+
lastCursorRow = cursorRow
|
|
271
|
+
lastCursorCol = cursorCol
|
|
272
|
+
}
|
|
273
|
+
process.stdout.write(out)
|
|
274
|
+
} else if (cursorRow !== lastCursorRow || cursorCol !== lastCursorCol) {
|
|
275
|
+
// Frame unchanged but cursor moved (e.g. arrow keys) — only reposition cursor
|
|
276
|
+
lastCursorRow = cursorRow
|
|
277
|
+
lastCursorCol = cursorCol
|
|
278
|
+
if (!(state.permission || state.question || state.picker || state.wizard?.step === "provider")) {
|
|
279
|
+
process.stdout.write(`\x1b[${cursorRow};${cursorCol}H${ansi.showCursor}`)
|
|
280
|
+
}
|
|
270
281
|
}
|
|
271
282
|
}
|
|
272
283
|
|
|
@@ -381,6 +392,51 @@ export async function startTUI(agent, opts = {}) {
|
|
|
381
392
|
|
|
382
393
|
showStartup({ agent, state, opts, pushLine, pushLabel, render, startWizard })
|
|
383
394
|
backgroundIndex({ agent, state, render })
|
|
395
|
+
|
|
396
|
+
// Check for updates (non-blocking, after startup screen)
|
|
397
|
+
;(async () => {
|
|
398
|
+
try {
|
|
399
|
+
const { readFileSync } = await import("node:fs")
|
|
400
|
+
const pkg = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8"))
|
|
401
|
+
const { checkForUpdate } = await import("../upgrade.mjs")
|
|
402
|
+
const result = await checkForUpdate(pkg.version)
|
|
403
|
+
if (result?.newer) {
|
|
404
|
+
// Defer: if wizard is still active, just show a dim line
|
|
405
|
+
if (state.wizard) {
|
|
406
|
+
pushLine(`Tip: thincoder ${result.latest} is available (run /upgrade later or restart)`, C.dim)
|
|
407
|
+
render()
|
|
408
|
+
} else {
|
|
409
|
+
openPicker({
|
|
410
|
+
title: `Update available: ${result.local} → ${result.latest}`,
|
|
411
|
+
entries: [
|
|
412
|
+
{ type: "header", text: `thincoder ${result.latest} is available (current: ${result.local})` },
|
|
413
|
+
{ type: "item", text: "Upgrade now", action: "upgrade" },
|
|
414
|
+
{ type: "item", text: "Later", action: "later" },
|
|
415
|
+
],
|
|
416
|
+
onSelect: async (sel) => {
|
|
417
|
+
if (sel.action === "upgrade") {
|
|
418
|
+
pushLabel(`❯ Upgrade`, ansi.bold + C.tool)
|
|
419
|
+
pushLine(`Upgrading to ${result.latest}...`, C.tool)
|
|
420
|
+
const { exec } = await import("node:child_process")
|
|
421
|
+
const cp = exec("npm install -g thincoder@latest", { windowsHide: true })
|
|
422
|
+
let stdout = ""
|
|
423
|
+
cp.stdout?.on("data", (d) => { stdout += d })
|
|
424
|
+
cp.stderr?.on("data", (d) => { stdout += d })
|
|
425
|
+
cp.on("close", (code) => {
|
|
426
|
+
if (code === 0) {
|
|
427
|
+
pushLine(`✓ Upgraded to ${result.latest}. Restart to apply.`, C.tool)
|
|
428
|
+
} else {
|
|
429
|
+
pushLine(`✗ Upgrade failed (exit ${code}). Run \`thincoder upgrade\` manually.`, C.error)
|
|
430
|
+
}
|
|
431
|
+
render()
|
|
432
|
+
})
|
|
433
|
+
}
|
|
434
|
+
},
|
|
435
|
+
})
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
} catch { /* network error or timeout — silently skip */ }
|
|
439
|
+
})()
|
|
384
440
|
}
|
|
385
441
|
|
|
386
442
|
function summarize(obj) {
|
package/src/tui/render-frame.mjs
CHANGED
|
@@ -194,8 +194,15 @@ export function countConvLines(state, cols) {
|
|
|
194
194
|
return buildConvLines(state, cols).length
|
|
195
195
|
}
|
|
196
196
|
|
|
197
|
-
/** Build conversation lines from state (sanitized + wrapped). Pure.
|
|
197
|
+
/** Build conversation lines from state (sanitized + wrapped). Pure.
|
|
198
|
+
* Cached: avoids O(n) rebuild on cursor moves — only recomputes when conversation grows/changes. */
|
|
199
|
+
let _convCache = { key: "", cols: 0, lines: [] }
|
|
198
200
|
function buildConvLines(state, cols) {
|
|
201
|
+
// Cheap cache key: structural hints that change whenever the conversation changes
|
|
202
|
+
const lastLine = state.lines.length > 0 ? state.lines[state.lines.length - 1] : null
|
|
203
|
+
const key = `${state.lines.length}|${lastLine?.text.length ?? 0}|${state.streaming.length}|${state.reasoning.length}|${Object.keys(state.toolStreams).length}`
|
|
204
|
+
if (_convCache.key === key && _convCache.cols === cols) return _convCache.lines
|
|
205
|
+
|
|
199
206
|
const convLines = []
|
|
200
207
|
for (const l of state.lines) {
|
|
201
208
|
for (const line of formatTables(sanitizeDisplay(l.text), cols - 1)) {
|
|
@@ -223,6 +230,7 @@ function buildConvLines(state, cols) {
|
|
|
223
230
|
convLines.push({ text: wrapped, color: C.dim })
|
|
224
231
|
}
|
|
225
232
|
}
|
|
233
|
+
_convCache = { key, cols, lines: convLines }
|
|
226
234
|
return convLines
|
|
227
235
|
}
|
|
228
236
|
|
|
@@ -26,6 +26,7 @@ import { handleModelCommand } from "./cmd-model.mjs"
|
|
|
26
26
|
import { handleConfigCommand } from "./cmd-config.mjs"
|
|
27
27
|
import { handleExtractCommand } from "./cmd-extract.mjs"
|
|
28
28
|
import { handleHelpCommand } from "./cmd-help.mjs"
|
|
29
|
+
import { handleUpgradeCommand } from "./cmd-upgrade.mjs"
|
|
29
30
|
|
|
30
31
|
export const SLASH_COMMANDS = [
|
|
31
32
|
{ name: "/plan", group: "Agent", desc: "toggle plan mode (design first, then implement)" },
|
|
@@ -34,6 +35,7 @@ export const SLASH_COMMANDS = [
|
|
|
34
35
|
{ name: "/goal", group: "Agent", desc: "set/view/cancel long-term goal" },
|
|
35
36
|
{ name: "/think", group: "Agent", desc: "thinking mode & reasoning effort" },
|
|
36
37
|
{ name: "/config", group: "Agent", desc: "config management (embedding / agent)" },
|
|
38
|
+
{ name: "/upgrade", group: "System", desc: "check for updates & upgrade" },
|
|
37
39
|
{ name: "/new", group: "Session", desc: "new session (old one archived to slot)" },
|
|
38
40
|
{ name: "/session", group: "Session", desc: "list/switch archived sessions" },
|
|
39
41
|
{ name: "/clear", group: "Session", desc: "clear screen" },
|
|
@@ -64,6 +66,7 @@ const HANDLERS = {
|
|
|
64
66
|
"/think": handleThinkCommand,
|
|
65
67
|
"/model": handleModelCommand,
|
|
66
68
|
"/config": handleConfigCommand,
|
|
69
|
+
"/upgrade": handleUpgradeCommand,
|
|
67
70
|
"/extract": handleExtractCommand,
|
|
68
71
|
"/help": handleHelpCommand,
|
|
69
72
|
}
|
package/src/upgrade.mjs
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* upgrade.mjs — version check and upgrade utilities
|
|
3
|
+
* Used by both CLI (bin/thincoder.mjs upgrade command) and TUI (startup check).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** Compare two semver-like version strings. Returns -1/0/1. */
|
|
7
|
+
export function compareVersions(a, b) {
|
|
8
|
+
const pa = String(a).split("."), pb = String(b).split(".")
|
|
9
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
10
|
+
const xa = pa[i] ?? "0", xb = pb[i] ?? "0"
|
|
11
|
+
const na = Number(xa), nb = Number(xb)
|
|
12
|
+
if (!Number.isNaN(na) && !Number.isNaN(nb)) {
|
|
13
|
+
if (na !== nb) return na < nb ? -1 : 1
|
|
14
|
+
} else if (xa !== xb) {
|
|
15
|
+
return xa < xb ? -1 : 1
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return 0
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Check npm registry for the latest version.
|
|
23
|
+
* Returns { local, latest, newer: boolean } or null on network error / timeout.
|
|
24
|
+
*/
|
|
25
|
+
export async function checkForUpdate(localVersion) {
|
|
26
|
+
try {
|
|
27
|
+
const res = await fetch("https://registry.npmjs.org/thincoder/latest", {
|
|
28
|
+
signal: AbortSignal.timeout(5000),
|
|
29
|
+
})
|
|
30
|
+
if (!res.ok) return null
|
|
31
|
+
const data = await res.json()
|
|
32
|
+
const latest = data.version
|
|
33
|
+
return {
|
|
34
|
+
local: localVersion,
|
|
35
|
+
latest,
|
|
36
|
+
newer: compareVersions(localVersion, latest) < 0,
|
|
37
|
+
}
|
|
38
|
+
} catch {
|
|
39
|
+
return null
|
|
40
|
+
}
|
|
41
|
+
}
|