gm-plugkit 2.0.2469 → 2.0.2471

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/SKILL.md CHANGED
@@ -60,7 +60,7 @@ bun x gm-plugkit@latest spool
60
60
 
61
61
  (`npx -y gm-plugkit@latest spool` if no `bun`.) Fire-and-forget: spawns the detached daemon and returns immediately (already-alive watcher also returns at once, unchanged) -- it does NOT wait for the watcher to check serving. No `&`, no `sleep`, no re-`cat`; write your first verb to `in/` right after it returns. A first-read "file does not exist" on that verb is normal (the just-spawned watcher hasn't noticed the file yet) -- re-Read next message, same as any dead-watcher-adjacent recheck. If you need to actively check serving before dispatching (rare), read `.gm/exec-spool/.status.json` yourself and check `ts` freshness.
62
62
 
63
- **The boot line resolves to agentplug-runner, the sole spool loader; `bun x gm-plugkit@latest spool` is the thin launcher that re-execs into it.** `gm-plugkit/cli.js::tryDelegateToRunner` execs `~/.gm-tools/agentplug-runner` -- a native wasmtime binary that loads gm.wasm alongside shared `bert`/`libsql`/`treesitter` plugins (so gm runs `plugkit-slim.wasm`, browser via direct CDP, task via a native registry); `.status.json` `runtime` reads `agentplug` when it serves. The JS wasm-host (`plugkit-wasm-wrapper.js`) and the redundant `gm-runner` native host are retired -- there is no fallback loader. `bin/install.js` hard-requires agentplug-runner: it downloads the sha256-verified binary from `AnEntrypoint/agentplug-bin` for the host platform and fails loudly if none is published (or the download fails) rather than leaving no loader. agentplug-runner auto-updates both its served `plugkit.wasm` and its own executable fully autonomously (600s poll each): a newer runner build is staged to `.new` in the background, then swapped in via a self-triggered `takeover` handoff on the next idle tick -- no agent action, no `bin/install.js` re-run, no restart ever required.
63
+ **The boot line resolves to agentplug-runner, the sole spool loader; `bun x gm-plugkit@latest spool` is the thin launcher that re-execs into it.** `gm-plugkit/cli.js::tryDelegateToRunner` execs `~/.gm-tools/agentplug-runner` -- a native wasmtime binary that loads gm.wasm alongside shared `bert`/`libsql`/`treesitter` plugins (so gm runs `plugkit-slim.wasm`, browser via direct CDP, task via a native registry); `.status.json` `runtime` reads `agentplug` when it serves. A missing runner binary is downloaded and sha256-verified automatically on the next boot attempt -- no separate install step. agentplug-runner auto-updates both its served `plugkit.wasm` and its own executable fully autonomously (600s poll each): a newer runner build is staged to `.new` in the background, then swapped in via a self-triggered `takeover` handoff on the next idle tick -- no agent action, no restart ever required.
64
64
 
65
65
  The `Resolving dependencies` / `Saved lockfile` chatter before the JSON payload is `bunx` re-resolving the `@latest` tag against the registry, not gm-plugkit hanging -- the daemon already spawns detached+unref'd and the CLI itself exits the instant that happens; the visible delay is entirely bunx's own network round-trip, unavoidable on `@latest` (a pinned exact version, once bunx-cached, skips it). `GM_PLUGKIT_SKIP_SELF_STALE_CHECK=1` skips the CLI's own redundant npm-registry version probe (already covered by `@latest`'s resolution) for a faster boot on repeat same-session invocations. In PowerShell 5.1, never `2>&1`-redirect this command into another cmdlet (e.g. `| Select-Object`) -- PowerShell wraps every stderr line from a native exe in a `NativeCommandError` record and reports failure even on exit 0, turning bun's routine stderr progress output into a misleading red error block; run it bare or capture stdout only.
66
66
 
