Repository navigation
Expand file tree
/
Copy pathinstall.sh
More file actions
executable file
·536 lines (486 loc) · 26 KB
/
Copy pathinstall.sh
File metadata and controls
executable file
·536 lines (486 loc) · 26 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
#!/usr/bin/env bash
# tokenwar one-shot installer.
#
# curl -fsSL https://raw.githubusercontent.com/oratelecom/tokenwar/main/install.sh | bash
# curl -fsSL .../install.sh | bash -s -- --with-plugins # + the 4 plugins
# curl -fsSL .../install.sh | bash -s -- --all # + plugins + RTK + pxpipe + graphify + OpenWiki
#
# Does:
# 1. git clone https://github.com/oratelecom/tokenwar ~/.claude/skills/tokenwar
# 2. chmod +x scripts/*.sh
# 3. patch ~/.claude/settings.json to wire the statusLine
# 4. wire the tokenwar/codex/gemini/kimi/opencode/copilot shell functions
# 5. opt-in installs (none by default — no surprise mutation):
# --with-plugins marketplace add + install + enable the 4 Claude Code
# plugins (context-mode, claude-mem, caveman, ponytail),
# with anti-clobber re-enable.
# --with-rtk install the RTK binary via rtk's official prebuilt
# installer (prebuilt, no toolchain), then wire its hook.
# --with-pxpipe install pxpipe proxy from a pinned npm package.
# --with-graphify install the graphify CLI (PyPI `graphifyy`) and register
# its assistant skill via `graphify install`.
# --with-openwiki install the pinned OpenWiki CLI. Repository initialization
# remains explicit because it writes docs and invokes an LLM.
# --with-copilot point the installed tools at GitHub Copilot CLI's own
# extension points (hook / skills / MCP) via copilot.sh.
# --all plugins + RTK + pxpipe + graphify + OpenWiki + Copilot wiring. After
# plugins/RTK, RTK's hook is wired via `rtk init -g`.
# Without any flag, plugin/RTK setup is left to /tokenwar activate.
#
# Idempotent: re-running pulls the latest tokenwar and only patches settings.json
# if the statusLine is not already pointing at the tokenwar script.
set -euo pipefail
REPO_URL="${TOKENWAR_REPO_URL:-https://github.com/oratelecom/tokenwar}"
INSTALL_DIR="${TOKENWAR_DIR:-$HOME/.claude/skills/tokenwar}"
SETTINGS_JSON="$HOME/.claude/settings.json"
STATUSLINE_CMD='bash ~/.claude/skills/tokenwar/scripts/tokenwar-statusline.sh'
readonly CLAUDE_BIN="claude"
# The 4 Claude Code plugins of the stack, paired with the upstream marketplace
# repo each is published from. RTK is intentionally absent — it is a Rust binary
# + settings hook, not a plugin.
readonly PLUGIN_MARKETPLACES=(
"mksglu/context-mode"
"thedotmack/claude-mem"
"JuliusBrussee/caveman"
"DietrichGebert/ponytail"
)
readonly PLUGIN_SLUGS=(
"context-mode@context-mode"
"claude-mem@thedotmack"
"caveman@caveman"
"ponytail@ponytail"
)
# RTK binary install (--with-rtk) via rtk's OWN official installer, which
# downloads a prebuilt binary (no toolchain) for every major platform. Pinned to
# a release tag so the script we pipe to sh is fixed/reviewable; ref is
# env-overridable. rtk drops the binary in ~/.local/bin.
readonly RTK_BIN="rtk"
# opencode has no Claude-Code-style hook system, so RTK ships a dedicated
# opencode plugin (`rtk init -g --opencode` writes ~/.config/opencode/plugins/rtk.ts).
# Without it, RTK never rewrites bash inside opencode and saves zero tokens there.
readonly OPENCODE_BIN="opencode"
readonly RTK_INSTALL_REF="${TOKENWAR_RTK_INSTALL_REF:-v0.42.4}"
readonly RTK_INSTALL_URL="https://raw.githubusercontent.com/rtk-ai/rtk/${RTK_INSTALL_REF}/install.sh"
readonly RTK_LOCAL_BIN="$HOME/.local/bin"
readonly NPM_BIN="npm"
readonly PXPIPE_BIN="pxpipe"
readonly PXPIPE_NPM_PACKAGE="pxpipe-proxy"
readonly PXPIPE_NPM_VERSION="0.10.0"
readonly PXPIPE_NPM_SPEC="${PXPIPE_NPM_PACKAGE}@${PXPIPE_NPM_VERSION}"
readonly USER_LOCAL_BIN="$HOME/.local/bin"
# graphify (--with-graphify): the repo/doc structure lane. Published on PyPI as
# `graphifyy` — the plain `graphify` name there is an unaffiliated package, per
# upstream's README — while the installed command stays `graphify`. Installed
# through an isolated-env manager when one exists (uv tool, then pipx): a plain
# `pip install` into a shared environment is what produces the upstream
# "ModuleNotFoundError: No module named 'graphify'" report, because the skill
# resolves its interpreter at runtime and can land on a different env.
readonly UV_BIN="uv"
readonly PIPX_BIN="pipx"
readonly PIP_BIN="pip"
readonly GRAPHIFY_BIN="graphify"
readonly GRAPHIFY_PYPI_PACKAGE="graphifyy"
# OpenWiki is shared project memory. Installation is safe and pinned; generating
# a wiki remains a deliberate per-repository action because it invokes an LLM.
readonly OPENWIKI_BIN="openwiki"
readonly OPENWIKI_NPM_SPEC="openwiki@0.5.1"
# Copilot wiring (--with-copilot). The tools are published for Claude Code and
# do not reach Copilot for free; copilot.sh points each at Copilot's own
# extension point. It is idempotent and no-ops when the CLI is absent.
readonly COPILOT_BIN="copilot"
readonly COPILOT_WIRE_SCRIPT_REL="scripts/copilot.sh"
# Shell-integration block markers — used to idempotently inject/remove the
# `tokenwar`, `codex`, `gemini`, `kimi`, and `opencode` wrapper functions in the
# user's shell rc.
readonly TW_RC_BEGIN="# >>> tokenwar shell integration >>>"
readonly TW_RC_END="# <<< tokenwar shell integration <<<"
readonly WRAPPED_PROVIDER_CLIS=(
"codex"
"gemini"
"kimi"
"opencode"
"copilot"
)
color() { printf '\033[%sm%s\033[0m' "$1" "$2"; }
green() { color 32 "$1"; }
yellow() { color 33 "$1"; }
red() { color 31 "$1"; }
say() { printf '%s %s\n' "$(green '==>')" "$*"; }
warn() { printf '%s %s\n' "$(yellow '!!')" "$*" >&2; }
die() { printf '%s %s\n' "$(red 'ERR')" "$*" >&2; exit 1; }
WITH_PLUGINS=false
WITH_RTK=false
WITH_PXPIPE=false
WITH_GRAPHIFY=false
WITH_OPENWIKI=false
WITH_COPILOT=false
for arg in "$@"; do
case "$arg" in
--with-plugins) WITH_PLUGINS=true ;;
--with-rtk) WITH_RTK=true ;;
--with-pxpipe) WITH_PXPIPE=true ;;
--with-graphify) WITH_GRAPHIFY=true ;;
--with-openwiki) WITH_OPENWIKI=true ;;
--with-copilot) WITH_COPILOT=true ;;
--all) WITH_PLUGINS=true; WITH_RTK=true; WITH_PXPIPE=true; WITH_GRAPHIFY=true; WITH_OPENWIKI=true; WITH_COPILOT=true ;;
-h|--help)
printf 'Usage: install.sh [--with-plugins] [--with-rtk] [--with-pxpipe] [--with-graphify] [--with-openwiki] [--with-copilot] [--all]\n'
printf ' --with-plugins install+enable the 4 Claude Code plugins (incl. ponytail)\n'
printf ' --with-rtk install the RTK binary (official prebuilt installer) + wire its hook\n'
printf ' --with-pxpipe install pxpipe proxy (%s)\n' "$PXPIPE_NPM_SPEC"
printf ' --with-graphify install the graphify CLI (PyPI %s) + register its skill\n' "$GRAPHIFY_PYPI_PACKAGE"
printf ' --with-openwiki install OpenWiki (%s); per-repo initialization stays explicit\n' "$OPENWIKI_NPM_SPEC"
printf ' --with-copilot wire the installed tools into GitHub Copilot CLI\n'
printf ' --all all of the above\n'
exit 0 ;;
*) die "unknown argument: $arg (supported: --with-plugins, --with-rtk, --with-pxpipe, --with-graphify, --with-openwiki, --with-copilot, --all)" ;;
esac
done
command -v git >/dev/null || die "git is required"
command -v node >/dev/null || die "node is required (used to patch settings.json)"
# 1. clone or update
if [[ -d "$INSTALL_DIR/.git" ]]; then
say "Updating existing tokenwar checkout at $INSTALL_DIR"
git -C "$INSTALL_DIR" pull --ff-only
elif [[ -e "$INSTALL_DIR" ]]; then
# Directory (or file) exists but is NOT a git checkout — e.g. an older
# manual copy. `git clone` refuses a non-empty target, so move the old
# one aside (never silently delete the user's data) and clone fresh.
backup="${INSTALL_DIR}.bak-$(date +%Y%m%d-%H%M%S)"
warn "$INSTALL_DIR exists but is not a git checkout — backing it up to $backup"
mv "$INSTALL_DIR" "$backup" || die "could not move $INSTALL_DIR aside"
say "Cloning tokenwar into $INSTALL_DIR"
mkdir -p "$(dirname "$INSTALL_DIR")"
git clone "$REPO_URL" "$INSTALL_DIR"
else
say "Cloning tokenwar into $INSTALL_DIR"
mkdir -p "$(dirname "$INSTALL_DIR")"
git clone "$REPO_URL" "$INSTALL_DIR"
fi
# 2. chmod scripts
say "Marking scripts executable"
chmod +x "$INSTALL_DIR"/scripts/*.sh
# 3. patch settings.json (statusLine)
say "Wiring statusLine in $SETTINGS_JSON"
mkdir -p "$(dirname "$SETTINGS_JSON")"
[[ -f "$SETTINGS_JSON" ]] || echo '{}' > "$SETTINGS_JSON"
SETTINGS_JSON="$SETTINGS_JSON" STATUSLINE_CMD="$STATUSLINE_CMD" node --input-type=module -e '
import { readFileSync, writeFileSync, copyFileSync } from "fs";
const path = process.env.SETTINGS_JSON;
const desired = process.env.STATUSLINE_CMD;
let cfg = {};
try { cfg = JSON.parse(readFileSync(path, "utf8")); } catch (e) { cfg = {}; }
const already = cfg.statusLine && cfg.statusLine.type === "command" && cfg.statusLine.command === desired;
if (already) {
console.log(" statusLine already wired — leaving settings.json alone");
process.exit(0);
}
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
copyFileSync(path, `${path}.bak-${stamp}`);
cfg.statusLine = { type: "command", command: desired };
writeFileSync(path, JSON.stringify(cfg, null, 2) + "\n");
console.log(` patched (backup at ${path}.bak-${stamp})`);
'
# 4. wire shell integration (tokenwar + provider functions) into shell rc.
#
# Claude Code shows the native statusLine; Codex, Gemini, Kimi, opencode, and
# GitHub Copilot CLI do not expose a status-bar API, so we wrap their launch with
# a banner + reminder. The `tokenwar` function makes `tokenwar status` work in
# any shell.
# Idempotent: an existing tokenwar block is replaced, never duplicated.
wire_shell_rc() {
local rc_file="$1"
[[ -f "$rc_file" ]] || return 0
# Strip any previous tokenwar block (between markers) to a temp copy.
local tmp
tmp="$(mktemp "${rc_file}.tokenwar.XXXXXX")" || { warn "mktemp failed for $rc_file"; return 1; }
TW_BEGIN="$TW_RC_BEGIN" TW_END="$TW_RC_END" awk '
$0 == ENVIRON["TW_BEGIN"] { skip = 1 }
skip != 1 { print }
$0 == ENVIRON["TW_END"] { skip = 0 }
' "$rc_file" > "$tmp" || { warn "could not rewrite $rc_file"; rm -f "$tmp"; return 1; }
# Append a fresh block. ~/.local/bin is where --with-rtk and --with-pxpipe
# drop their binaries; on many systems (notably macOS) it is not on PATH by
# default, so the block exports it — otherwise rtk/pxpipe install "succeeds"
# but every later `command -v rtk` returns nothing and the hooks/scrips break.
{
printf '%s\n' "$TW_RC_BEGIN"
printf '%s\n' 'case ":$PATH:" in *":'"$USER_LOCAL_BIN"':"*) : ;; *) export PATH="'"$USER_LOCAL_BIN"':$PATH" ;; esac'
printf '%s\n' "tokenwar() { command bash \"\$HOME/.claude/skills/tokenwar/scripts/tokenwar.sh\" \"\$@\"; }"
local provider_cli
for provider_cli in "${WRAPPED_PROVIDER_CLIS[@]}"; do
printf '%s\n' "${provider_cli}() { command bash \"\$HOME/.claude/skills/tokenwar/scripts/tokenwar-launch.sh\" ${provider_cli} \"\$@\"; command ${provider_cli} \"\$@\"; }"
done
printf '%s\n' "$TW_RC_END"
} >> "$tmp"
if ! mv -f "$tmp" "$rc_file"; then
warn "could not write $rc_file"; rm -f "$tmp"; return 1
fi
say "Wired tokenwar/codex/gemini/kimi/opencode/copilot shell functions in $rc_file"
}
# Print the ids of every currently-enabled plugin, one per line (from
# `claude plugin list --json`). Empty on any failure.
list_enabled_ids() {
"$CLAUDE_BIN" plugin list --json 2>/dev/null | node --input-type=module -e '
let s = "";
process.stdin.on("data", d => s += d).on("end", () => {
let arr = [];
try { arr = JSON.parse(s || "[]"); } catch {}
for (const p of arr) if (p && p.enabled) console.log(p.id);
});
' 2>/dev/null || true
}
# --with-plugins: register marketplaces, then install + enable the 4 plugins.
# Anti-clobber (gotcha 2026-05-18): the first `claude plugin enable` materialises
# enabledPlugins in settings.json and can flip implicitly-enabled plugins to
# disabled — so we snapshot the enabled set first and re-enable any that drop.
install_plugins() {
if ! command -v "$CLAUDE_BIN" >/dev/null 2>&1; then
warn "claude CLI not found — skipping --with-plugins. Install Claude Code, then re-run with --with-plugins (or use /tokenwar activate)."
return 0
fi
say "Installing the stack's 4 Claude Code plugins (--with-plugins)"
local mp
for mp in "${PLUGIN_MARKETPLACES[@]}"; do
"$CLAUDE_BIN" plugin marketplace add "$mp" >/dev/null 2>&1 \
|| warn "marketplace add $mp failed (may already be registered)"
done
local before_enabled
before_enabled="$(list_enabled_ids)"
local slug
for slug in "${PLUGIN_SLUGS[@]}"; do
"$CLAUDE_BIN" plugin install "$slug" >/dev/null 2>&1 || warn "install $slug failed"
"$CLAUDE_BIN" plugin enable "$slug" >/dev/null 2>&1 || true
done
local now_enabled id
now_enabled="$(list_enabled_ids)"
while IFS= read -r id; do
[[ -z "$id" ]] && continue
if ! grep -qxF "$id" <<<"$now_enabled"; then
warn "re-enabling $id (clobbered by plugin enable)"
"$CLAUDE_BIN" plugin enable "$id" >/dev/null 2>&1 || true
fi
done <<<"$before_enabled"
say "Plugins installed + enabled. Restart Claude Code to load them."
}
# Wire RTK's hook. `rtk init -g --auto-patch --hook-only` is non-interactive
# (the bare `rtk init -g` is interactive and blocks waiting for input, which
# the previous `>/dev/null 2>&1` made invisible — the install silently exited
# without writing the hook). --hook-only skips the RTK.md file; --auto-patch
# patches ~/.claude/settings.json without prompting. No-op when the binary
# isn't installed.
#
# RTK's Claude Code hook only rewrites bash INSIDE Claude Code — opencode uses a
# separate plugin runtime and never sees it. So when opencode is present we also
# install RTK's native opencode plugin, otherwise RTK saves zero tokens there.
wire_rtk_hook() {
if command -v "$RTK_BIN" >/dev/null 2>&1; then
say "Wiring RTK hook (rtk init -g --auto-patch --hook-only)"
"$RTK_BIN" init -g --auto-patch --hook-only >/dev/null 2>&1 || warn "rtk init -g --auto-patch --hook-only failed — run it manually"
if command -v "$OPENCODE_BIN" >/dev/null 2>&1; then
say "Installing RTK opencode plugin (rtk init -g --opencode)"
"$RTK_BIN" init -g --opencode >/dev/null 2>&1 \
|| warn "rtk init -g --opencode failed — run it manually (restart opencode after)"
fi
else
warn "rtk binary not found — install it with --with-rtk (or run \`rtk init -g\` after installing the RTK CLI)."
fi
}
# --with-rtk: install the RTK binary via rtk's official installer, then wire its
# hook. RTK ships prebuilt binaries for every major platform, so there is no
# toolchain to set up — no cargo, no compiling.
install_rtk() {
if command -v "$RTK_BIN" >/dev/null 2>&1; then
say "RTK already installed ($("$RTK_BIN" --version 2>/dev/null || echo present)) — skipping install"
return 0
fi
if ! command -v curl >/dev/null 2>&1; then
warn "curl not found — cannot run rtk's installer. See https://github.com/rtk-ai/rtk"
return 0
fi
say "Installing RTK via the official prebuilt installer ($RTK_INSTALL_REF)"
curl -fsSL "$RTK_INSTALL_URL" | sh >/dev/null 2>&1 || warn "rtk installer failed — see https://github.com/rtk-ai/rtk"
# rtk drops the binary in ~/.local/bin; make it visible to this process.
case ":$PATH:" in *":$RTK_LOCAL_BIN:"*) : ;; *) PATH="$RTK_LOCAL_BIN:$PATH" ;; esac
command -v "$RTK_BIN" >/dev/null 2>&1 \
&& say "RTK installed ($("$RTK_BIN" --version 2>/dev/null || echo ok))." \
|| warn "RTK install failed — see https://github.com/rtk-ai/rtk for manual steps."
}
# --with-pxpipe: install pxpipe via its published npm package. pxpipe is a
# provider proxy that turns selected prompt/context text into PNG payloads and
# records proxy-side savings in ~/.pxpipe/events.jsonl.
install_pxpipe() {
if command -v "$PXPIPE_BIN" >/dev/null 2>&1; then
say "pxpipe already installed ($("$PXPIPE_BIN" --version 2>/dev/null || echo present)) — skipping install"
return 0
fi
if ! command -v "$NPM_BIN" >/dev/null 2>&1; then
warn "npm not found — cannot install pxpipe. See https://github.com/teamchong/pxpipe"
return 0
fi
say "Installing pxpipe via npm ($PXPIPE_NPM_SPEC)"
"$NPM_BIN" install -g "$PXPIPE_NPM_SPEC" >/dev/null 2>&1 || {
warn "pxpipe install failed — see https://github.com/teamchong/pxpipe"
return 0
}
if ! command -v "$PXPIPE_BIN" >/dev/null 2>&1; then
local npm_prefix npm_pxpipe link_target
npm_prefix="$("$NPM_BIN" config get prefix 2>/dev/null || echo "")"
npm_pxpipe="${npm_prefix}/bin/${PXPIPE_BIN}"
link_target="${USER_LOCAL_BIN}/${PXPIPE_BIN}"
if [[ -n "$npm_prefix" && -x "$npm_pxpipe" ]]; then
mkdir -p "$USER_LOCAL_BIN" || {
warn "could not create $USER_LOCAL_BIN"
return 0
}
# When npm's prefix resolves to USER_LOCAL_BIN (e.g. both are
# ~/.local), source and target are the SAME file and `ln -sfn src dst`
# would create a self-referential symlink (dst -> dst), breaking
# `command -v`. In that case the binary is already where we want it —
# nothing to link.
# NB: canonicalize via the PARENT dir — `cd` onto a file path fails,
# so `cd "$file" && pwd -P` yields "" for both sides and would skip
# the link even when the paths differ. Resolve dirname, keep basename.
local src_canon dst_canon
src_canon="$(cd "$(dirname "$npm_pxpipe")" 2>/dev/null && printf '%s/%s' "$(pwd -P)" "$(basename "$npm_pxpipe")")"
dst_canon="$(cd "$(dirname "$link_target")" 2>/dev/null && printf '%s/%s' "$(pwd -P)" "$(basename "$link_target")")"
if [[ -n "$src_canon" && "$src_canon" == "$dst_canon" ]]; then
: # same file — skip the symlink
else
ln -sfn "$npm_pxpipe" "$link_target" \
|| warn "could not link pxpipe into $USER_LOCAL_BIN"
fi
fi
fi
command -v "$PXPIPE_BIN" >/dev/null 2>&1 \
&& say "pxpipe installed ($("$PXPIPE_BIN" --version 2>/dev/null || echo ok))." \
|| warn "pxpipe install finished, but pxpipe is not on PATH. Add npm's global bin directory or $USER_LOCAL_BIN to PATH."
}
# --with-graphify: install the graphify CLI, then register its assistant skill.
# Both halves are required — the CLI alone builds graphs no agent knows to query.
install_graphify() {
if command -v "$GRAPHIFY_BIN" >/dev/null 2>&1; then
say "graphify already installed ($("$GRAPHIFY_BIN" --version 2>/dev/null || echo present)) — skipping install"
elif command -v "$UV_BIN" >/dev/null 2>&1; then
say "Installing graphify via uv tool ($GRAPHIFY_PYPI_PACKAGE)"
"$UV_BIN" tool install "$GRAPHIFY_PYPI_PACKAGE" >/dev/null 2>&1 \
|| warn "uv tool install $GRAPHIFY_PYPI_PACKAGE failed — see https://github.com/Graphify-Labs/graphify"
elif command -v "$PIPX_BIN" >/dev/null 2>&1; then
say "Installing graphify via pipx ($GRAPHIFY_PYPI_PACKAGE)"
"$PIPX_BIN" install "$GRAPHIFY_PYPI_PACKAGE" >/dev/null 2>&1 \
|| warn "pipx install $GRAPHIFY_PYPI_PACKAGE failed — see https://github.com/Graphify-Labs/graphify"
elif command -v "$PIP_BIN" >/dev/null 2>&1; then
warn "no uv/pipx found — falling back to pip. Upstream recommends an isolated env (uv tool / pipx)."
"$PIP_BIN" install "$GRAPHIFY_PYPI_PACKAGE" >/dev/null 2>&1 \
|| warn "pip install $GRAPHIFY_PYPI_PACKAGE failed — see https://github.com/Graphify-Labs/graphify"
else
warn "no uv/pipx/pip found — cannot install graphify. See https://github.com/Graphify-Labs/graphify"
return 0
fi
# Both managers drop the command in ~/.local/bin, which the shell block above
# exports but this process may not have picked up yet.
case ":$PATH:" in *":$USER_LOCAL_BIN:"*) : ;; *) PATH="$USER_LOCAL_BIN:$PATH" ;; esac
if command -v "$GRAPHIFY_BIN" >/dev/null 2>&1; then
say "Registering the graphify skill (graphify install)"
"$GRAPHIFY_BIN" install >/dev/null 2>&1 \
|| warn "graphify install failed — run it manually to register the skill"
say "graphify installed ($("$GRAPHIFY_BIN" --version 2>/dev/null || echo ok))."
else
warn "graphify install finished, but graphify is not on PATH. Add $USER_LOCAL_BIN to PATH (or run \`uv tool update-shell\`)."
fi
}
# --with-openwiki: install the shared project-memory CLI. Never initialize a
# repository here: `openwiki --init` writes generated docs and invokes an LLM.
install_openwiki() {
if command -v "$OPENWIKI_BIN" >/dev/null 2>&1; then
say "OpenWiki already installed ($("$OPENWIKI_BIN" --version 2>/dev/null || echo present)) — skipping install"
return 0
fi
if ! command -v "$NPM_BIN" >/dev/null 2>&1; then
warn "npm not found — cannot install OpenWiki. Node.js 22+ is required; see https://github.com/langchain-ai/openwiki"
return 0
fi
say "Installing shared project memory via npm ($OPENWIKI_NPM_SPEC)"
"$NPM_BIN" install -g "$OPENWIKI_NPM_SPEC" >/dev/null 2>&1 || {
warn "OpenWiki install failed. Node.js 22+ is required; see https://github.com/langchain-ai/openwiki"
return 0
}
case ":$PATH:" in *":$USER_LOCAL_BIN:"*) : ;; *) PATH="$USER_LOCAL_BIN:$PATH" ;; esac
command -v "$OPENWIKI_BIN" >/dev/null 2>&1 \
&& say "OpenWiki installed ($("$OPENWIKI_BIN" --version 2>/dev/null || echo ok)). Initialize it deliberately inside a repository with: openwiki --init" \
|| warn "OpenWiki installed, but is not on PATH. Add npm's global bin directory to PATH."
}
# --with-copilot: point the installed tools at Copilot CLI's extension points.
# Delegates to scripts/copilot.sh so the wiring has ONE implementation, shared
# with `tokenwar copilot wire` — no second copy to drift.
wire_copilot_stack() {
if ! command -v "$COPILOT_BIN" >/dev/null 2>&1; then
warn "GitHub Copilot CLI not found — skipping --with-copilot (npm install -g @github/copilot, then re-run \`tokenwar copilot wire\`)."
return 0
fi
local wire_script="${INSTALL_DIR}/${COPILOT_WIRE_SCRIPT_REL}"
if [[ ! -f "$wire_script" ]]; then
warn "copilot wiring script not found at $wire_script"
return 0
fi
say "Wiring the stack into GitHub Copilot CLI"
bash "$wire_script" wire --yes || warn "Copilot wiring reported failures — see \`tokenwar copilot\`"
}
say "Wiring shell integration"
wired_any=false
for rc in "$HOME/.bashrc" "$HOME/.zshrc"; do
if [[ -f "$rc" ]]; then
wire_shell_rc "$rc" && wired_any=true
fi
done
if ! $wired_any; then
# No rc found — create ~/.bashrc so the integration lands somewhere.
: > "$HOME/.bashrc"
wire_shell_rc "$HOME/.bashrc" || warn "could not create shell integration in ~/.bashrc"
fi
# 5. plugins + rtk + pxpipe + graphify + OpenWiki (opt-in)
if $WITH_PLUGINS; then install_plugins; fi
if $WITH_RTK; then install_rtk; fi
if $WITH_PXPIPE; then install_pxpipe; fi
if $WITH_GRAPHIFY; then install_graphify; fi
if $WITH_OPENWIKI; then install_openwiki; fi
if $WITH_PLUGINS || $WITH_RTK; then wire_rtk_hook; fi
# Copilot wiring runs LAST: it reflects whatever the steps above installed, so a
# tool added in this same run is picked up rather than reported as missing.
if $WITH_COPILOT; then wire_copilot_stack; fi
if $WITH_PLUGINS && $WITH_RTK && $WITH_PXPIPE && $WITH_GRAPHIFY && $WITH_OPENWIKI; then
next_steps="The complete stack and shared project-memory layer are installed. Restart Claude Code. Inside each long-lived repository, run \`graphify .\` once and \`openwiki --init\` once; then keep them current with \`graphify update .\` after code changes and \`openwiki --update\` after merges."
elif $WITH_PLUGINS && $WITH_RTK && $WITH_PXPIPE; then
next_steps="Plugins + RTK + pxpipe installed. Add --with-graphify for structural navigation and --with-openwiki for shared project memory."
elif $WITH_PLUGINS && $WITH_RTK; then
next_steps="Plugins + RTK installed and RTK's hook wired. pxpipe not installed — add --with-pxpipe if you want proxy-side prompt-to-PNG savings. Restart Claude Code to load the plugins."
elif $WITH_PLUGINS; then
next_steps="Plugins installed (restart Claude Code). RTK not installed — add --with-rtk, or install the RTK CLI and run \`rtk init -g\`. pxpipe not installed — add --with-pxpipe."
elif $WITH_RTK; then
next_steps="RTK installed + hook wired. Install the plugins with --with-plugins (or /tokenwar activate). pxpipe not installed — add --with-pxpipe."
elif $WITH_PXPIPE; then
next_steps="pxpipe installed. Start it when you want proxy-side prompt-to-PNG savings; install the Claude plugins/RTK with --all for the full stack."
else
next_steps="Activate the core tools via the tokenwar skill:
/tokenwar activate
Or re-run with --all. OpenWiki is strongly recommended for active team repositories: its LLM generation costs tokens once, then its committed wiki saves repeated exploration for every developer and agent."
fi
if ! command -v "$OPENWIKI_BIN" >/dev/null 2>&1; then
next_steps+="
Recommended project memory is not active: re-run with --with-openwiki, then use openwiki --init in the repository."
fi
cat <<EOF
$(green 'tokenwar installed.')
Sanity check now:
bash $INSTALL_DIR/scripts/status.sh
bash $INSTALL_DIR/scripts/check.sh
bash $INSTALL_DIR/scripts/gain.sh
Statusline appears after restarting Claude Code.
Shell integration wired (reload your shell or 'source ~/.bashrc'):
tokenwar status # works in any shell — Codex, Gemini, Kimi, opencode, Copilot, plain terminal
codex / gemini / kimi / opencode / copilot
# now print the tokenwar banner on launch
$next_steps
To uninstall:
bash $INSTALL_DIR/uninstall.sh
EOF