@stemtrooper/learningcode 0.2.1 → 0.3.1

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 CHANGED
@@ -1,34 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 TLC (The Learning Curve) / stemtrooper
4
-
5
- learningcode is a distribution wrapper. It does not contain source copied from
6
- Pi; it depends on @earendil-works/pi-coding-agent at runtime.
7
-
8
- That dependency is MIT licensed:
9
-
10
- MIT License
11
- Copyright (c) Mario Zechner (and contributors)
12
-
13
- Permission is hereby granted, free of charge, to any person obtaining a copy
14
- of this software and associated documentation files (the "Software"), to deal
15
- in the Software without restriction, including without limitation the rights
16
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
17
- copies of the Software, and to permit persons to whom the Software is
18
- furnished to do so, subject to the following conditions:
19
-
20
- The above copyright notice and this permission notice shall be included in all
21
- copies or substantial portions of the Software.
22
-
23
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
29
- SOFTWARE.
30
-
31
- The MIT licence terms below govern learningcode itself.
3
+ Copyright (c) 2026 TLC (The Learning Curve)
32
4
 
33
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
34
6
  of this software and associated documentation files (the "Software"), to deal
package/NOTICE ADDED
@@ -0,0 +1,42 @@
1
+ learningcode
2
+ Copyright (c) 2026 TLC (The Learning Curve)
3
+
4
+ This product includes software developed by third parties.
5
+
6
+ --------------------------------------------------------------------------------
7
+ Pi — https://github.com/earendil-works/pi
8
+ MIT License
9
+ Copyright (c) Mario Zechner (and contributors)
10
+
11
+ learningcode is a distribution wrapper and contains no source copied from Pi. It
12
+ depends on @earendil-works/pi-coding-agent at runtime, and on
13
+ @earendil-works/pi-tui. Both are MIT licensed:
14
+
15
+ Permission is hereby granted, free of charge, to any person obtaining a copy
16
+ of this software and associated documentation files (the "Software"), to deal
17
+ in the Software without restriction, including without limitation the rights
18
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
19
+ copies of the Software, and to permit persons to whom the Software is
20
+ furnished to do so, subject to the following conditions:
21
+
22
+ The above copyright notice and this permission notice shall be included in all
23
+ copies or substantial portions of the Software.
24
+
25
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
26
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
27
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
28
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
29
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
30
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
31
+ SOFTWARE.
32
+
33
+ Pi's own NOTICE credits Ratatui (MIT, Florian Dehau and Ratatui Developers).
34
+
35
+ --------------------------------------------------------------------------------
36
+ Trademarks
37
+
38
+ "The Learning Curve", TLC and Spark are trademarks of The Learning Curve.
39
+
40
+ learningcode is not affiliated with, endorsed by, or connected to Pi, OpenCode,
41
+ Anomaly, or any model provider reachable through this client. Model names
42
+ belonging to their respective owners are used only to identify what is available.
package/README.md CHANGED
@@ -1,55 +1,140 @@
1
1
  # learningcode
2
2
 
3
- A terminal coding agent for TLC students, wired to [Spark](../spark) — the
4
- inference gateway that holds per-student tokens, timetable policy, seat limits
5
- and weekly quota.
3
+ A terminal coding agent for **The Learning Curve** students, running on TLC's own
4
+ inference through **TLC-Spark**.
6
5
 