@@ -74,7 +74,7 @@ Two more real, code-checked env toggles beyond `GM_PLUGKIT_SKIP_SELF_STALE_CHECK
74
74
 
75
75
  **Reboot-loop escape (watcher dies ~30-90s after every boot).** If a fresh `bun x gm-plugkit@latest spool` boots but the watcher dies again shortly after (heartbeat `ts` goes stale >30s with no future `busy_until`, then a new pid appears, repeatedly), the on-disk index has not finished embedding and each boot re-triggers the same synchronous code-index embed that can block the heartbeat past the supervisor's 30s stale limit (`STATUS_STALE_MS`). Confirm by reading `.gm/exec-spool/.watcher.log` for repeated `codeinsight_rebuild` + `partial pass (wall budget) ... deferred_files=N` lines whose `deferred_files` never reaches 0. The embed is genuinely converging in that case: do NOT immediately re-boot on the first stale reading -- read `.watcher.log`, and as long as `deferred_files` is strictly decreasing across `codeinsight_index_partial` events the index is converging (each accepted verb advances it one wall-budget); give it repeated single verbs until a `code_index: done` / `deferred_files=0` line appears, then normal dispatch resumes. Rebooting mid-convergence resets this progress -- the loop is the reboot, not the embed.
76
76
 
77
- **A self-update in progress or just-completed is visible directly in `.status.json`, no manual log-reading needed.** `runner_update_in_progress: true` (with `runner_update_waiting_ms`) means a newer runner build is staged and mid-handoff right now -- a dispatch that lands in this window can come back `dispatch_orphaned` (`"was claimed by a daemon that died before answering... a self-update handoff that exited while this dispatch was still running"`); this is expected, not a real failure, and the fix is a bare re-dispatch of the exact same verb once `.status.json`'s `ts` is fresh again, never a different recovery path. `last_completed_runner_swap: {version, swapped_at_ts}` is the durable "a swap just happened" record -- diff it against what you last saw this session to notice a version change mid-turn (served prose, gate behavior, or verb set can differ across a swap) without needing to poll continuously. `loaded_plugin_versions: {gm, bert, libsql, treesitter}` reports each currently-loaded plugin's version string, purely informational -- a plugin content-hash recompile evicts and rebuilds in-process without a process restart, so it never orphans a dispatch the way a runner takeover does, but the version is still worth diffing against a prior read to notice gm's own behavior changed mid-session. None of these three fields change what you dispatch next; they explain WHY a dispatch might have just failed or WHY served behavior just shifted, so it is recognized and handled immediately instead of investigated as a mystery.
77
+ A `dispatch_orphaned` response ("claimed by a daemon that died before answering... a self-update handoff") is expected during a runner takeover, never a real failure -- bare re-dispatch the exact same verb once `.status.json`'s `ts` is fresh again. `.status.json`'s `last_completed_runner_swap`/`loaded_plugin_versions` are diagnostic-only, worth diffing against a prior read to notice served behavior changed mid-session; neither changes what to dispatch next.
78
78
 
79
79
  **Apparent tooling failure is never grounds to ask the user, never a blind restart.** "Spooler not working" / missing response / stale watcher / `gm_plugkit_stale` flagged in a response = your own mechanical self-recovery: honor a future `busy_until` (wait), else boot + re-dispatch. You have boot authority; asking the user to do what a verb can do is a deviation. Staleness of any kind (stale watcher version, stale served prose vs published source) is itself a deviation to resolve immediately, the same turn it's noticed -- `bun x gm-plugkit@latest spool` first, before any other work.
80
80
 
package/bootstrap.js CHANGED
@@ -231,6 +231,24 @@ function httpGetBuffer(url, timeoutMs) {
231
231
  });
232
232
  }
233
233
 
234
+ function httpHeadOk(url, timeoutMs) {
235
+ const https = require('https');
236
+ return new Promise((resolve, reject) => {
237
+ const req = https.request(url, { method: 'HEAD', timeout: timeoutMs || 3000, headers: { 'user-agent': 'gm-plugkit-bootstrap' } }, (res) => {
238
+ res.resume();
239
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
240
+ httpHeadOk(res.headers.location, timeoutMs).then(resolve, reject);
241
+ return;
242
+ }
243
+ if (res.statusCode === 200) resolve();
244
+ else reject(new Error(`HTTP ${res.statusCode} ${url}`));
245
+ });
246
+ req.on('timeout', () => { try { req.destroy(new Error(`idle-timeout ${timeoutMs || 3000}ms ${url}`)); } catch (_) {} });
247
+ req.on('error', reject);
248
+ req.end();
249
+ });
250
+ }
251
+
234
252
  async function downloadFromGithubReleases(destPath, version, artifactName) {
235
253
  const name = artifactName || 'plugkit.wasm';
236
254
  const base = `https://github.com/AnEntrypoint/plugkit-bin/releases/download/v${version}`;
