@zerotimedev/thinker 0.0.0-stage → 0.1.37
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/LICENSE +21 -0
- package/README.md +14 -2
- package/install.mjs +24 -0
- package/install.sh +410 -0
- package/package.json +20 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Yoav Shmariahu
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @zerotimedev/thinker
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Installs [thinker](https://github.com/yoavshmariahu/thinker), a knowledge cache for coding agents.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npx @zerotimedev/thinker
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
The package carries thinker's installer and runs it. It downloads the latest thinker release, verifies it
|
|
10
|
+
against the release's sha256, installs it into `~/.thinker`, wires it into the coding agents on
|
|
11
|
+
the machine and opens the warm-up page. The installer's options pass through, for example
|
|
12
|
+
`npx @zerotimedev/thinker --uninstall`.
|
|
13
|
+
|
|
14
|
+
Requires bash, git, curl, tar and Node 20+. After the install, `thinker update` and the daily
|
|
15
|
+
auto-update keep the tool current; npm is not involved again.
|
package/install.mjs
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// npx @zerotimedev/thinker: runs the installer this package carries (install.sh), with the
|
|
3
|
+
// arguments passed through. Nothing of thinker runs from
|
|
4
|
+
// the npx cache, which npm may clear: the installer puts the tool in THINKER_HOME (~/.thinker),
|
|
5
|
+
// where the hooks and `thinker update` find it. scripts/pack.sh copies install.sh in beside this
|
|
6
|
+
// file and sets the version; the package is not meant to be installed as a dependency.
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import { spawnSync } from 'node:child_process';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
|
|
12
|
+
const script = path.join(path.dirname(fileURLToPath(import.meta.url)), 'install.sh');
|
|
13
|
+
const die = (msg) => { process.stderr.write(`thinker: ${msg}\n`); process.exit(1); };
|
|
14
|
+
if (!fs.existsSync(script)) die(`the installer is missing from this package (${script})`);
|
|
15
|
+
|
|
16
|
+
const r = spawnSync('bash', [script, ...process.argv.slice(2)], { stdio: 'inherit' });
|
|
17
|
+
if (r.error?.code === 'ENOENT') {
|
|
18
|
+
die(process.platform === 'win32'
|
|
19
|
+
? 'the installer is a bash script: run this from WSL or Git Bash'
|
|
20
|
+
: 'bash is required to run the installer');
|
|
21
|
+
}
|
|
22
|
+
if (r.error) die(r.error.message);
|
|
23
|
+
if (r.signal) process.kill(process.pid, r.signal);
|
|
24
|
+
process.exit(r.status ?? 1);
|
package/install.sh
ADDED
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# thinker install: installs the thinker tool and wires it into the coding agents on
|
|
3
|
+
# this machine (their own settings: hooks and the MCP server, once). Thinker then
|
|
4
|
+
# runs in every git repository on this machine: a cache starts with the first
|
|
5
|
+
# session in a repository, and the warm-up page that opens at the end (thinker ui)
|
|
6
|
+
# builds caches for the repositories your agents have worked in, from their merged
|
|
7
|
+
# pull requests. Nothing is asked in the terminal.
|
|
8
|
+
#
|
|
9
|
+
# Thinker is open source; no access code or GitHub token is required. It installs the latest
|
|
10
|
+
# GitHub release (https://github.com/yoavshmariahu/thinker/releases: a tag vX.Y.Z carrying
|
|
11
|
+
# thinker.tgz, version.json and install.sh), verified against the sha256 in version.json.
|
|
12
|
+
#
|
|
13
|
+
# npx @zerotimedev/thinker
|
|
14
|
+
#
|
|
15
|
+
# The npm package of that name carries this file and runs it with the arguments passed through.
|
|
16
|
+
#
|
|
17
|
+
# --build from inside a repository builds its cache right here, in the terminal,
|
|
18
|
+
# instead of opening the page. If a cache was already built for the repository,
|
|
19
|
+
# pass --cache instead and nothing has to be built.
|
|
20
|
+
#
|
|
21
|
+
# Options
|
|
22
|
+
# --build build the cache of this repository now, in the terminal (the page is not opened)
|
|
23
|
+
# --no-build accepted from old scripts: nothing is built without --build anyway
|
|
24
|
+
# --no-seed, --areas <n> accepted from old scripts and ignored: a build no longer explores the code
|
|
25
|
+
# --no-behaviors accepted from old scripts and ignored: there is no behaviors prompt to print
|
|
26
|
+
# --depth <d> with --build: full (default) or shallow, 30% of the full build
|
|
27
|
+
# --prs <n> with --build: merged pull requests to mine (default 60; skipped without the gh CLI)
|
|
28
|
+
# --pr <number> specific PR number to target for the paired benchmark
|
|
29
|
+
# --benchmark run paired PR benchmark during setup
|
|
30
|
+
# --no-benchmark skip the paired PR benchmark step
|
|
31
|
+
# -y, --yes accept defaults and skip interactive confirmation prompts
|
|
32
|
+
# --clients <list> coding agents to wire up: claude, codex, cursor, gemini, pi, windsurf, copilot, opencode, all or auto
|
|
33
|
+
# (default auto)
|
|
34
|
+
# --cache <source> cache built for this repo. One of: gh:<path in the thinker repo>, an https URL, a local file.
|
|
35
|
+
# Omit if .thinker/notes is already in the repo.
|
|
36
|
+
# --no-learn do not distill your own sessions into new notes (learning is on by default, for any of the
|
|
37
|
+
# agents, and uses that agent's login; switch it off for evals)
|
|
38
|
+
# --late also serve notes about files as the agent opens them
|
|
39
|
+
# --mcp accepted for compatibility: the MCP server is always registered (needs npm)
|
|
40
|
+
# --no-git-hook do not install the git post-commit hook (it re-checks and maintains notes after each commit)
|
|
41
|
+
# --ref <tag> release to install, a tag such as v0.1.34 (default latest; --branch also accepted)
|
|
42
|
+
# --update update an installed thinker to the latest release and exit
|
|
43
|
+
# --no-auto-update do not schedule daily background auto-updates
|
|
44
|
+
# --no-modify-path do not add THINKER_HOME/bin to PATH in your shell's startup file
|
|
45
|
+
# --uninstall remove hooks and registration from this repo (add --purge to delete notes too); run outside
|
|
46
|
+
# a repository, it removes the wiring from your agents' own settings
|
|
47
|
+
#
|
|
48
|
+
# Environment
|
|
49
|
+
# THINKER_HOME where the tool is installed (default ~/.thinker)
|
|
50
|
+
# THINKER_GH_REPO GitHub repository whose releases to install from (default yoavshmariahu/thinker)
|
|
51
|
+
# THINKER_REF release tag (default latest)
|
|
52
|
+
# THINKER_DIST_URL install from this tarball URL (with version.json beside it) instead of a GitHub release
|
|
53
|
+
# GITHUB_TOKEN only for --cache gh:<path>: a token for the GitHub API (GH_TOKEN works too;
|
|
54
|
+
# if neither is set and the GitHub CLI is logged in, its token is used)
|
|
55
|
+
#
|
|
56
|
+
# The script writes to THINKER_HOME, to .thinker/ and the agents' config in this repository, and adds one
|
|
57
|
+
# line putting THINKER_HOME/bin on PATH to your shell's startup file (--no-modify-path leaves it alone). No sudo.
|
|
58
|
+
set -euo pipefail
|
|
59
|
+
export COPYFILE_DISABLE=1
|
|
60
|
+
export COPY_EXTENDED_ATTRIBUTES_DISABLE=1
|
|
61
|
+
|
|
62
|
+
# add_to_path <dir>: put <dir> on PATH in the startup file of the user's shell, once.
|
|
63
|
+
# Prints the file it wrote to, or nothing when it could not tell which one.
|
|
64
|
+
add_to_path() {
|
|
65
|
+
local dir="$1" rc line
|
|
66
|
+
case "$(basename "${SHELL:-}")" in
|
|
67
|
+
zsh) rc="${ZDOTDIR:-$HOME}/.zshrc"; line="export PATH=\"$dir:\$PATH\"" ;;
|
|
68
|
+
bash) # macOS terminals start login shells, which read .bash_profile and not .bashrc
|
|
69
|
+
if [ "$(uname)" = Darwin ]; then rc="$HOME/.bash_profile"; else rc="$HOME/.bashrc"; fi
|
|
70
|
+
line="export PATH=\"$dir:\$PATH\"" ;;
|
|
71
|
+
fish) rc="${XDG_CONFIG_HOME:-$HOME/.config}/fish/conf.d/thinker.fish"; line="fish_add_path \"$dir\"" ;;
|
|
72
|
+
*) return 0 ;;
|
|
73
|
+
esac
|
|
74
|
+
if ! grep -qsF "$line" "$rc"; then
|
|
75
|
+
mkdir -p "$(dirname "$rc")" && printf '\n# thinker\n%s\n' "$line" >> "$rc" || return 0
|
|
76
|
+
fi
|
|
77
|
+
printf '%s' "$rc"
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
# path_hint: how to reach `thinker` from this shell; reads main's $home and $rcfile
|
|
81
|
+
path_hint() {
|
|
82
|
+
case ":$PATH:" in
|
|
83
|
+
*":$home/bin:"*) ;;
|
|
84
|
+
*) if [ -n "$rcfile" ]; then say " 'thinker' is on your PATH in new terminals (added to $rcfile); in this one run: export PATH=\"$home/bin:\$PATH\""
|
|
85
|
+
else say " Optional: add $home/bin to your PATH to run 'thinker' directly."; fi ;;
|
|
86
|
+
esac
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
main() {
|
|
90
|
+
local cache="" build="" areas="" depth="" prs="" clients="" learn=1 late=0 mcp=0 githook=0 uninstall=0 purge=0 update=0 autoupdate=1 modpath=1 ref="${THINKER_REF:-latest}" benchmark="" pr_target="" yes=0 no_seed=0
|
|
91
|
+
while [ $# -gt 0 ]; do
|
|
92
|
+
case "$1" in
|
|
93
|
+
--cache) cache="${2:-}"; build=0; shift 2 ;;
|
|
94
|
+
--build) build=1; shift ;;
|
|
95
|
+
--no-build) build=0; shift ;;
|
|
96
|
+
--no-seed) shift ;;
|
|
97
|
+
--no-behaviors) shift ;;
|
|
98
|
+
-y|--yes) yes=1; shift ;;
|
|
99
|
+
--areas) shift 2 ;;
|
|
100
|
+
--depth) depth="${2:-}"; shift 2 ;;
|
|
101
|
+
--prs) prs="${2:-}"; shift 2 ;;
|
|
102
|
+
--pr) pr_target="${2:-}"; shift 2 ;;
|
|
103
|
+
--benchmark) benchmark=1; shift ;;
|
|
104
|
+
--no-benchmark) benchmark=0; shift ;;
|
|
105
|
+
--clients) clients="${2:-}"; shift 2 ;;
|
|
106
|
+
--branch|--ref) ref="${2:-}"; shift 2 ;;
|
|
107
|
+
--learn) learn=1; shift ;;
|
|
108
|
+
--no-learn) learn=0; shift ;;
|
|
109
|
+
--late) late=1; shift ;;
|
|
110
|
+
--shared) echo "Team sharing is no longer supported." >&2; return 1 ;;
|
|
111
|
+
--mcp) mcp=1; shift ;;
|
|
112
|
+
--git-hook) githook=1; shift ;;
|
|
113
|
+
--no-git-hook) githook=0; shift ;;
|
|
114
|
+
--update) update=1; shift ;;
|
|
115
|
+
--auto-update) autoupdate=1; shift ;;
|
|
116
|
+
--no-auto-update) autoupdate=0; shift ;;
|
|
117
|
+
--no-modify-path) modpath=0; shift ;;
|
|
118
|
+
--uninstall) uninstall=1; shift ;;
|
|
119
|
+
--purge) purge=1; shift ;;
|
|
120
|
+
-h|--help) echo "see the header of install.sh or ONBOARDING.md for options"; exit 0 ;;
|
|
121
|
+
*) echo "unknown option: $1" >&2; exit 2 ;;
|
|
122
|
+
esac
|
|
123
|
+
done
|
|
124
|
+
|
|
125
|
+
local home="${THINKER_HOME:-$HOME/.thinker}"
|
|
126
|
+
local ghrepo="${THINKER_GH_REPO:-yoavshmariahu/thinker}"
|
|
127
|
+
local ref="${ref:-${THINKER_REF:-latest}}"
|
|
128
|
+
local dist="${THINKER_DIST_URL:-}"
|
|
129
|
+
# the GitHub release for the ref: the latest one, or the tag named
|
|
130
|
+
if [ -z "$dist" ]; then
|
|
131
|
+
case "$ref" in
|
|
132
|
+
latest|main) dist="https://github.com/$ghrepo/releases/latest/download/thinker.tgz" ;;
|
|
133
|
+
*) dist="https://github.com/$ghrepo/releases/download/$ref/thinker.tgz" ;;
|
|
134
|
+
esac
|
|
135
|
+
fi
|
|
136
|
+
local token="${GITHUB_TOKEN:-${GH_TOKEN:-}}"
|
|
137
|
+
say() { printf '%s\n' "$*"; }
|
|
138
|
+
die() { printf 'thinker: %s\n' "$*" >&2; exit 1; }
|
|
139
|
+
# --- look: colors, boxes and spinners on a terminal, plain lines anywhere else --------------
|
|
140
|
+
local fancy=0 bold="" dim="" cyan="" green="" yellow="" red="" magenta="" reset=""
|
|
141
|
+
if [ -t 1 ] && [ -z "${NO_COLOR:-}" ] && [ "${TERM:-}" != dumb ] && [ -z "${CI:-}" ]; then
|
|
142
|
+
fancy=1 bold=$'\033[1m' dim=$'\033[2m' cyan=$'\033[36m' green=$'\033[32m' yellow=$'\033[33m' red=$'\033[31m' magenta=$'\033[35m' reset=$'\033[0m'
|
|
143
|
+
fi
|
|
144
|
+
# box <color> <line>...: a rounded box 74 columns wide, as setup draws it; a line may carry color codes, padded on its plain text
|
|
145
|
+
# box <color> <line>...: the lines in a rounded frame, at least 68 columns wide and as wide as the
|
|
146
|
+
# longest line (a path can run past 68, and a frame narrower than its text comes out ragged)
|
|
147
|
+
box() {
|
|
148
|
+
local color="$1"; shift
|
|
149
|
+
local width=68 line plain
|
|
150
|
+
for line in "$@"; do
|
|
151
|
+
plain="$(printf '%s' "$line" | sed $'s/\033\\[[0-9;]*m//g')"
|
|
152
|
+
[ "${#plain}" -gt "$width" ] && width=${#plain}
|
|
153
|
+
done
|
|
154
|
+
printf '%s╭%s╮%s\n' "$color" "$(printf '─%.0s' $(seq 1 $((width + 4))))" "$reset"
|
|
155
|
+
for line in "$@"; do
|
|
156
|
+
plain="$(printf '%s' "$line" | sed $'s/\033\\[[0-9;]*m//g')"
|
|
157
|
+
printf '%s│%s %s%*s %s│%s\n' "$color" "$reset" "$line" $((width - ${#plain})) "" "$color" "$reset"
|
|
158
|
+
done
|
|
159
|
+
printf '%s╰%s╯%s\n' "$color" "$(printf '─%.0s' $(seq 1 $((width + 4))))" "$reset"
|
|
160
|
+
}
|
|
161
|
+
# spin <label> <command...>: runs the command with a turning frame and the time so far, then a ✓ or ✗
|
|
162
|
+
# line; its output is kept and shown only if it fails. Off a terminal: the label, then the command.
|
|
163
|
+
spin() {
|
|
164
|
+
local label="$1"; shift
|
|
165
|
+
if [ "$fancy" != 1 ]; then say " ${label}…"; "$@"; return; fi
|
|
166
|
+
local log; log="$(mktemp)"
|
|
167
|
+
"$@" >"$log" 2>&1 &
|
|
168
|
+
local pid=$! i=0 start=$SECONDS code=0
|
|
169
|
+
local frames=(⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏)
|
|
170
|
+
while kill -0 "$pid" 2>/dev/null; do
|
|
171
|
+
printf '\r\033[2K %s%s%s %s %s· %ss%s' "$cyan" "${frames[$((i % 10))]}" "$reset" "$label" "$dim" "$((SECONDS - start))" "$reset"
|
|
172
|
+
i=$((i + 1)); sleep 0.1
|
|
173
|
+
done
|
|
174
|
+
wait "$pid" || code=$?
|
|
175
|
+
if [ "$code" -eq 0 ]; then
|
|
176
|
+
printf '\r\033[2K %s✓%s %s %s· %ss%s\n' "$green" "$reset" "$label" "$dim" "$((SECONDS - start))" "$reset"
|
|
177
|
+
else
|
|
178
|
+
printf '\r\033[2K %s✗%s %s\n' "$red" "$reset" "$label"
|
|
179
|
+
cat "$log" >&2
|
|
180
|
+
fi
|
|
181
|
+
rm -f "$log"
|
|
182
|
+
return "$code"
|
|
183
|
+
}
|
|
184
|
+
# the GitHub CLI is optional: it is only asked for a token when none is set
|
|
185
|
+
if [ -z "$token" ] && command -v gh >/dev/null; then token="$(gh auth token 2>/dev/null || true)"; fi
|
|
186
|
+
# gh_fetch <api endpoint in the thinker repo> <output file>
|
|
187
|
+
gh_fetch() {
|
|
188
|
+
local url="https://api.github.com/$1" out="$2"
|
|
189
|
+
if [ -n "$token" ]; then
|
|
190
|
+
curl -fsSL -H "Authorization: Bearer $token" -H "Accept: application/vnd.github.raw" -o "$out" "$url" && return 0
|
|
191
|
+
die "could not download $url with the token provided (does it have read access to $ghrepo?)"
|
|
192
|
+
fi
|
|
193
|
+
curl -fsSL -H "Accept: application/vnd.github.raw" -o "$out" "$url" 2>/dev/null && return 0
|
|
194
|
+
die "could not download $url: check connectivity and the repository/ref, or set GITHUB_TOKEN if GitHub rate-limits the request"
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
# --- prerequisites -----------------------------------------------------
|
|
198
|
+
command -v git >/dev/null || die "git is required"
|
|
199
|
+
command -v node >/dev/null || die "Node.js 20 or newer is required (https://nodejs.org)"
|
|
200
|
+
command -v curl >/dev/null || die "curl is required"
|
|
201
|
+
command -v tar >/dev/null || die "tar is required"
|
|
202
|
+
local nodemajor; nodemajor="$(node -p 'process.versions.node.split(".")[0]')"
|
|
203
|
+
[ "$nodemajor" -ge 20 ] || die "Node.js 20 or newer is required (found $(node -v))"
|
|
204
|
+
# outside a git repository only the tool is installed; `thinker setup` sets a repository up later
|
|
205
|
+
local repo; repo="$(git rev-parse --show-toplevel 2>/dev/null)" || repo=""
|
|
206
|
+
[ -n "$repo" ] && cd "$repo"
|
|
207
|
+
[ -n "$cache" ] && [ -z "$repo" ] && die "--cache needs a repository to put the cache in: run this from inside it"
|
|
208
|
+
|
|
209
|
+
local thinker="$home/bin/thinker"
|
|
210
|
+
|
|
211
|
+
if [ "$uninstall" = 1 ]; then
|
|
212
|
+
[ -x "$thinker" ] || die "thinker is not installed in $home"
|
|
213
|
+
if [ -z "$repo" ]; then "$thinker" uninstall --user
|
|
214
|
+
elif [ "$purge" = 1 ]; then "$thinker" uninstall --purge --repo "$repo"; else "$thinker" uninstall --repo "$repo"; fi
|
|
215
|
+
say "To remove the tool itself: rm -rf \"$home\", and delete the '# thinker' PATH line from your shell's startup file"
|
|
216
|
+
exit 0
|
|
217
|
+
fi
|
|
218
|
+
|
|
219
|
+
if [ "$update" = 1 ]; then
|
|
220
|
+
[ -x "$thinker" ] || die "thinker is not installed in $home"
|
|
221
|
+
fi
|
|
222
|
+
|
|
223
|
+
# --- install the tool ----------------------------------------------------
|
|
224
|
+
local tmp; tmp="$(mktemp -d)"
|
|
225
|
+
# expand now: the variable is local and gone by the time the trap runs
|
|
226
|
+
# shellcheck disable=SC2064
|
|
227
|
+
trap "rm -rf '$tmp'" EXIT
|
|
228
|
+
say ""
|
|
229
|
+
if [ "$fancy" = 1 ]; then
|
|
230
|
+
local sparkle="${yellow}*${reset} ${magenta}~${reset} ${yellow}*${reset}"
|
|
231
|
+
box "$cyan" "$sparkle ${bold}${cyan}Thinker${reset} $sparkle" "${bold}A knowledge cache for coding & review agents${reset}" \
|
|
232
|
+
"${magenta}~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~${reset}" "" \
|
|
233
|
+
"${dim}Learns from your merged fixes, flags the change that would undo one,${reset}" \
|
|
234
|
+
"${dim}and hands your coding agents what the repository already knows.${reset}"
|
|
235
|
+
else
|
|
236
|
+
say "Thinker"
|
|
237
|
+
say "A knowledge cache for coding & review agents"
|
|
238
|
+
fi
|
|
239
|
+
say ""
|
|
240
|
+
case "$dist" in https://*) ;; *) die "releases must be downloaded over HTTPS" ;; esac
|
|
241
|
+
# GitHub answers with a redirect to its asset host; curl follows it over HTTPS only
|
|
242
|
+
download_release() {
|
|
243
|
+
curl -fsSL --proto-redir '=https' -o "$tmp/thinker.tgz" "$dist" &&
|
|
244
|
+
curl -fsSL --proto-redir '=https' -o "$tmp/version.json" "${dist%/*}/version.json"
|
|
245
|
+
}
|
|
246
|
+
spin "Downloading the release" download_release || die "could not download $dist or its version.json (is there a release for $ref at https://github.com/$ghrepo/releases?)"
|
|
247
|
+
say " Verifying download…"
|
|
248
|
+
node --input-type=module - "$tmp/thinker.tgz" "$tmp/version.json" <<'JS'
|
|
249
|
+
import fs from 'node:fs';
|
|
250
|
+
import crypto from 'node:crypto';
|
|
251
|
+
const [archive, manifest] = process.argv.slice(2);
|
|
252
|
+
const release = JSON.parse(fs.readFileSync(manifest, 'utf8'));
|
|
253
|
+
if (release.schemaVersion !== 1 || !/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(release.version || '') ||
|
|
254
|
+
!/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(release.commit || '') || !/^[a-f0-9]{64}$/.test(release.sha256 || '')) {
|
|
255
|
+
throw new Error('invalid or unsupported release manifest');
|
|
256
|
+
}
|
|
257
|
+
const actual = crypto.createHash('sha256').update(fs.readFileSync(archive)).digest('hex');
|
|
258
|
+
if (actual !== release.sha256) throw new Error('release checksum mismatch; refusing to extract');
|
|
259
|
+
JS
|
|
260
|
+
tar_extract() {
|
|
261
|
+
local archive="$1" dest="$2"
|
|
262
|
+
local warn_opt=""
|
|
263
|
+
if tar --warning=no-unknown-keyword --version >/dev/null 2>&1; then
|
|
264
|
+
warn_opt="--warning=no-unknown-keyword"
|
|
265
|
+
fi
|
|
266
|
+
local code=0
|
|
267
|
+
local err
|
|
268
|
+
# shellcheck disable=SC2086
|
|
269
|
+
err="$(COPYFILE_DISABLE=1 COPY_EXTENDED_ATTRIBUTES_DISABLE=1 tar ${warn_opt} -xzf "$archive" -C "$dest" 2>&1)" || code=$?
|
|
270
|
+
if [ -n "$err" ]; then
|
|
271
|
+
local filtered
|
|
272
|
+
filtered="$(printf '%s\n' "$err" | grep -v "Ignoring unknown extended header keyword" || true)"
|
|
273
|
+
if [ -n "$filtered" ]; then
|
|
274
|
+
printf '%s\n' "$filtered" >&2
|
|
275
|
+
fi
|
|
276
|
+
fi
|
|
277
|
+
[ "$code" -eq 0 ] || return "$code"
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
say " Installing CLI…"
|
|
281
|
+
mkdir -p "$tmp/app" && tar_extract "$tmp/thinker.tgz" "$tmp/app"
|
|
282
|
+
# GitHub archives wrap everything in one top-level directory
|
|
283
|
+
if [ ! -f "$tmp/app/src/cli.js" ]; then
|
|
284
|
+
local inner; inner="$(find "$tmp/app" -mindepth 1 -maxdepth 1 -type d | head -1)"
|
|
285
|
+
[ -n "$inner" ] && [ -f "$inner/src/cli.js" ] || die "downloaded archive does not look like thinker"
|
|
286
|
+
mv "$inner" "$tmp/app.inner" && rm -rf "$tmp/app" && mv "$tmp/app.inner" "$tmp/app"
|
|
287
|
+
fi
|
|
288
|
+
node --input-type=module - "$tmp/app/package.json" "$tmp/version.json" <<'JS'
|
|
289
|
+
import fs from 'node:fs';
|
|
290
|
+
const pkg = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
|
|
291
|
+
const release = JSON.parse(fs.readFileSync(process.argv[3], 'utf8'));
|
|
292
|
+
if (pkg.version !== release.version) throw new Error(`release version ${release.version} does not match archive version ${pkg.version}`);
|
|
293
|
+
JS
|
|
294
|
+
# --update: the downloaded copy's updater installs itself, so an older install (one that still
|
|
295
|
+
# downloads from the old host, say) moves to the releases. The home and its settings are kept.
|
|
296
|
+
if [ "$update" = 1 ]; then
|
|
297
|
+
node --input-type=module - "$tmp/app/src/update.js" "$home" "$dist" "$ref" <<'JS'
|
|
298
|
+
import path from 'node:path';
|
|
299
|
+
import { pathToFileURL } from 'node:url';
|
|
300
|
+
const [modulePath, home, dist, ref] = process.argv.slice(2);
|
|
301
|
+
const { applyUpdate } = await import(pathToFileURL(modulePath));
|
|
302
|
+
const result = await applyUpdate({ home, rootDir: path.join(home, 'app'), dist, ref });
|
|
303
|
+
console.log(`Updated thinker to v${result.version}`);
|
|
304
|
+
JS
|
|
305
|
+
exit 0
|
|
306
|
+
fi
|
|
307
|
+
# the tool needs only its source and manifest at runtime
|
|
308
|
+
rm -rf "$tmp/app/bench" "$tmp/app/test" "$tmp/app/caches"
|
|
309
|
+
mkdir -p "$home/bin"
|
|
310
|
+
rm -rf "$home/app.new" && mv "$tmp/app" "$home/app.new"
|
|
311
|
+
rm -rf "$home/app.old"; [ -d "$home/app" ] && mv "$home/app" "$home/app.old"
|
|
312
|
+
mv "$home/app.new" "$home/app" && rm -rf "$home/app.old"
|
|
313
|
+
cat > "$thinker" <<SHIM
|
|
314
|
+
#!/bin/sh
|
|
315
|
+
exec node "$home/app/src/cli.js" "\$@"
|
|
316
|
+
SHIM
|
|
317
|
+
chmod +x "$thinker"
|
|
318
|
+
cat > "$home/install.json" <<EOF
|
|
319
|
+
{
|
|
320
|
+
"source": "archive",
|
|
321
|
+
"ghrepo": "$ghrepo",
|
|
322
|
+
"ref": "$ref",
|
|
323
|
+
"dist": "$dist",
|
|
324
|
+
"installedAt": "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
325
|
+
}
|
|
326
|
+
EOF
|
|
327
|
+
if [ "$autoupdate" = 1 ]; then
|
|
328
|
+
"$thinker" update --schedule --quiet 2>/dev/null || true
|
|
329
|
+
fi
|
|
330
|
+
local rcfile=""
|
|
331
|
+
if [ "$modpath" = 1 ]; then rcfile="$(add_to_path "$home/bin")"; fi
|
|
332
|
+
|
|
333
|
+
# whatever agents are on this machine are wired up, in their own settings
|
|
334
|
+
[ -z "$clients" ] && clients="auto"
|
|
335
|
+
# The dependencies are always installed: the hooks rank notes with a local cross-encoder whose runtime is one
|
|
336
|
+
# of them, and the MCP server, registered for every agent, needs its SDK. Without npm the hooks still work,
|
|
337
|
+
# ranking by words alone, and the MCP server is left out (thinker connect registers it once npm ci has run).
|
|
338
|
+
if command -v npm >/dev/null; then
|
|
339
|
+
mcp=1
|
|
340
|
+
install_deps() { cd "$home/app" && npm ci --omit=dev --ignore-scripts --no-audit --no-fund --silent; }
|
|
341
|
+
spin "Installing dependencies (the ranking runtime is most of it)" install_deps || die "npm ci failed in $home/app"
|
|
342
|
+
spin "Fetching the ranking model" "$thinker" ranker fetch --quiet || say " The ranking model could not be fetched (offline?); notes are ranked by words until 'thinker ranker fetch' succeeds."
|
|
343
|
+
else
|
|
344
|
+
say "npm was not found: the dependencies were not installed (cd \"$home/app\" && npm ci --omit=dev --ignore-scripts, then thinker ranker fetch and thinker connect). Notes are ranked by words alone until then."
|
|
345
|
+
mcp=0
|
|
346
|
+
fi
|
|
347
|
+
|
|
348
|
+
# --- wire the agents, once, in their own settings ---------------------------
|
|
349
|
+
# Nothing is asked here: Codex's hook-review marks are written outright, and every other choice
|
|
350
|
+
# (which repositories to build a cache for, how deep, with which model) is made on the warm-up
|
|
351
|
+
# page that opens next. Thinker then runs in every git repository on this machine.
|
|
352
|
+
local cargs="--clients $clients --yes"
|
|
353
|
+
[ "$learn" = 1 ] || cargs="$cargs --no-learn"
|
|
354
|
+
[ "$late" = 1 ] && cargs="$cargs --late"
|
|
355
|
+
[ "$mcp" = 1 ] || cargs="$cargs --no-mcp"
|
|
356
|
+
# shellcheck disable=SC2086
|
|
357
|
+
"$thinker" connect $cargs
|
|
358
|
+
|
|
359
|
+
# --- a cache handed over, or a build asked for by flag -------------------------
|
|
360
|
+
if [ -n "$cache" ]; then
|
|
361
|
+
case "$cache" in
|
|
362
|
+
gh:*) gh_fetch "repos/$ghrepo/contents/${cache#gh:}?ref=$ref" "$tmp/cache.tgz"; "$thinker" import "$tmp/cache.tgz" --repo "$repo" ;;
|
|
363
|
+
*) "$thinker" import "$cache" --repo "$repo" ;;
|
|
364
|
+
esac
|
|
365
|
+
elif [ "$build" = 1 ] && [ -n "$repo" ]; then
|
|
366
|
+
# --build from inside a repository: the build runs here, without the page, as before
|
|
367
|
+
local args="--build --yes --no-ui"
|
|
368
|
+
[ -n "$clients" ] && args="$args --clients $clients"
|
|
369
|
+
[ "$learn" = 1 ] || args="$args --no-learn"
|
|
370
|
+
[ "$githook" = 0 ] && args="$args --no-git-hook"
|
|
371
|
+
[ "$mcp" = 1 ] || args="$args --no-mcp"
|
|
372
|
+
[ -n "$depth" ] && args="$args --depth $depth"
|
|
373
|
+
[ -n "$prs" ] && args="$args --prs $prs"
|
|
374
|
+
[ -n "$pr_target" ] && args="$args --pr $pr_target"
|
|
375
|
+
[ "$benchmark" = 1 ] && args="$args --benchmark"
|
|
376
|
+
[ "$benchmark" = 0 ] && args="$args --no-benchmark"
|
|
377
|
+
# shellcheck disable=SC2086
|
|
378
|
+
THINKER_INSTALLER=1 "$thinker" setup $args --repo "$repo"
|
|
379
|
+
fi
|
|
380
|
+
|
|
381
|
+
# --- the warm-up page -----------------------------------------------------------
|
|
382
|
+
# The page lists the repositories your agents have worked in; pick the ones to build a cache for.
|
|
383
|
+
# Opened detached (the server exits on its own once idle); not from CI or a pipe with no terminal.
|
|
384
|
+
local opened=0
|
|
385
|
+
if [ -z "${CI:-}" ] && [ -t 1 ] && [ "$build" != 1 ]; then
|
|
386
|
+
( "$thinker" ui --from-setup --tab warmup >/dev/null 2>&1 & ) && opened=1
|
|
387
|
+
fi
|
|
388
|
+
local version; version="$(node -p "require('$home/app/package.json').version" 2>/dev/null || echo '?')"
|
|
389
|
+
path_hint
|
|
390
|
+
say ""
|
|
391
|
+
local how="Pick the repositories to warm up on the page that just opened"
|
|
392
|
+
[ "$opened" = 1 ] || how="Pick the repositories to warm up: ${cyan}thinker ui${reset}"
|
|
393
|
+
if [ "$fancy" = 1 ]; then
|
|
394
|
+
box "$green" "${green}✓${reset} ${bold}Install complete${reset} ${dim}· Thinker v$version${reset}" "" \
|
|
395
|
+
"Installed into ${home/#$HOME/~} and wired into your agents." \
|
|
396
|
+
"Thinker now runs in every git repository on this machine." \
|
|
397
|
+
"$how" \
|
|
398
|
+
"${dim}(open it again any time with thinker ui).${reset}" \
|
|
399
|
+
"${dim}Notes stay on this machine; Thinker uploads none.${reset}" "" \
|
|
400
|
+
"${yellow}*${reset} ${magenta}~${reset} ${yellow}*${reset} ${bold}all done · happy shipping${reset} ${yellow}*${reset} ${magenta}~${reset} ${yellow}*${reset}"
|
|
401
|
+
else
|
|
402
|
+
say "Install complete: Thinker v$version, installed into $home and wired into your agents."
|
|
403
|
+
say "Thinker now runs in every git repository on this machine."
|
|
404
|
+
if [ "$opened" = 1 ]; then say "Pick the repositories to warm up on the page that just opened (open it again any time with thinker ui)."
|
|
405
|
+
else say "Pick the repositories to warm up: thinker ui"; fi
|
|
406
|
+
say "Notes stay on this machine; Thinker uploads none."
|
|
407
|
+
fi
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
main "$@"
|
package/package.json
CHANGED
|
@@ -1,6 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zerotimedev/thinker",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.37",
|
|
4
|
+
"description": "Installer for thinker, a knowledge cache for coding agents: npx @zerotimedev/thinker",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"thinker-install": "install.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"install.mjs",
|
|
11
|
+
"install.sh"
|
|
12
|
+
],
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=20"
|
|
15
|
+
},
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/yoavshmariahu/thinker.git"
|
|
19
|
+
},
|
|
20
|
+
"homepage": "https://github.com/yoavshmariahu/thinker#readme",
|
|
21
|
+
"license": "MIT"
|
|
22
|
+
}
|