@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 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 is wrong, or it was rotated and the old one no longer works.
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
@@ -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 reads provider credentials from auth.json before the environment, and
324
- // auth.json is keyed per provider. Storing the key under opencode-go alone
325
- // authenticates Go while leaving Zen credential-less, so its 111 pay-per-use
326
- // models never register. Zen and Go otherwise share OPENCODE_API_KEY, so
327
- // leaving the env var in place would expose both.
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
- if (cached) return { token: cached, source: "cached" };
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.startsWith(TOKEN_PREFIX);
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.0",
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",