pi-hypercharm-provider 1.3.40 → 1.3.42

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.
@@ -0,0 +1,60 @@
1
+ name: Update models
2
+
3
+ on:
4
+ schedule:
5
+ - cron: "7 0 * * *" # 07:07 Asia/Ho_Chi_Minh
6
+ workflow_dispatch:
7
+
8
+ # Publishing uses npm Trusted Publishing (OIDC) bound to this workflow file, so
9
+ # no npm token is stored. A version pushed while publishing was unavailable is
10
+ # published by the next run that finds it missing from npm.
11
+ permissions:
12
+ contents: write
13
+ id-token: write
14
+
15
+ concurrency:
16
+ group: update-models
17
+ cancel-in-progress: false
18
+
19
+ jobs:
20
+ update:
21
+ runs-on: ubuntu-latest
22
+ timeout-minutes: 15
23
+ steps:
24
+ - uses: actions/checkout@v5
25
+
26
+ - uses: actions/setup-node@v5
27
+ with:
28
+ node-version: 24
29
+
30
+ - name: Fetch the model catalog
31
+ env:
32
+ HYPERCHARM_API_KEY: ${{ secrets.HYPERCHARM_API_KEY }}
33
+ run: node scripts/update-models.js
34
+
35
+ - name: Commit, bump and push
36
+ run: |
37
+ if git diff --quiet; then
38
+ echo "No model changes."
39
+ exit 0
40
+ fi
41
+ git config user.name "github-actions[bot]"
42
+ git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
43
+ git add -A
44
+ git commit -m "feat: update hypercharm models from API"
45
+ npm version patch --no-git-tag-version
46
+ version=$(node -p "require('./package.json').version")
47
+ git commit -am "chore(release): v$version"
48
+ git push origin HEAD:main
49
+
50
+ - name: Publish to npm if this version is missing
51
+ run: |
52
+ name=$(node -p "require('./package.json').name")
53
+ version=$(node -p "require('./package.json').version")
54
+ if npm view "$name@$version" version >/dev/null 2>&1; then
55
+ echo "$name@$version is already on npm."
56
+ exit 0
57
+ fi
58
+ # Trusted publishing needs npm 11.5.1 or newer.
59
+ npm install --global npm@latest
60
+ npm publish --access public --provenance
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.3.42
4
+
5
+ - Pin Pi SDK development dependencies to 1.2.0 while retaining wildcard host peers; verify real manifest loading, lifecycle and native/bundled Pi hosts offline.
6
+
7
+ ## 1.3.41
8
+
9
+ - `/login` while the session-start catalog revalidation is still in flight now fetches the new account's catalog right away: the forced refresh aborts the old-credential revalidation and waits for it to release the sync lock instead of skipping as "locked" until the next session start.
10
+
3
11
  ## 1.3.40
4
12
 
5
13
  - Quiet startup sync: background `/v1/provider` revalidation runs only with credentials, only when the disk cache is older than 1 hour (`PI_PROVIDER_SYNC_TTL_MS`, `0` = always), missing, or fetched with different credentials, and only in one pi process at a time (lock file next to the cache). Failed refreshes back off for 5 minutes; sessions that skip adopt a cache refreshed by another process. A completed `/login` forces a revalidation.
package/index.ts CHANGED
@@ -526,6 +526,8 @@ let cachedApiKey: string | undefined;
526
526
  // Stable identity behind cachedApiKey for the quiet-sync fingerprints.
527
527
  let cachedSyncIdentity: string | undefined;
528
528
  let revalidateAbort: AbortController | null = null;
529
+ // The in-flight catalog revalidation (session start or /login); it never rejects.
530
+ let modelSync: Promise<void> = Promise.resolve();
529
531
 
530
532
  const AUTH_PATH = path.join(getAgentDir(), "auth.json");
531
533
 