7
- Students run one command:
6
+ `learningcode` is a purpose-built client for the TLC-Spark token plan. It is not a
7
+ general-purpose AI coding tool: the model, the quota and the seat limits all belong
8
+ to TLC, and every token you spend comes out of your course allocation.
9
+
10
+ ---
11
+
12
+ ## You will need a TLC-Spark token
13
+
14
+ `learningcode` cannot reach a model on its own. It connects to the TLC-Spark
15
+ gateway, which checks who you are and whether you are allowed to use it right now.
16
+
17
+ **Ask your teacher for a Spark API token.** In the Spark bench, go to
18
+ **Issue / rotate token**. The token is shown **once** and cannot be retrieved
19
+ afterwards, so copy it immediately. It looks like `spark_live_…`.
20
+
21
+ Without a token, `learningcode` will not start a conversation. That is by design:
22
+ there is no shared school key and no fallback to somebody else's account.
23
+
24
+ ---
25
+
26
+ ## Requirements
27
+
28
+ | | |
29
+ |---|---|
30
+ | **Node.js** | **22.19.0 or newer** (24.x LTS recommended) |
31
+ | **Disk** | about 120 MB |
32
+ | **Network** | must reach `spark.learning.com.my` |
33
+ | **Token** | your personal `spark_live_…` from the Spark bench |
34
+ | **Terminal** | 51+ columns for the banner; 99+ for the large one |
35
+
36
+ Node 22.19 is the floor because the underlying agent runtime requires it. If you
37
+ install Node and `npm i` only *warns*, check `node --version` — an older Node
38
+ produces a confusing error later rather than at install time.
39
+
40
+ ---
41
+
42
+ ## Install
43
+
44
+ ### Windows (PowerShell)
45
+
46
+ ```powershell
47
+ node --version
48
+ npm install -g @stemtrooper/learningcode
49
+ learningcode --version
50
+ ```
51
+
52
+ If PowerShell refuses to run the command because script execution is disabled:
53
+
54
+ ```powershell
55
+ Set-ExecutionPolicy -Scope CurrentUser RemoteSigned
56
+ ```
57
+
58
+ ### macOS / Linux
8
59
 
9
60
  ```bash
10
- npm i -g @stemtrooper/learningcode
11
- learningcode
61
+ node --version
62
+ npm install -g @stemtrooper/learningcode
63
+ learningcode --version
12
64
  ```
13
65
 
14
- On first run it asks for the personal `spark_live_` token from the Spark bench,
15
- caches it with `0600`, writes the TLC-Spark provider config, and hands off to
16
- Pi with the model pinned.
66
+ ### Termux (Android)
17
67
 
68
+ No root and no `proot-distro` needed.
69
+
70
+ ```bash
71
+ pkg install nodejs-lts npm
72
+ node --version
73
+ npm install -g @stemtrooper/learningcode
74
+ learningcode --version
18
75
  ```
19
- baseURL https://spark.learning.com.my/v1
20
- model TLC-Spark
76
+
77
+ Install `npm` explicitly: Termux stopped bundling it with Node at 25.3.0.
78
+ Prefer `nodejs-lts` (24.x) over `nodejs` (26.x).
79
+
80
+ ---
81
+
82
+ ## First run
83
+
84
+ ```bash
85
+ cd ~/your-project
86
+ learningcode
21
87
  ```
22
88
 
23
- ## What students get
89
+ It asks for your Spark token, stores it with owner-only permissions in
90
+ `~/.learningcode/agent/spark-token`, and writes its configuration. The token is
91
+ only ever read from your own machine.
24
92
 
25
- Pi's four built-in tools — `read`, `bash`, `edit`, `write` — against
26
- Qwen3.8-27B. Two extra commands, because Spark is not a plain OpenAI endpoint:
93
+ You now have an agent that can read files, run shell commands, and edit code in
94
+ the directory you started it from. **It runs those commands on your machine, as
95
+ you.** Nothing runs on a microcontroller, and Spark never executes anything on
96
+ your behalf.
27
97
 
28
- | Command | Why it exists |
29
- |---|---|
30
- | `/quota` | Tokens used against today's limit, and whether AI is enabled for your account |
31
- | `/seats` | Live seat queue, so a student can see why a turn is waiting |
98
+ ---
32
99
 
33
- `session_start` also warns once if you cross 80% of your daily quota or if a
34
- teacher has switched AI off for your account.
100
+ ## Using it
35
101
 
36
- ## Why not just ship Pi?
102
+ Type a request in plain language.
37
103
 
38
- Because Spark's limits are the design constraint, not the model:
104
+ ```
105
+ write a function that reads notes.txt and returns the average resistance
106
+ why does my ESP32 reset when I power the servo
107
+ explain this error: "Brown detector failed"
108
+ ```
39
109
 
40
- - **32K max context, 16K default.** Agent sessions burn this fast. `learningcode`
41
- starts Pi with `--no-skills`, since skills are pure prompt-token spend.
42
- - **One active generation per student.** Subagents and parallel tool calls would
43
- only earn `429`s. Pi is serial by default and this wrapper does not turn that off.
44
- - **250K tokens per week.** Frugality is the product, so the budget is visible
45
- instead of mysterious.
110
+ Useful commands, typed inside the session:
46
111
 
