@stemtrooper/learningcode 0.4.0 → 0.4.2
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/README.md +2 -2
- package/bin/learningcode.mjs +9 -5
- package/lib/token.mjs +19 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -174,8 +174,8 @@ You do not have a token yet, or it is not cached. Run `learningcode --login` to
|
|
|
174
174
|
enter a new one. If you never had one, ask your teacher.
|
|
175
175
|
|
|
176
176
|
**`Token rejected (401)`**
|
|
177
|
-
The token
|
|
178
|
-
`learningcode --login
|
|
177
|
+
The token was probably rotated, which revokes the previous one. Run `/spark-login`
|
|
178
|
+
to confirm, then `learningcode --login` to enter the new one.
|
|
179
179
|
|
|
180
180
|
**`AI off — ask your teacher` / `AI disabled for your account`**
|
|
181
181
|
Spark has AI switched off for your account — usually a timetable window or a
|
package/bin/learningcode.mjs
CHANGED
|
@@ -288,6 +288,9 @@ async function main() {
|
|
|
288
288
|
"it may not be a Spark token.\n",
|
|
289
289
|
);
|
|
290
290
|
}
|
|
291
|
+
// Keep Pi's stored credential and the environment in step. See the note
|
|
292
|
+
// beside saveProviderKey below.
|
|
293
|
+
await saveProviderKey(dir, PROVIDER_ID, token);
|
|
291
294
|
if (resolved.shouldCache) await writeCachedToken(dir, token);
|
|
292
295
|
} else if (explicitToken) {
|
|
293
296
|
process.stderr.write(
|
|
@@ -320,11 +323,12 @@ async function main() {
|
|
|
320
323
|
|
|
321
324
|
const childEnv = { ...process.env };
|
|
322
325
|
|
|
323
|
-
// Pi
|
|
324
|
-
//
|
|
325
|
-
//
|
|
326
|
-
//
|
|
327
|
-
//
|
|
326
|
+
// Pi prefers a stored credential in auth.json over the `$SPARK_API_KEY` the
|
|
327
|
+
// provider config names, so leaving the token only in the environment lets the
|
|
328
|
+
// two drift apart: Pi authenticates with whatever auth.json holds while the
|
|
329
|
+
// /quota and /seats commands read the environment, and a stale entry then shows
|
|
330
|
+
// up as a 401 on those two but not on completions. Writing the token to
|
|
331
|
+
// auth.json as well makes every path use the same value whichever Pi prefers.
|
|
328
332
|
const goKey = process.env.LEARNINGCODE_GO_KEY || process.env.OPENCODE_API_KEY;
|
|
329
333
|
if (goKey && process.env.LEARNINGCODE_ALLOW_ZEN !== "1") {
|
|
330
334
|
await saveProviderKey(dir, GO_PROVIDER, goKey);
|
package/lib/token.mjs
CHANGED
|
@@ -101,7 +101,10 @@ export async function resolveToken(agentDir, explicit) {
|
|
|
101
101
|
if (fromEnv) return { token: fromEnv.trim(), source: "environment" };
|
|
102
102
|
|
|
103
103
|
const cached = await readCachedToken(agentDir);
|
|
104
|
-
|
|
104
|
+
// A cached value that is not a Spark token is worse than no cache: it would be
|
|
105
|
+
// sent to Spark, rejected, and reported as a 401 that looks like a revoked
|
|
106
|
+
// token. Fall through and ask instead.
|
|
107
|
+
if (cached && isSparkToken(cached)) return { token: cached, source: "cached" };
|
|
105
108
|
|
|
106
109
|
const entered = await promptSecret(
|
|
107
110
|
"Paste your Spark API token (Bench > Issue / rotate token): ",
|
|
@@ -111,9 +114,23 @@ export async function resolveToken(agentDir, explicit) {
|
|
|
111
114
|
}
|
|
112
115
|
|
|
113
116
|
export function looksLikeToken(token) {
|
|
114
|
-
return token
|
|
117
|
+
return isSparkToken(token);
|
|
115
118
|
}
|
|
116
119
|
|
|
120
|
+
/**
|
|
121
|
+
* Whether a value could be a Spark token at all. Spark mints them as
|
|
122
|
+
* `spark_live_<secret>`, so anything else in the cache slot came from somewhere
|
|
123
|
+
* else and must not be sent to Spark.
|
|
124
|
+
*
|
|
125
|
+
* The length of the secret is deliberately not checked: Spark chooses it, and
|
|
126
|
+
* tightening this to today's value would start rejecting valid tokens the day
|
|
127
|
+
* that changes. Requiring something after the prefix is the safe minimum.
|
|
128
|
+
*/
|
|
129
|
+
export const isSparkToken = (token) =>
|
|
130
|
+
typeof token === "string" &&
|
|
131
|
+
token.startsWith(TOKEN_PREFIX) &&
|
|
132
|
+
token.length > TOKEN_PREFIX.length;
|
|
133
|
+
|
|
117
134
|
/** True when a token can be found without asking the student anything. */
|
|
118
135
|
export async function hasToken(agentDir, explicit) {
|
|
119
136
|
if (explicit) return true;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stemtrooper/learningcode",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
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",
|