Shows the UiPath CLI org, tenant, environment and token expiry above the prompt

Two small mods for Claude Code. Each mod is separate. Install only the mods that you want.
| Mod | What it does |
|---|---|
context-band | Shows a coloured bar above the prompt. The bar shows how much of the context window is full. |
uip-auth-band | Shows your UiPath CLI org, tenant, environment, and token expiry above the prompt. |
Do this one time. In a terminal, run:
claude plugin marketplace add jms-dcksn/claude-mods
Install each mod with its own command. Run only the lines for the mods that you want:
claude plugin install context-band@claude-mods
claude plugin install uip-auth-band@claude-mods
Close Claude Code and open it again. The mods load when the session starts.
/context-band to show it again.This mod needs the UiPath CLI (uip).
uip login.uip command./uip-auth to show it again.ctrl+x and then tab to move into the band. Use tab or the arrow keys to select a button, then press Enter. Press Esc to go back to the prompt.claude plugin marketplace update claude-mods
claude plugin update context-band@claude-mods
claude plugin uninstall context-band@claude-mods
git clone https://github.com/jms-dcksn/claude-mods.git
claude --plugin-dir ./claude-mods/context-band
To run the tests of a mod:
claude plugin test ./claude-mods/context-bandhooks/register.tsx 162 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { UipAuth } from '../types'
5
6const auth = atom({ plugin: 'uip-auth-band', key: 'auth' } as const, null)
7const isHidden = atom({ plugin: 'uip-auth-band', key: 'isHidden' } as const, false)
8
9const WARN_MS = 15 * 60 * 1000
10
11// "https://staging.uipath.com" -> "staging"
12function envOf(baseUrl: unknown): string {
13 if (typeof baseUrl !== 'string') return '?'
14 try {
15 return new URL(baseUrl).hostname.split('.')[0] ?? '?'
16 } catch {
17 return '?'
18 }
19}
20
21function parse(stdout: string): UipAuth {
22 let json: { Data?: Record<string, unknown> }
23 try {
24 json = JSON.parse(stdout)
25 } catch {
26 return { kind: 'error', message: 'unreadable output' }
27 }
28 const data = json.Data
29 if (!data || data.Status !== 'Logged in') return { kind: 'loggedOut' }
30 const expiresAt =
31 typeof data.ExpirationDate === 'string' ? Date.parse(data.ExpirationDate) : NaN
32 return {
33 kind: 'ok',
34 org: String(data.Organization ?? '?'),
35 tenant: String(data.Tenant ?? '?'),
36 env: envOf(data.BaseUrl),
37 expiresAt: Number.isNaN(expiresAt) ? null : expiresAt,
38 }
39}
40
41async function refresh($: EngineInterface): Promise<void> {
42 let next: UipAuth
43 try {
44 const ran = await $.process.run(['uip', 'login', 'status', '--output', 'json'], {
45 timeoutMs: 10_000,
46 })
47 next = parse(ran.stdout)
48 } catch {
49 next = { kind: 'missing' }
50 }
51 await update($, auth, () => next)
52}
53
54function background($: EngineInterface): void {
55 refresh($).catch(err => $.ui.log(`uip-auth-band: ${String(err)}`, { to: 'debug' }))
56}
57
58function clock(ms: number): string {
59 const d = new Date(ms)
60 return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`
61}
62
63export const register: Register = on => {
64 on('session.start', async ($, e, next) => {
65 await $.command.register({
66 name: 'uip-auth',
67 description: 'Show the UiPath auth band again and re-check uip login status',
68 })
69 const started = await next(e)
70 background($)
71 return started
72 })
73
74 on('command.run', { command: 'uip-auth' }, async $ => {
75 await update($, isHidden, () => false)
76 await refresh($)
77 return { text: 'UiPath auth band shown and refreshed.' }
78 })
79
80 // Re-check after any shell command that runs the uip CLI (login, logout, tenant switch).
81 on('tool.call', { tool: 'Bash' }, async ($, e, next) => {
82 const ran = await next(e)
83 if (/(^|[\s;&|(])uip\s/.test(e.command)) background($)
84 return ran
85 }).catch(($, e, next) => next(e))
86
87 // Re-check after each turn to catch a login done in another terminal.
88 on('turn.complete', ($, e, next) => {
89 background($)
90 return next(e)
91 })
92
93 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
94 const state = await read($, auth)
95 if (e.props.hasSurvey || state === null || (await read($, isHidden))) {
96 return next(e)
97 }
98
99 const { Box, Button, Text } = $.ui.resolve(e)
100 const now = await $.clock.now()
101
102 let dot: 'success' | 'warning' | 'error' | 'subtle' = 'success'
103 let main = ''
104 let detail = ''
105 let detailColor: 'subtle' | 'warning' | 'error' = 'subtle'
106
107 if (state.kind === 'ok') {
108 main = `${state.org} / ${state.tenant}`
109 detail = state.env
110 if (state.expiresAt !== null) {
111 const left = state.expiresAt - now
112 if (left <= 0) {
113 dot = 'error'
114 detailColor = 'error'
115 detail += ' · token expired'
116 } else if (left < WARN_MS) {
117 dot = 'warning'
118 detailColor = 'warning'
119 detail += ` · expires in ${Math.ceil(left / 60000)} min`
120 } else {
121 detail += ` · exp ${clock(state.expiresAt)}`
122 }
123 }
124 } else if (state.kind === 'loggedOut') {
125 dot = 'error'
126 main = 'not logged in'
127 detail = 'run: uip login'
128 } else if (state.kind === 'missing') {
129 dot = 'subtle'
130 main = 'uip CLI not found'
131 } else {
132 dot = 'error'
133 main = 'status check failed'
134 detail = state.message
135 }
136
137 const mine = (
138 <Box>
139 <Text color={dot}>● </Text>
140 <Text bold>UiPath </Text>
141 <Text wrap="truncate-end">
142 {main}
143 </Text>
144 {detail !== '' && <Text color={detailColor}> {detail}</Text>}
145 <Text> </Text>
146 <Button key="refresh" label="Refresh" onPress={() => refresh($)} />
147 <Text> </Text>
148 <Button key="hide" label="Hide" onPress={() => update($, isHidden, () => true)} />
149 </Box>
150 )
151
152 // The band has one instance: keep whatever another mod draws there below ours.
153 const below = await next(e)
154 return (
155 <Box flexDirection="column">
156 {mine}
157 {below}
158 </Box>
159 )
160 })
161}
162types/index.d.ts 12 lines1export type UipAuth =
2 | { kind: 'ok'; org: string; tenant: string; env: string; expiresAt: number | null }
3 | { kind: 'loggedOut' }
4 | { kind: 'missing' }
5 | { kind: 'error'; message: string }
6
7declare module 'claude-code' {
8 interface PluginState {
9 'uip-auth-band': { auth: UipAuth | null; isHidden: boolean }
10 }
11}
12