47
- Stock Pi does not know `/v1/me/quota` or `/v1/queue` exist. That gap is the whole
48
- reason this wrapper exists.
112
+ | Command | What it does |
113
+ |---|---|
114
+ | `/help` | list everything available |
115
+ | `/quota` | your token spend today |
116
+ | `/seats` | how many seats are free |
117
+ | `/model` | switch model |
118
+ | `/hotkeys` | keyboard shortcuts |
49
119
 
50
- ## Banner
120
+ `/quota` and `/seats` talk to Spark directly. They are the two commands a stock
121
+ AI coding tool does not have, because those endpoints are not part of any public
122
+ API.
51
123
 
52
- The LEARNINGCODE block banner replaces Pi's header at startup:
124
+ One-off, non-interactive:
125
+
126
+ ```bash
127
+ learningcode -p "explain main.py"
128
+ learningcode -c # continue your last session
129
+ learningcode --mode json # machine-readable output
130
+ ```
131
+
132
+ ---
133
+
134
+ ## What it looks like
135
+
136
+ The TLC brand theme and banner ship with the package, so there is nothing to
137
+ configure.
53
138
 
54
139
  ```
55
140
  ██╗ ███████╗ █████╗ ██████╗ ███╗ ██╗██╗███╗ ██╗ ██████╗ ██████╗ ██████╗ ██████╗ ███████╗
@@ -58,128 +143,121 @@ The LEARNINGCODE block banner replaces Pi's header at startup:
58
143
  ██║ ██╔══╝ ██╔══██║██╔══██╗██║╚██╗██║██║██║╚██╗██║██║ ██║██║ ██║ ██║██║ ██║██╔══╝
59
144
  ███████╗███████╗██║ ██║██║ ██║██║ ╚████║██║██║ ╚████║╚██████╔╝╚██████╗╚██████╔╝██████╔╝███████╗
60
145
  ╚══════╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═══╝╚═╝╚═╝ ╚═══╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝
61
- ═════════════════════════════════════════════════════════════════════════════════════════════════
62
146
  The Learning Curve · Sarawak
63
- /help commands · /quota today's spend · /hotkeys keys
64
147
  ```
65
148
 
66
- The figlet "ANSI Shadow" face, 97 columns wide, kept verbatim because the
67
- double-line box characters only align if every row keeps its exact offset.
149
+ On a narrower terminal it steps down to a condensed banner, then to the wordmark,
150
+ rather than drawing art that would be clipped.
68
151
 
69
- Colour comes from the active theme, so it stays legible in light and dark.
152
+ A footer shows your remaining quota whenever you are connected to Spark:
70
153
 
71
- **It needs a 99 column terminal.** Below that it collapses to a wordmark,
72
- rather than drawing art that would be clipped into something that looks broken.
73
- An 80 column terminal will show the compact form, so widen the window or reduce
74
- the art.
154
+ ```
155
+ █████░░░░░ 50% 125k / 250k today
156
+ ```
75
157
 
76
- It installs via `ctx.ui.setHeader`, the supported way to brand a fork.
158
+ ---
77
159
 
78
- ## Other providers
160
+ ## Troubleshooting
79
161
 
80
- Any `--model` other than `tlc-spark/...` skips the Spark token and the Spark
81
- health check, so you can work while Spark is down:
162
+ **`Node 22.19.0 or newer is required`**
163
+ Upgrade Node, then reinstall: `npm i -g @stemtrooper/learningcode`.
82
164
 
83
- ```bash
84
- export OPENCODE_API_KEY=...
85
- learningcode --model opencode-go/glm-5.3-flash
86
- ```
165
+ **`No Spark API token configured`**
166
+ You do not have a token yet, or it is not cached. Run `learningcode --login` to
167
+ enter a new one. If you never had one, ask your teacher.
168
+
169
+ **`Token rejected (401)`**
170
+ The token is wrong, or it was rotated and the old one no longer works.
171
+ `learningcode --login`.
87
172
 
88
- **OpenCode Zen is excluded.** Zen and Go share one `OPENCODE_API_KEY`, so leaving
89
- that variable in place authenticates both and exposes Zen's 111 pay-per-use models
90
- alongside the 29 a Go subscription covers, at up to $20/M output. learningcode
91
- moves the key into Pi's `auth.json` under `opencode-go` alone, which leaves Zen
92
- credential-less so it never registers. Set `LEARNINGCODE_ALLOW_ZEN=1` to opt in.
173
+ **`AI off — ask your teacher` / `AI disabled for your account`**
174
+ Spark has AI switched off for your account — usually a timetable window or a
175
+ policy setting. This is not something you can fix.
93
176
 