@@ -546,12 +564,59 @@ function discoverBundledSkillsAndSourcesLocal() {
546
564
  return found;
547
565
  }
548
566
 
567
+ // Walks a (decompressed) POSIX tar stream's fixed 512-byte headers, yielding
568
+ // each entry's name. `git archive --remote` was tried first and does NOT
569
+ // work against GitHub (its git server returns HTTP 422 for upload-archive,
570
+ // a documented GitHub limitation) -- codeload.github.com's tarball endpoint
571
+ // is the real plain-HTTPS, non-api.github.com path that actually serves
572
+ // repo content.
573
+ function* tarEntryNames(buf) {
574
+ let offset = 0;
575
+ while (offset + 512 <= buf.length) {
576
+ const header = buf.subarray(offset, offset + 512);
577
+ if (header.every(b => b === 0)) break;
578
+ const name = header.subarray(0, 100).toString('utf-8').replace(/\0.*$/s, '');
579
+ const sizeOctal = header.subarray(124, 136).toString('utf-8').replace(/\0.*$/s, '').trim();
580
+ const size = parseInt(sizeOctal, 8) || 0;
581
+ if (name) yield name;
582
+ offset += 512 + Math.ceil(size / 512) * 512;
583
+ }
584
+ }
585
+
586
+ async function discoverRemoteSkillNamesViaCodeload(timeoutMs) {
587
+ const url = `https://codeload.github.com/${SKILL_MD_REMOTE_REPO}/tar.gz/refs/heads/${SKILL_MD_REMOTE_BRANCH}`;
588
+ const gz = await httpGetBuffer(url, timeoutMs || 15000);
589
+ const tar = require('zlib').gunzipSync(gz);
590
+ const names = new Set();
591
+ for (const name of tarEntryNames(tar)) {
592
+ const m = /^[^/]+\/skills\/([^/]+)\/$/.exec(name);
593
+ if (m) names.add(m[1]);
594
+ }
595
+ if (names.size === 0) throw new Error('codeload tarball listing produced zero skill directories');
596
+ return Array.from(names);
597
+ }
598
+
599
+ // The GitHub Contents API this normally uses already fails soft (caller
600
+ // falls back to bundled local skills), but a fully API-scope-restricted
601
+ // environment (a corporate proxy or org policy blocking api.github.com
602
+ // specifically) loses fresh remote skill discovery entirely even though
603
+ // the same repo is reachable over codeload.github.com's plain-HTTPS
604
+ // tarball endpoint -- a different host, no API token or REST surface.
549
605
  async function discoverRemoteSkillNames(timeoutMs) {
550
606
  const url = `https://api.github.com/repos/${SKILL_MD_REMOTE_REPO}/contents/skills?ref=${SKILL_MD_REMOTE_BRANCH}`;
551
- const buf = await httpGetBuffer(url, timeoutMs || 5000);
552
- const entries = JSON.parse(buf.toString('utf-8'));
553
- if (!Array.isArray(entries)) throw new Error('unexpected github contents API response shape');
554
- return entries.filter(e => e && e.type === 'dir' && e.name).map(e => e.name);
607
+ try {
608
+ const buf = await httpGetBuffer(url, timeoutMs || 5000);
609
+ const entries = JSON.parse(buf.toString('utf-8'));
610
+ if (!Array.isArray(entries)) throw new Error('unexpected github contents API response shape');
611
+ return entries.filter(e => e && e.type === 'dir' && e.name).map(e => e.name);
612
+ } catch (apiErr) {
613
+ try {
614
+ return await discoverRemoteSkillNamesViaCodeload(timeoutMs);
615
+ } catch (fallbackErr) {
616
+ obsEvent('bootstrap', 'discover-remote-skill-names.codeload-fallback-failed', { error: fallbackErr.message });
617
+ throw apiErr;
618
+ }
619
+ }
555
620
  }