@@ -1338,7 +1340,12 @@ export default function (pi: ExtensionAPI) {
1338
1340
  lastCreditsFetchAt = 0;
1339
1341
  const epoch = statusEpoch;
1340
1342
  setTimeout(() => {
1341
- void syncModels(epoch, revalidateAbort?.signal, true).catch(() => {});
1343
+ // The new credentials supersede an in-flight revalidation, which must
1344
+ // release its sync lock first, or this forced refresh would skip as "locked".
1345
+ revalidateAbort?.abort();
1346
+ revalidateAbort = new AbortController();
1347
+ const signal = revalidateAbort.signal;
1348
+ modelSync = modelSync.then(() => (signal.aborted ? undefined : syncModels(epoch, signal, true))).catch(() => {});
1342
1349
  }, 0);
1343
1350
  };
1344
1351
 
@@ -1370,7 +1377,7 @@ export default function (pi: ExtensionAPI) {
1370
1377
  // the provider API. Nothing is awaited and every rejection is swallowed.
1371
1378
  // A key-resolution failure is surfaced (it used to vanish), then we keep
1372
1379
  // serving the embedded/cached catalog.
1373
- void resolveApiKey(ctx.modelRegistry)
1380
+ modelSync = resolveApiKey(ctx.modelRegistry)
1374
1381
  .catch((err) => {
1375
1382
  notifier.warn(`Unable to resolve HyperCharm credentials: ${describeError(err)} — serving cached/embedded models.`);
1376
1383
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-hypercharm-provider",
3
- "version": "1.3.40",
3
+ "version": "1.3.42",
4
4
  "description": "HyperCharm provider extension for pi - Access DeepSeek, GLM, Kimi, Qwen, MiniMax, Gemma, and GPT-OSS models through the Charm Hyper API",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -31,17 +31,17 @@
31
31
  "gpt-oss"
32
32
  ],
33
33
  "peerDependencies": {
34
- "@earendil-works/pi-tui": "*",
35
34
  "@earendil-works/pi-ai": "*",
36
- "@earendil-works/pi-coding-agent": "*"
35
+ "@earendil-works/pi-coding-agent": "*",
36
+ "@earendil-works/pi-tui": "*"
37
37
  },
38
38
  "overrides": {
39
39
  "brace-expansion": "5.0.12"
40
40
  },
41
41
  "devDependencies": {
42
- "@earendil-works/pi-ai": "1.1.0",
43
- "@earendil-works/pi-coding-agent": "1.1.0",
44
- "@earendil-works/pi-tui": "1.1.0",
42
+ "@earendil-works/pi-ai": "1.2.0",
43
+ "@earendil-works/pi-coding-agent": "1.2.0",
44
+ "@earendil-works/pi-tui": "1.2.0",
45
45
  "@types/node": "^24.10.1",
46
46
  "jiti": "2.7.0",
47
47
  "typescript": "6.0.3",
@@ -53,5 +53,9 @@
53
53
  "extensions": [
54
54
  "./index.ts"
55
55
  ]
56
+ },
57
+ "repository": {
58
+ "type": "git",
59
+ "url": "git+https://github.com/monotykamary/pi-hypercharm-provider.git"
56
60
  }
57
61
  }
@@ -1,4 +1,4 @@
1
- // Offline Pi 1.1.0 manifest, lifecycle, catalog and real transport regression.
1
+ // Offline Pi 1.2.0 manifest, lifecycle, catalog and real transport regression.
2
2
  import assert from 'node:assert/strict';
3
3
  import { mkdtemp, readFile, rm } from 'node:fs/promises';
4
4
  import { tmpdir } from 'node:os';
@@ -18,13 +18,13 @@ try {
18
18
  const hostEntry = process.env.PI1_HOST_ENTRY === 'bundle' ? 'dist/bundle/index.js' : 'dist/index.js';
19
19
  const sdk = await import(host ? pathToFileURL(join(host, hostEntry)).href : '@earendil-works/pi-coding-agent');
20
20
  const { createAgentSession, DefaultResourceLoader, ModelRuntime, SessionManager, SettingsManager, VERSION } = sdk;
21
- assert.equal(VERSION, '1.1.0', 'executing host version');
22
- assert.equal((await import('@earendil-works/pi-coding-agent')).VERSION, '1.1.0', 'development host version');
21
+ assert.equal(VERSION, '1.2.0', 'executing host version');
22
+ assert.equal((await import('@earendil-works/pi-coding-agent')).VERSION, '1.2.0', 'development host version');
23
23
  const manifest = JSON.parse(await readFile(join(root, 'package.json'), 'utf8'));
24
24
  for (const name of ['@earendil-works/pi-ai', '@earendil-works/pi-agent-core', '@earendil-works/pi-coding-agent', '@earendil-works/pi-tui', 'typebox']) {
25
25
  assert.equal(manifest.dependencies?.[name], undefined, `${name}: do not bundle host packages`);
26
26
  if (manifest.peerDependencies?.[name] !== undefined) assert.equal(manifest.peerDependencies[name], '*');
27
- if (name !== 'typebox' && manifest.devDependencies?.[name]) assert.equal(manifest.devDependencies[name], '1.1.0');
27
+ if (name !== 'typebox' && manifest.devDependencies?.[name]) assert.equal(manifest.devDependencies[name], '1.2.0');
28
28
  }
29
29
  const settingsManager = SettingsManager.inMemory({ packages: [root], compaction: { enabled: false }, retry: { enabled: false } });
30
30
  const resourceLoader = new DefaultResourceLoader({ cwd: home, agentDir: home, settingsManager,
@@ -363,6 +363,33 @@ describe("HyperCharm quiet sync variants", () => {
363
363
  expect(new Headers(fetch.mock.calls[0][1]?.headers).get("Authorization")).toBe("Bearer fresh-access");
364
364
  });
365
365
 
366
+ it("fetches the new account's catalog when /login races the session revalidation holding the lock", async () => {
367
+ setupHome();
368
+ writeCache(2 * HOUR);
369
+ vi.doMock("../oauth", () => ({
370
+ USER_AGENT: "pi-hypercharm-provider/test",
371
+ loginHypercharm: async () => ({ type: "oauth", refresh: "fresh-refresh", access: "fresh-access", expires: Date.now() + HOUR }),
372
+ refreshHypercharmToken: async (c: unknown) => c,
373
+ }));
374
+ let release!: () => void;
375
+ const fetch = stubFetch((_url, init) => new Promise<Response>((resolve, reject) => {
376
+ const signal = init?.signal;
377
+ signal?.addEventListener("abort", () => reject(signal.reason));
378
+ release = () => resolve(liveModelsResponse());
379
+ }));
380
+ const ext = await loadExtension();
381
+ await ext.startSession();
382
+ await vi.waitFor(() => expect(catalogCalls(fetch)).toHaveLength(1)); // session sync in flight, lock held
383
+
384
+ await ext.registrations[0].oauth.login({});
385
+ await vi.waitFor(() => expect(catalogCalls(fetch)).toHaveLength(2));
386
+ release();
387
+ await vi.waitFor(() => expect(hasLiveModel(ext.registrations.at(-1))).toBe(true));
388
+ expect(new Headers(catalogCalls(fetch)[1][1]?.headers).get("Authorization")).toBe("Bearer fresh-access");
389
+ expect(JSON.parse(fs.readFileSync(syncMetaPath(cachePath), "utf8")).keyFingerprint).toBe(keyFingerprint("oauth:fresh-refresh"));
390
+ expect(fs.existsSync(syncLockPath(cachePath))).toBe(false);
391
+ });
392
+
366
393
  it("skips the startup account poll without a UI (print mode, child agents)", async () => {
367
394
  setupHome();
368
395
  writeCache(10 * 60 * 1000);