94
- ## Theme
177
+ **`You already have a generation running`**
178
+ Spark allows one active generation per student. Stop the running one first.
95
179
 
96
- Two TLC themes ship with the package and are seeded into
97
- `~/.learningcode/agent/themes/` on first run:
180
+ **`Every seat is taken` / queue position**
181
+ All seats are busy. `learningcode` prints your position; try again shortly.
98
182
 
99
- | Theme | Accent | Sampled from |
100
- |---|---|---|
101
- | `tlc-dark` | `#29c8f2` | `TLC_BLACKBGND.png` |
102
- | `tlc-light` | `#0dacd6` | `TLC_WHITEBGND.png` |
183
+ **The banner looks like plain text**
184
+ Your terminal is narrower than 51 columns. Widen it.
103
185
 
104
- The values are read out of the logo pixels, not eyeballed. The logo ships two
105
- cyans because the darker one has to hold contrast on a white field, which maps
106
- exactly onto Pi's light/dark split.
186
+ ---
107
187
 
108
- A seeded theme is **never overwritten**, so an edit survives upgrades. Override
109
- the default with `--theme`, or `LEARNINGCODE_THEME=tlc-light`. To try Pi's own:
188
+ ## Configuration
189
+
190
+ Everything lives in `~/.learningcode/agent/`. Nothing is uploaded anywhere except
191
+ to Spark, and no telemetry is sent.
192
+
193
+ | Variable | Purpose |
194
+ |---|---|
195
+ | `LEARNINGCODE_DIR` | config location (default `~/.learningcode/agent`) |
196
+ | `LEARNINGCODE_SPARK_BASE_URL` | point at a different Spark deployment |
197
+ | `LEARNINGCODE_TOKEN` | supply a token without using the cached file |
198
+ | `LEARNINGCODE_THEME` | `tlc-dark` (default), `tlc-light`, or any Pi theme |
199
+ | `LEARNINGCODE_PI_FLAGS` | extra flags passed to the agent on every launch |
110
200
 
111
201
  ```bash
112
- learningcode --theme dark
202
+ learningcode --show-config # show resolved paths and settings
203
+ learningcode --login # enter a new token
204
+ learningcode --list-models # every reachable model
205
+ learningcode --theme dark # use the upstream theme instead of TLC's
113
206
  ```
114
207
 
115
- 28 of Pi's 56 colour tokens are re-tinted: the accent, borders, greys, markdown,
116
- syntax highlighting, diff colours, selected backgrounds, and the thinking-level
117
- ramp. Semantic colours (red, green, yellow) are left alone, because those mean
118
- error, success and warning rather than anything about the brand. The thinking ramp
119
- runs grey to cyan and keeps red at the top, where it still means "this is getting
120
- expensive".
121
-
122
- The banner does **not** follow the theme. It uses the brand cyan directly, the way
123
- Pi's own logo does, because a wordmark that changes colour with whatever theme is
124
- active stops being a wordmark. It picks the right one of the two cyans from the
125
- terminal's colour mode.
208
+ **Your edits are kept.** If you change the theme or the model configuration,
209
+ upgrading `learningcode` will not overwrite your copy.
126
210
 
127
- ## Footer
211
+ ---
128
212
 
129
- While connected to Spark, a footer shows today's token spend:
213
+ ## For educators
130
214
 
131
- ```
132
- █████░░░░░ 50% 125k / 250k today
133
- ```
215
+ <details>
216
+ <summary>Running a local Spark, and other model providers</summary>
134
217
 
135
- Spark's quota endpoint is not part of the OpenAI API, so no stock harness shows
136
- this. It polls at most every two minutes, since Spark allows one active
137
- generation per student and a per-turn refresh would add latency to the thing the
138
- student is waiting on. When AI is switched off for an account the footer says so
139
- instead of showing a bar.
218
+ **Local bench.** `learningcode --base-url http://localhost:3000/v1` points the
219
+ client at a Spark running on your own machine, for demos without spending course
220
+ quota.
140
221
 
141
- Off Spark, or with no token, it says so rather than drawing an empty bar that would
142
- read as "you have spent nothing".
222
+ **Other providers.** Any `--model` other than `tlc-spark/…` skips the Spark token
223
+ and health check entirely:
143
224
 
