thinkersh 0.0.0-stage → 0.2.22

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 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,29 @@
1
- # Temporary Holding Version
1
+ # thinkersh
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [Thinker](https://zerotime.dev) helps your coding agent use fewer tokens.
4
+ Find wasted context, review what to cut, and reuse what your agent learns about your code.
5
+
6
+ ```bash
7
+ npx thinkersh
8
+ ```
9
+
10
+ The installer connects your coding agents and shows suggested savings from your recent sessions.
11
+ Review them in your browser or terminal, then keep coding as usual.
12
+
13
+ Requires bash, git, curl, tar and Node 20+. [Setup and privacy](https://zerotime.dev/docs#install).
14
+
15
+ ## Installer details
16
+
17
+ The package downloads the latest GitHub release, verifies its SHA-256 and installs it under
18
+ `~/.thinker`. Thinker keeps itself updated; npm is not needed again.
19
+ Previously installed with `npx @zerotimedev/thinker`? No reinstall is needed: `thinker update`
20
+ and automatic updates use the same GitHub releases. `thinkersh` is the canonical npm package.
21
+
22
+ Installer options pass through, for example `npx thinkersh --uninstall`.
23
+
24
+ ## As a pi package
25
+
26
+ The same package is a [pi](https://pi.dev) package: `pi install npm:thinkersh` adds an extension that
27
+ runs the installed thinker in every pi session (notes with each prompt and as files are opened, sessions
28
+ distilled into notes). It is a pointer: install thinker first with the command above. Where `thinker connect`
29
+ has already wired pi (`~/.pi/agent/extensions/thinker.js`), the package stays quiet, so nothing is served twice.
package/install.mjs ADDED
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env node
2
+ // npx thinkersh: 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,416 @@
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 grows from the sessions there. Nothing is asked in
6
+ # the terminal.
7
+ #
8
+ # Thinker is open source; no access code or GitHub token is required. It installs the latest
9
+ # GitHub release (https://github.com/yoavshmariahu/thinker/releases: a tag vX.Y.Z carrying
10
+ # thinker.tgz, version.json and install.sh), verified against the sha256 in version.json.
11
+ #
12
+ # npx thinkersh
13
+ #
14
+ # The npm package of that name carries this file and runs it with the arguments passed through.
15
+ #
16
+ # Options
17
+ # --build, --no-build, --depth <d>, --prs <n>, --pr <n>, --benchmark, --no-benchmark
18
+ # accepted from old scripts and ignored: there is no cache build (2026-10-21)
19
+ # --no-seed, --areas <n>, --no-behaviors accepted from old scripts and ignored
20
+ # -y, --yes accept defaults and skip interactive confirmation prompts
21
+ # --clients <list> coding agents to wire up: claude, codex, cursor, gemini, pi, windsurf, copilot, opencode, all or auto
22
+ # (default auto)
23
+ # --cache <source> cache built for this repo. One of: gh:<path in the thinker repo>, an https URL, a local file.
24
+ # Omit if .thinker/notes is already in the repo.
25
+ # --no-learn do not distill your own sessions into new notes (learning is on by default, for any of the
26
+ # agents, and uses that agent's login; switch it off for evals)
27
+ # --late also serve notes about files as the agent opens them
28
+ # --mcp accepted for compatibility: the MCP server is always registered (needs npm)
29
+ # --no-git-hook do not install the git post-commit hook (it re-checks and maintains notes after each commit)
30
+ # --no-ui do not offer the browser for the Context report; it is shown in the terminal, with its suggestions to pick from
31
+ # --ref <tag> release to install, a tag such as v0.1.34 (default latest; --branch also accepted)
32
+ # --update update an installed thinker to the latest release and exit
33
+ # --no-auto-update do not schedule background auto-updates (every 4 hours)
34
+ # --no-modify-path do not add THINKER_HOME/bin to PATH in your shell's and bash's startup files
35
+ # --uninstall remove hooks and registration from this repo (add --purge to delete notes too); run outside
36
+ # a repository, it removes the wiring from your agents' own settings
37
+ #
38
+ # Environment
39
+ # THINKER_HOME where the tool is installed (default ~/.thinker)
40
+ # THINKER_GH_REPO GitHub repository whose releases to install from (default yoavshmariahu/thinker)
41
+ # THINKER_REF release tag (default latest)
42
+ # THINKER_DIST_URL install from this tarball URL (with version.json beside it) instead of a GitHub release
43
+ # GITHUB_TOKEN only for --cache gh:<path>: a token for the GitHub API (GH_TOKEN works too;
44
+ # if neither is set and the GitHub CLI is logged in, its token is used)
45
+ #
46
+ # The script writes to THINKER_HOME, to .thinker/ and the agents' config in this repository, and adds one
47
+ # line putting THINKER_HOME/bin on PATH to your shell's startup file and bash's (--no-modify-path leaves them alone). No sudo.
48
+ set -euo pipefail
49
+ export COPYFILE_DISABLE=1
50
+ export COPY_EXTENDED_ATTRIBUTES_DISABLE=1
51
+
52
+ # add_to_path <dir>: put <dir> on PATH in the startup file of the user's shell, and in bash's, where agents
53
+ # may run their commands whatever the login shell is (src/shell-path.js keeps the same list), once each.
54
+ # Prints the login shell's file, or nothing when it could not tell which one.
55
+ add_to_path() {
56
+ local dir="$1" line="export PATH=\"$1:\$PATH\"" rc="" f login
57
+ case "$(basename "${SHELL:-}")" in
58
+ zsh) rc="${ZDOTDIR:-$HOME}/.zshrc" ;;
59
+ fish) rc="${XDG_CONFIG_HOME:-$HOME/.config}/fish/conf.d/thinker.fish"
60
+ if ! grep -qsF "fish_add_path \"$dir\"" "$rc"; then mkdir -p "$(dirname "$rc")" && printf '\n# thinker\n%s\n' "fish_add_path \"$dir\"" >> "$rc" || rc=""; fi ;;
61
+ esac
62
+ # bash: .bashrc, and the login file a login bash reads, the first that exists (creating .bash_profile would
63
+ # hide a .profile; made only on macOS). With no login file on Linux, create .profile.
64
+ # A Linux .bashrc returns early when not interactive, so the end of it
65
+ # is not enough for `bash -l`.
66
+ local files="$HOME/.bashrc"
67
+ login=""
68
+ for f in .bash_profile .bash_login .profile; do if [ -e "$HOME/$f" ]; then login="$HOME/$f"; break; fi; done
69
+ if [ -z "$login" ]; then
70
+ if [ "$(uname)" = Darwin ]; then login="$HOME/.bash_profile"; else login="$HOME/.profile"; fi
71
+ fi
72
+ [ -n "$login" ] && files="$login $files"
73
+ [ "$(basename "${SHELL:-}")" = bash ] && rc="${files%% *}"
74
+ [ "$(basename "${SHELL:-}")" = zsh ] && files="$rc $files"
75
+ for f in $files; do
76
+ if ! grep -qsF "$line" "$f"; then
77
+ mkdir -p "$(dirname "$f")" && printf '\n# thinker\n%s\n' "$line" >> "$f" || true
78
+ fi
79
+ done
80
+ printf '%s' "$rc"
81
+ }
82
+
83
+ # path_hint: how to reach `thinker` from this shell; reads main's $home and $rcfile
84
+ path_hint() {
85
+ case ":$PATH:" in
86
+ *":$home/bin:"*) ;;
87
+ *) 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\""
88
+ else say " Optional: add $home/bin to your PATH to run 'thinker' directly."; fi ;;
89
+ esac
90
+ }
91
+
92
+ main() {
93
+ local cache="" areas="" clients="" learn=1 late=0 mcp=0 githook=0 uninstall=0 purge=0 update=0 autoupdate=1 modpath=1 ref="${THINKER_REF:-latest}" yes=0 no_seed=0 ui=1
94
+ while [ $# -gt 0 ]; do
95
+ case "$1" in
96
+ --cache) cache="${2:-}"; shift 2 ;;
97
+ --build|--no-build) shift ;;
98
+ --no-seed) shift ;;
99
+ --no-behaviors) shift ;;
100
+ -y|--yes) yes=1; shift ;;
101
+ --areas) shift 2 ;;
102
+ --depth|--prs|--pr) shift 2 ;;
103
+ --benchmark|--no-benchmark) shift ;;
104
+ --clients) clients="${2:-}"; shift 2 ;;
105
+ --branch|--ref) ref="${2:-}"; shift 2 ;;
106
+ --learn) learn=1; shift ;;
107
+ --no-learn) learn=0; shift ;;
108
+ --late) late=1; shift ;;
109
+ --shared) echo "Team sharing is no longer supported." >&2; return 1 ;;
110
+ --mcp) mcp=1; shift ;;
111
+ --git-hook) githook=1; shift ;;
112
+ --no-git-hook) githook=0; shift ;;
113
+ --no-ui) ui=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
+ local uargs=(uninstall)
214
+ if [ -z "$repo" ]; then uargs+=(--user)
215
+ else [ "$purge" = 1 ] && uargs+=(--purge); uargs+=(--repo "$repo"); fi
216
+ [ "$yes" = 1 ] && uargs+=(--yes)
217
+ # the terminal, not the piped script, answers the optional question why
218
+ if [ "$yes" != 1 ] && [ -r /dev/tty ] && (exec < /dev/tty) 2>/dev/null; then "$thinker" "${uargs[@]}" < /dev/tty
219
+ else "$thinker" "${uargs[@]}"; fi
220
+ say "To remove the tool itself: rm -rf \"$home\", and delete the '# thinker' PATH line from your shell's startup file"
221
+ exit 0
222
+ fi
223
+
224
+ if [ "$update" = 1 ]; then
225
+ [ -x "$thinker" ] || die "thinker is not installed in $home"
226
+ fi
227
+
228
+ # --- install the tool ----------------------------------------------------
229
+ local tmp; tmp="$(mktemp -d)"
230
+ # expand now: the variable is local and gone by the time the trap runs
231
+ # shellcheck disable=SC2064
232
+ trap "rm -rf '$tmp'" EXIT
233
+ say ""
234
+ if [ "$fancy" = 1 ]; then
235
+ local sparkle="${yellow}*${reset} ${magenta}~${reset} ${yellow}*${reset}"
236
+ # the same headline and lines as `thinker setup` (src/setup/ui.js:HEADLINE, TAGLINE)
237
+ box "$cyan" "$sparkle ${bold}${cyan}Thinker${reset} $sparkle" "${bold}Think ahead. Save tokens.${reset}" \
238
+ "${magenta}~~~~~~~~~~~~~~~~~~~~~~~~~~~${reset}" "" \
239
+ "${dim}Thinker helps you save tokens with context compression, knowledge caching,${reset}" \
240
+ "${dim}and leaner agent instructions, skills and MCP servers.${reset}"
241
+ else
242
+ say "Thinker"
243
+ say "Think ahead. Save tokens."
244
+ say "Thinker helps you save tokens with context compression, knowledge caching, and leaner agent instructions, skills and MCP servers."
245
+ fi
246
+ say ""
247
+ case "$dist" in https://*) ;; *) die "releases must be downloaded over HTTPS" ;; esac
248
+ # GitHub answers with a redirect to its asset host; curl follows it over HTTPS only
249
+ download_release() {
250
+ curl -fsSL --proto-redir '=https' -o "$tmp/thinker.tgz" "$dist" &&
251
+ curl -fsSL --proto-redir '=https' -o "$tmp/version.json" "${dist%/*}/version.json"
252
+ }
253
+ 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?)"
254
+ say " Verifying download…"
255
+ node --input-type=module - "$tmp/thinker.tgz" "$tmp/version.json" <<'JS'
256
+ import fs from 'node:fs';
257
+ import crypto from 'node:crypto';
258
+ const [archive, manifest] = process.argv.slice(2);
259
+ const release = JSON.parse(fs.readFileSync(manifest, 'utf8'));
260
+ if (release.schemaVersion !== 1 || !/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(release.version || '') ||
261
+ !/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(release.commit || '') || !/^[a-f0-9]{64}$/.test(release.sha256 || '')) {
262
+ throw new Error('invalid or unsupported release manifest');
263
+ }
264
+ const actual = crypto.createHash('sha256').update(fs.readFileSync(archive)).digest('hex');
265
+ if (actual !== release.sha256) throw new Error('release checksum mismatch; refusing to extract');
266
+ JS
267
+ tar_extract() {
268
+ local archive="$1" dest="$2"
269
+ local warn_opt=""
270
+ if tar --warning=no-unknown-keyword --version >/dev/null 2>&1; then
271
+ warn_opt="--warning=no-unknown-keyword"
272
+ fi
273
+ local code=0
274
+ local err
275
+ # shellcheck disable=SC2086
276
+ err="$(COPYFILE_DISABLE=1 COPY_EXTENDED_ATTRIBUTES_DISABLE=1 tar ${warn_opt} -xzf "$archive" -C "$dest" 2>&1)" || code=$?
277
+ if [ -n "$err" ]; then
278
+ local filtered
279
+ filtered="$(printf '%s\n' "$err" | grep -v "Ignoring unknown extended header keyword" || true)"
280
+ if [ -n "$filtered" ]; then
281
+ printf '%s\n' "$filtered" >&2
282
+ fi
283
+ fi
284
+ [ "$code" -eq 0 ] || return "$code"
285
+ }
286
+
287
+ say " Installing CLI…"
288
+ mkdir -p "$tmp/app" && tar_extract "$tmp/thinker.tgz" "$tmp/app"
289
+ # GitHub archives wrap everything in one top-level directory
290
+ if [ ! -f "$tmp/app/src/cli.js" ]; then
291
+ local inner; inner="$(find "$tmp/app" -mindepth 1 -maxdepth 1 -type d | head -1)"
292
+ [ -n "$inner" ] && [ -f "$inner/src/cli.js" ] || die "downloaded archive does not look like thinker"
293
+ mv "$inner" "$tmp/app.inner" && rm -rf "$tmp/app" && mv "$tmp/app.inner" "$tmp/app"
294
+ fi
295
+ node --input-type=module - "$tmp/app/package.json" "$tmp/version.json" <<'JS'
296
+ import fs from 'node:fs';
297
+ const pkg = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
298
+ const release = JSON.parse(fs.readFileSync(process.argv[3], 'utf8'));
299
+ if (pkg.version !== release.version) throw new Error(`release version ${release.version} does not match archive version ${pkg.version}`);
300
+ JS
301
+ # --update: the downloaded copy's updater installs itself, so an older install (one that still
302
+ # downloads from the old host, say) moves to the releases. The home and its settings are kept.
303
+ if [ "$update" = 1 ]; then
304
+ node --input-type=module - "$tmp/app/src/update.js" "$home" "$dist" "$ref" <<'JS'
305
+ import path from 'node:path';
306
+ import { pathToFileURL } from 'node:url';
307
+ const [modulePath, home, dist, ref] = process.argv.slice(2);
308
+ const { applyUpdate } = await import(pathToFileURL(modulePath));
309
+ const result = await applyUpdate({ home, rootDir: path.join(home, 'app'), dist, ref });
310
+ console.log(`Updated thinker to v${result.version}`);
311
+ JS
312
+ exit 0
313
+ fi
314
+ # the tool needs only its source and manifest at runtime
315
+ rm -rf "$tmp/app/bench" "$tmp/app/test" "$tmp/app/caches"
316
+ mkdir -p "$home/bin"
317
+ rm -rf "$home/app.new" && mv "$tmp/app" "$home/app.new"
318
+ rm -rf "$home/app.old"; [ -d "$home/app" ] && mv "$home/app" "$home/app.old"
319
+ mv "$home/app.new" "$home/app" && rm -rf "$home/app.old"
320
+ cat > "$thinker" <<SHIM
321
+ #!/bin/sh
322
+ exec node "$home/app/src/cli.js" "\$@"
323
+ SHIM
324
+ chmod +x "$thinker"
325
+ cat > "$home/install.json" <<EOF
326
+ {
327
+ "source": "archive",
328
+ "ghrepo": "$ghrepo",
329
+ "ref": "$ref",
330
+ "dist": "$dist",
331
+ "installedAt": "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
332
+ }
333
+ EOF
334
+ if [ "$autoupdate" = 1 ]; then
335
+ "$thinker" update --schedule --quiet 2>/dev/null || true
336
+ else
337
+ # recorded, so the background check does not put the schedule back (update.js:ensureSchedule)
338
+ "$thinker" update --unschedule --quiet >/dev/null 2>&1 || true
339
+ fi
340
+ local rcfile=""
341
+ if [ "$modpath" = 1 ]; then rcfile="$(add_to_path "$home/bin")"; fi
342
+
343
+ # whatever agents are on this machine are wired up, in their own settings
344
+ [ -z "$clients" ] && clients="auto"
345
+ # The dependencies are always installed: the hooks rank notes with a local cross-encoder whose runtime is one
346
+ # of them, and the MCP server, registered for every agent, needs its SDK. Without npm the hooks still work,
347
+ # ranking by words alone, and the MCP server is left out (thinker connect registers it once npm ci has run).
348
+ if command -v npm >/dev/null; then
349
+ mcp=1
350
+ # the cd lives in a subshell: `spin` runs its command in this shell when there is no terminal (the piped
351
+ # install), so a bare cd left the installer sitting in $home/app -- a directory this script replaces and an
352
+ # update deletes, and every command after it then died on a working directory that was gone (2026-10-28)
353
+ install_deps() { ( cd "$home/app" && npm ci --omit=dev --ignore-scripts --no-audit --no-fund --silent ); }
354
+ spin "Installing dependencies (the ranking runtime is most of it)" install_deps || die "npm ci failed in $home/app"
355
+ 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."
356
+ else
357
+ 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."
358
+ mcp=0
359
+ fi
360
+
361
+ # --- wire the agents, once, in their own settings ---------------------------
362
+ # Nothing is asked here: Codex's hook-review marks are written outright. Thinker then runs in every
363
+ # git repository on this machine.
364
+ local cargs="--clients $clients --yes"
365
+ [ "$learn" = 1 ] || cargs="$cargs --no-learn"
366
+ [ "$late" = 1 ] && cargs="$cargs --late"
367
+ [ "$mcp" = 1 ] || cargs="$cargs --no-mcp"
368
+ # shellcheck disable=SC2086
369
+ "$thinker" connect $cargs
370
+
371
+ # --- where the agents' context goes, from the sessions already on this machine ----------------------------
372
+ # Nothing to build and no cache needed: the Context report reads the agents' own transcripts of the last 30
373
+ # days, so it is there now, from the sessions that ran before thinker was installed. The most active
374
+ # repositories are read (each report kept for the page) and the person is asked where to see what to cut
375
+ # first: in the browser (the page over every repository) or here, where the suggestions are a list to pick
376
+ # from and apply at once, with the page's address for the detail (setup/context-ask.js). The terminal, not
377
+ # the piped script, answers; with --yes or no terminal the report is printed. `thinker context --machine
378
+ # [--ask]` does the same any time.
379
+ say ""
380
+ local xargs=(context --machine --ask)
381
+ [ "$ui" = 1 ] || xargs+=(--no-browser)
382
+ if [ "$yes" != 1 ] && [ -r /dev/tty ] && (exec < /dev/tty) 2>/dev/null; then
383
+ "$thinker" "${xargs[@]}" < /dev/tty || say " The Context report could not be made now; thinker context --machine prints it, thinker ui shows it."
384
+ else
385
+ "$thinker" context --machine || say " The Context report could not be made now; thinker context --machine prints it, thinker ui shows it."
386
+ fi
387
+
388
+ # --- a cache handed over -------------------------------------------------------
389
+ if [ -n "$cache" ]; then
390
+ case "$cache" in
391
+ gh:*) gh_fetch "repos/$ghrepo/contents/${cache#gh:}?ref=$ref" "$tmp/cache.tgz"; "$thinker" import "$tmp/cache.tgz" --repo "$repo" ;;
392
+ *) "$thinker" import "$cache" --repo "$repo" ;;
393
+ esac
394
+ fi
395
+
396
+ # --- done -------------------------------------------------------------------------
397
+ # Every git repository has a cache from here on, and it grows from the sessions there.
398
+ local version; version="$(node -p "require('$home/app/package.json').version" 2>/dev/null || echo '?')"
399
+ path_hint
400
+ say ""
401
+ if [ "$fancy" = 1 ]; then
402
+ box "$green" "${green}✓${reset} ${bold}Install complete${reset} ${dim}· Thinker v$version${reset}" "" \
403
+ "Installed into ${home/#$HOME/~} and wired into your agents." \
404
+ "Thinker now runs in every git repository on this machine." \
405
+ "Where your agents' context goes, any time: ${cyan}thinker ui${reset}." \
406
+ "${dim}Notes stay on this machine; Thinker uploads none.${reset}" "" \
407
+ "${yellow}*${reset} ${magenta}~${reset} ${yellow}*${reset} ${bold}all done · happy shipping${reset} ${yellow}*${reset} ${magenta}~${reset} ${yellow}*${reset}"
408
+ else
409
+ say "Install complete: Thinker v$version, installed into $home and wired into your agents."
410
+ say "Thinker now runs in every git repository on this machine."
411
+ say "Where your agents' context goes, any time: thinker ui."
412
+ say "Notes stay on this machine; Thinker uploads none."
413
+ fi
414
+ }
415
+
416
+ main "$@"
package/package.json CHANGED
@@ -1,6 +1,31 @@
1
1
  {
2
2
  "name": "thinkersh",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.22",
4
+ "description": "Installer for thinker (think ahead, save context): npx thinkersh",
5
+ "type": "module",
6
+ "bin": {
7
+ "thinker-install": "install.mjs"
8
+ },
9
+ "files": [
10
+ "install.mjs",
11
+ "install.sh",
12
+ "pi/"
13
+ ],
14
+ "engines": {
15
+ "node": ">=20"
16
+ },
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/yoavshmariahu/thinker.git"
20
+ },
21
+ "homepage": "https://github.com/yoavshmariahu/thinker#readme",
22
+ "license": "MIT",
23
+ "pi": {
24
+ "extensions": [
25
+ "./pi/thinker.js"
26
+ ],
27
+ "skills": [],
28
+ "prompts": [],
29
+ "themes": []
30
+ }
31
+ }
package/pi/thinker.js ADDED
@@ -0,0 +1,45 @@
1
+ // The pi package (`pi install npm:thinkersh`): a pointer to the thinker installed on this
2
+ // machine, loaded when pi starts. Nothing of thinker runs from the package: the hooks, the learning and
3
+ // the notes come from the installed copy (THINKER_HOME/app, by default ~/.thinker/app), through the same
4
+ // extension module `thinker connect` wires for pi (src/integrations/pi.js), in every checkout.
5
+ //
6
+ // One copy only: `thinker connect` writes ~/.pi/agent/extensions/thinker.js, and where that file is in
7
+ // place it does the work and this package does nothing, or every note would reach the agent twice.
8
+ // Without an installed thinker the package says so once per session and does nothing else.
9
+ import fs from 'node:fs';
10
+ import os from 'node:os';
11
+ import path from 'node:path';
12
+ import { pathToFileURL } from 'node:url';
13
+
14
+ const home = () => process.env.THINKER_HOME || path.join(os.homedir(), '.thinker');
15
+ const piDir = () => process.env.PI_CODING_AGENT_DIR || path.join(os.homedir(), '.pi', 'agent');
16
+ export const INSTALL_HINT = 'thinker is not installed on this machine: run `npx thinkersh` (the pi package only points at the installed copy).';
17
+
18
+ // the installed command line: THINKER_CLI names one (tests), else the install under THINKER_HOME
19
+ export function findCli(env = process.env) {
20
+ const named = env.THINKER_CLI;
21
+ if (named) return fs.existsSync(named) ? named : null;
22
+ const cli = path.join(home(), 'app', 'src', 'cli.js');
23
+ return fs.existsSync(cli) ? cli : null;
24
+ }
25
+
26
+ export const connectedByThinker = () => fs.existsSync(path.join(piDir(), 'extensions', 'thinker.js'));
27
+
28
+ // Handlers are registered at once, synchronously; the extension module behind them is loaded on
29
+ // the first event, so a pi that loads extensions synchronously is served too.
30
+ export default function thinkerPackage(pi, { cli = findCli(), yieldTo = connectedByThinker() } = {}) {
31
+ if (yieldTo) return;
32
+ if (!cli) {
33
+ let said = false;
34
+ pi.on('before_agent_start', async (_e, ctx) => { if (!said && ctx.hasUI) { said = true; ctx.ui.notify(INSTALL_HINT, 'warning'); } });
35
+ return;
36
+ }
37
+ const handlers = {};
38
+ const collector = { on: (event, fn) => { handlers[event] = fn; } };
39
+ const loaded = import(pathToFileURL(path.join(path.dirname(cli), 'integrations', 'pi.js')).href)
40
+ .then(m => { m.piExtension(collector, { client: 'pi', repo: null, cli, hooks: true, learn: true, late: true, shared: false, mcp: false }); })
41
+ .catch(() => {}); // a broken install must not interrupt the host agent
42
+ for (const event of ['before_agent_start', 'tool_result', 'agent_end', 'session_shutdown']) {
43
+ pi.on(event, async (e, ctx) => { await loaded; return handlers[event] ? handlers[event](e, ctx) : undefined; });
44
+ }
45
+ }