556
621
 
557
622
  async function fetchRemoteSkillMd(skillName, timeoutMs) {
@@ -659,6 +724,42 @@ function installedVersionAtTools() {
659
724
  } catch (_) { return null; }
660
725
  }
661
726
 
727
+ function compareDottedSemverAscending(a, b) {
728
+ const pa = a.replace(/^v/, '').split('.').map(Number);
729
+ const pb = b.replace(/^v/, '').split('.').map(Number);
730
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
731
+ const d = (pa[i] || 0) - (pb[i] || 0);
732
+ if (d !== 0) return d;
733
+ }
734
+ return 0;
735
+ }
736
+
737
+ // Fallback when the Releases-list API is unreachable: git ls-remote can
738
+ // see every tag over plain git protocol without touching api.github.com,
739
+ // but cannot see release ASSETS -- so a tag it finds is verified against
740
+ // a HEAD probe on the raw release-download URL for plugkit.wasm before
741
+ // being trusted, matching the API path's own hasPlugkitWasm check.
742
+ async function resolveLatestRemoteVersionViaGit(timeoutMs) {
743
+ const { execFileSync } = require('child_process');
744
+ const out = execFileSync('git', ['ls-remote', '--tags', '--refs', 'https://github.com/AnEntrypoint/plugkit-bin.git'], {
745
+ encoding: 'utf8',
746
+ stdio: ['ignore', 'pipe', 'ignore'],
747
+ });
748
+ const tags = out.split('\n')
749
+ .map(line => line.match(/refs\/tags\/v(\d+\.\d+\.\d+(?:-[A-Za-z0-9.]+)?)$/))
750
+ .filter(Boolean)
751
+ .map(m => m[1]);
752
+ tags.sort(compareDottedSemverAscending);
753
+ for (let i = tags.length - 1; i >= 0; i--) {
754
+ const version = tags[i];
755
+ try {
756
+ await httpHeadOk(`https://github.com/AnEntrypoint/plugkit-bin/releases/download/v${version}/plugkit.wasm`, timeoutMs || 3000);
757
+ return version;
758
+ } catch (_) { /* asset missing for this tag, try the next-newest */ }
759
+ }
760
+ return null;
761
+ }
762
+
662
763
  async function resolveLatestRemoteVersion(timeoutMs) {
663
764
  try {
664
765
  const buf = await httpGetBuffer('https://api.github.com/repos/AnEntrypoint/plugkit-bin/releases?per_page=50', timeoutMs || 3000);
@@ -674,6 +775,12 @@ async function resolveLatestRemoteVersion(timeoutMs) {
674
775
  }
675
776
  } catch (e) {
676
777
  obsEvent('bootstrap', 'resolve-latest-remote-version.failed', { error: e.message });
778
+ try {
779
+ const viaGit = await resolveLatestRemoteVersionViaGit(timeoutMs);
780
+ if (viaGit) return viaGit;
781
+ } catch (gitErr) {
782
+ obsEvent('bootstrap', 'resolve-latest-remote-version.git-fallback-failed', { error: gitErr.message });
783
+ }
677
784
  }
678
785
  return null;
679
786
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gm-plugkit",
3
- "version": "2.0.2469",
3
+ "version": "2.0.2471",
4
4
  "description": "Bootstrap and daemon-spawn tool for gm plugkit binary. Downloads the correct platform wasm, verifies SHA256, and launches agentplug-runner (the native wasm host) as the spool watcher daemon.",
5
5
  "main": "index.js",
6
6
  "bin": {
package/plugkit.version CHANGED
@@ -1 +1 @@
1
- 0.1.1182
1
+ 0.1.1183