144
- ## Configuration
225
+ ```bash
226
+ export OPENCODE_API_KEY=...
227
+ learningcode --model opencode-go/glm-5.3-flash
228
+ ```
145
229
 
146
- `~/.learningcode/agent/models.json` is created on first run and **never
147
- overwritten** — local edits survive upgrades. The provider is seeded with these
148
- compatibility flags, each of which matches a field Spark either rejects or drops:
230
+ This uses **your own** provider account and **your own** credits, with no Spark
231
+ quota, seat limit or audit trail. `learningcode` deliberately excludes OpenCode
232
+ Zen, which bills per token, so a stale entry cannot silently spend money. It is
233
+ still not something to hand to students.
149
234
 
150
- | Flag | Value | Spark behaviour |
151
- |---|---|---|
152
- | `supportsDeveloperRole` | `false` | message union is `system \| user \| assistant \| tool`; there is no `developer` |
153
- | `maxTokensField` | `"max_tokens"` | `v1.ts` reads `body.max_tokens` only |
154
- | `supportsReasoningEffort` | `false` | not forwarded to the runtime |
155
- | `supportsStore` | `false` | not implemented |
235
+ **Seat awareness.** Spark enforces one active generation per student, so the
236
+ client runs strictly serially with subagents off. Expect a 429 rather than
237
+ parallel fan-out.
156
238
 
157
- Environment variables:
239
+ </details>
158
240
 
159
- | Variable | Purpose |
160
- |---|---|
161
- | `LEARNINGCODE_DIR` | agent directory (default `~/.learningcode/agent`) |
162
- | `LEARNINGCODE_SPARK_BASE_URL` | point at another Spark deployment |
163
- | `LEARNINGCODE_TOKEN` | Spark token, skips the cached file |
164
- | `LEARNINGCODE_PI_FLAGS` | extra flags appended to every launch |
241
+ ---
165
242
 
166
- ```bash
167
- learningcode --show-config # resolved paths, model, token prefix
168
- learningcode --base-url http://localhost:3000/v1 # local bench
169
- learningcode --login # re-enter a rotated token
170
- learningcode -c # continue last session
171
- learningcode -p "explain main.py"
172
- learningcode --mode json # machine-readable event stream
173
- ```
243
+ ## How it works
174
244
 
175
- Anything not listed as a `learningcode` flag is passed straight to Pi.
245
+ `learningcode` is a thin wrapper around [Pi](https://github.com/earendil-works/pi),
246
+ an open-source coding agent by Mario Zechner. It adds the TLC-Spark provider
247
+ configuration, the TLC theme and banner, and the quota and seat commands.
176
248
 
177
- ## Pinning
249
+ The underlying model is **Qwen3.8-27B**, served through TLC-Spark. Every turn is a
250
+ separate request, takes a seat, and spends quota — an agent session that reads ten
251
+ files costs roughly eleven turns. That is why the footer exists.
178
252
 
179
- `@earendil-works/pi-coding-agent` is pinned to an exact version, not a range. Pi
180
- ships breaking changes daily, and a student's install must not change underneath
181
- them mid-term. Bump it deliberately, after testing against a live pod.
253
+ Pi is pinned to an exact version on purpose: the runtime ships breaking changes
254
+ often, and a student's install must not change underneath them mid-term.
182
255
 
183
256
  ## Licence
184
257
 
185
- MIT. Depends on Pi, also MIT. See [LICENSE](LICENSE).
258
+ MIT. `learningcode` is distributed under the MIT licence and depends on Pi, which
259
+ is also MIT. See [LICENSE](LICENSE).
260
+
261
+ Not affiliated with, endorsed by, or connected to Pi, OpenCode, Anomaly, or any of
262
+ the model providers reachable through this client. "The Learning Curve", TLC and
263
+ Spark are trademarks of The Learning Curve.
@@ -5,22 +5,25 @@ import { foregroundAnsi, rgbColor } from "@earendil-works/pi-tui";
5
5
  * LEARNINGCODE startup banner.
6
6
  *
7
7
  * Pi lets an extension replace the whole header, which is the supported way to
8
- * brand a fork without touching Pi's internals. The built-in header is Pi's own
9
- * logo plus key hints; we trade that for the TLC banner and a line of slash
10
- * commands, so nothing is claimed about keybindings we cannot read.
8
+ * brand a fork without touching Pi's internals.
11
9
  *
12
- * The art is the figlet "ANSI Shadow" face, kept verbatim rather than rebuilt from
13
- * a glyph map: the double-line box characters only line up if every row keeps
14
- * its exact offset, and a one-space drift makes the whole thing look broken.
10
+ * Three tiers, because the full face does not fit everywhere. A phone will never
11
+ * have 97 columns, and clipping box characters looks like a rendering bug rather
12
+ * than a design, so the banner steps down instead:
15
13
  *
16
- * Rendering uses the active theme's colour tokens (`accent`, `border`, `dim`,
17
- * `muted`) so the banner stays legible in light and dark terminals instead of
18
- * hard-coding colours that break on one of them.
14
+ * >= 99 columns figlet "ANSI Shadow", 97 wide, 6 rows
15
+ * >= 51 columns condensed 4 row face, 47 wide
16
+ * below that the wordmark
17
+ *
18
+ * The full art is stored verbatim rather than rebuilt from a glyph map. That face
19
+ * only aligns because every row keeps its exact offset, the top row flush left
20
+ * and the rest flush too; a one-space drift makes the whole banner look broken.
21
+ * A test asserts those offsets.
19
22
  */
20
23
 
21
24
  /**
22
- * Only the parts of Pi's Theme this file touches, to keep the runtime import
23
- * down to the two colour helpers it actually needs.
25
+ * Only the parts of Pi's Theme this file touches, to keep the runtime import down
26
+ * to the two colour helpers actually needed.
24
27
  */
25
28
  type ThemeLike = {
26
29
  fg(token: string, text: string): string;
@@ -45,14 +48,20 @@ type HeaderComponent = { render(width: number): string[] };
45
48
  *
46
49
  * Note this must not go through `theme.fg()`. That resolves theme *tokens*, and
47
50
  * `Theme.tokenAnsi` throws `Unknown theme color: #29c8f2` for anything that is
48
- * not one, so a raw hex there crashes the header at render time. `foregroundAnsi`
49
- * takes a colour value directly, and unlike a hand-rolled escape it still
50
- * degrades to 256 colour on terminals without truecolour support.
51
+ * not one, so a raw hex there crashes the header at render time.
52
+ * `foregroundAnsi` takes a colour value directly, and unlike a hand-rolled escape
53
+ * it still degrades to 256 colour on terminals without truecolour support.
51
54
  */
52
55
  const CYAN_DARK_BG = rgbColor(0x29, 0xc8, 0xf2);
53
56
  const CYAN_LIGHT_BG = rgbColor(0x0d, 0xac, 0xd6);
54
57
  const RESET = "\x1b[0m";
55
58
 
59
+ const WORD = "LEARNINGCODE";
60
+ const PAD = " ";
61
+ const TAGLINE = "The Learning Curve · Sarawak";
62
+ const HINTS = "/help commands · /quota today's spend · /hotkeys keys";
63
+
64
+ /** figlet "ANSI Shadow", verbatim. 97 columns, 6 rows. */
56
65
  const ART: readonly string[] = [
57
66
  "██╗ ███████╗ █████╗ ██████╗ ███╗ ██╗██╗███╗ ██╗ ██████╗ ██████╗ ██████╗ ██████╗ ███████╗",
58
67
  "██║ ██╔════╝██╔══██╗██╔══██╗████╗ ██║██║████╗ ██║██╔════╝ ██╔════╝██╔═══██╗██╔══██╗██╔════╝",
@@ -62,26 +71,47 @@ const ART: readonly string[] = [
62
71
  "╚══════╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═══╝╚═╝╚═╝ ╚═══╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝",
63
72
  ];
64
73
 
65
- const TAGLINE = "The Learning Curve · Sarawak";
66
- const HINTS = "/help commands · /quota today's spend · /hotkeys keys";
74
+ /**
75
+ * Condensed fallback for narrow terminals. Three columns per glyph, which is
76
+ * the most a twelve letter word can be compressed before the letterforms stop
77
+ * reading: an N needs four columns to show its diagonal, and dropping it to
78
+ * three turns the letter into a filled block.
79
+ */
80
+ const CONDENSED_GLYPHS: Record<string, readonly string[]> = {
81
+ L: ["█ ", "█ ", "█ ", "███"],
82
+ E: ["███", "█ ", "███", "███"],
83
+ A: [" █ ", "█ █", "███", "█ █"],
84
+ R: ["███", "█ █", "███", "█ █"],
85
+ N: ["█ █", "███", "█ █", "█ █"],
86
+ I: ["███", " █ ", " █ ", "███"],
87
+ G: ["███", "█ ", "█ █", "███"],
88
+ C: ["███", "█ ", "█ ", "███"],
89
+ O: ["███", "█ █", "█ █", "███"],
90
+ D: ["███", "█ █", "█ █", "███"],
91
+ };
67
92
 
68
- const PAD = " ";
93
+ const condensedArt = (): string[] => {
94
+ const rows = CONDENSED_GLYPHS[WORD[0]].length;
95
+ return Array.from({ length: rows }, (_, row) =>
96
+ [...WORD].map((letter) => CONDENSED_GLYPHS[letter][row]).join(" ").trimEnd(),
97
+ );
98
+ };
69
99
 
70
100
  /** A copy, so a caller mutating the result cannot corrupt later renders. */
71
101
  export const bannerArt = (): string[] => [...ART];
72
102
 
103
+ export const condensedArt_ = condensedArt;
104
+
73
105
  export const bannerWidth = (): number => Math.max(...ART.map((line) => line.length));
106
+ export const condensedWidth = (): number => Math.max(...condensedArt().map((line) => line.length));
74
107
 
75
- /**
76
- * The art is 97 columns, which does not fit the classic 80 column terminal.
77
- * Below the art plus its indent we show a wordmark instead, because clipped box
78
- * characters look like a rendering bug rather than a design.
79
- */
108
+ /** Art plus its indent. Derived, never guessed. */
80
109
  const MIN_FULL_WIDTH = bannerWidth() + PAD.length;
110
+ const MIN_CONDENSED_WIDTH = condensedWidth() + PAD.length;
81
111
 
82
112
  export function createBanner(theme: ThemeLike): HeaderComponent {
83
- const art = bannerArt();
84
- const artWidth = bannerWidth();
113
+ const full = bannerArt();
114
+ const small = condensedArt();
85
115
 
86
116
  // Two different things, easy to swap by accident:
87
117
  // appearance light or dark, decides which brand cyan is readable
@@ -95,16 +125,23 @@ export function createBanner(theme: ThemeLike): HeaderComponent {
95
125
 
96
126
  return {
97
127
  render(width: number): string[] {
98
- if (width < MIN_FULL_WIDTH) {
99
- return ["", ink(PAD + "learningcode"), theme.fg("muted", PAD + TAGLINE)];
128
+ if (width >= MIN_FULL_WIDTH) {
129
+ const lines = ["", ...full.map((line) => ink(PAD + line))];
130
+ lines.push(theme.fg("border", PAD + "═".repeat(bannerWidth())));
131
+ lines.push(theme.fg("muted", PAD + TAGLINE));
132
+ lines.push(theme.fg("dim", PAD + HINTS));
133
+ lines.push("");
134
+ return lines;
135
+ }
136
+
137
+ if (width >= MIN_CONDENSED_WIDTH) {
138
+ const lines = ["", ...small.map((line) => ink(PAD + line))];
139
+ lines.push(theme.fg("muted", PAD + TAGLINE));
140
+ lines.push("");
141
+ return lines;
100
142
  }
101
143
 
102
- const lines = ["", ...art.map((line) => ink(PAD + line))];
103
- lines.push(theme.fg("border", PAD + "═".repeat(artWidth)));
104
- lines.push(theme.fg("muted", PAD + TAGLINE));
105
- lines.push(theme.fg("dim", PAD + HINTS));
106
- lines.push("");
107
- return lines;
144
+ return ["", ink(PAD + "learningcode"), theme.fg("muted", PAD + TAGLINE)];
108
145
  },
109
146
  };
110
147
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stemtrooper/learningcode",
3
- "version": "0.2.1",
3
+ "version": "0.3.1",
4
4
  "description": "TLC Spark coding agent for students: Pi wired to the Spark OpenAI-compatible endpoint with per-student tokens, quota and seat-queue awareness.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -13,7 +13,8 @@
13
13
  "lib",
14
14
  "extensions",
15
15
  "README.md",
16
- "LICENSE"
16
+ "LICENSE",
17
+ "NOTICE"
17
18
  ],
18
19
  "scripts": {
19
20
  "start": "node ./bin/learningcode.mjs",