@skaile/workspaces 3.7.3 → 3.8.0
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/CHANGELOG.md +103 -0
- package/dist/app-sdk/index.js +1 -1
- package/dist/{asset-feeds-6XDLVHQJ.js → asset-feeds-7HM3HONM.js} +4 -4
- package/dist/{asset-feeds-6XDLVHQJ.js.map → asset-feeds-7HM3HONM.js.map} +1 -1
- package/dist/asset-manager/index.js +1 -1
- package/dist/bridge/drivers/claude-sdk.js +229 -23
- package/dist/bridge/drivers/claude-sdk.js.map +1 -1
- package/dist/bridge/drivers/codex.js +1 -1
- package/dist/bridge/drivers/continue.js +2 -2
- package/dist/bridge/drivers/echo.js +1 -1
- package/dist/bridge/drivers/gemini.js +3 -3
- package/dist/bridge/drivers/goose.js +2 -2
- package/dist/bridge/drivers/omp.js +1 -1
- package/dist/bridge/drivers/opencode.js +2 -2
- package/dist/bridge/drivers/qwen.js +3 -3
- package/dist/bridge/index.js +2 -2
- package/dist/bridge/src/drivers/claude-sdk.d.ts +96 -1
- package/dist/bridge/src/drivers/claude-sdk.d.ts.map +1 -1
- package/dist/bridge/src/types.d.ts +103 -0
- package/dist/bridge/src/types.d.ts.map +1 -1
- package/dist/{chunk-WHOM6NF3.js → chunk-2GREL3OD.js} +3 -3
- package/dist/{chunk-WHOM6NF3.js.map → chunk-2GREL3OD.js.map} +1 -1
- package/dist/{chunk-ZMCBWZ77.js → chunk-2JW5SWIU.js} +6 -6
- package/dist/{chunk-ZMCBWZ77.js.map → chunk-2JW5SWIU.js.map} +1 -1
- package/dist/{chunk-5Z4JLURL.js → chunk-4WTC6BSV.js} +3 -3
- package/dist/{chunk-5Z4JLURL.js.map → chunk-4WTC6BSV.js.map} +1 -1
- package/dist/{chunk-DGWJAPRC.js → chunk-6GDVGAIZ.js} +447 -95
- package/dist/chunk-6GDVGAIZ.js.map +1 -0
- package/dist/{chunk-BSQGQMGA.js → chunk-7SHXLJQP.js} +3 -3
- package/dist/{chunk-BSQGQMGA.js.map → chunk-7SHXLJQP.js.map} +1 -1
- package/dist/{chunk-YICH6VQB.js → chunk-B7TA2XI7.js} +2 -2
- package/dist/{chunk-YICH6VQB.js.map → chunk-B7TA2XI7.js.map} +1 -1
- package/dist/{chunk-ML6OHA3S.js → chunk-BGAAZMBK.js} +2 -2
- package/dist/chunk-BGAAZMBK.js.map +1 -0
- package/dist/{chunk-765ZGKVC.js → chunk-DGDELPOD.js} +3 -3
- package/dist/{chunk-765ZGKVC.js.map → chunk-DGDELPOD.js.map} +1 -1
- package/dist/{chunk-26L2B6P6.js → chunk-FUCYTGAE.js} +4 -4
- package/dist/chunk-FUCYTGAE.js.map +1 -0
- package/dist/{chunk-6GTPLCMO.js → chunk-IH2KJYAU.js} +142 -13
- package/dist/chunk-IH2KJYAU.js.map +1 -0
- package/dist/{chunk-UGBN7MQZ.js → chunk-KNPBMR2P.js} +3 -3
- package/dist/{chunk-UGBN7MQZ.js.map → chunk-KNPBMR2P.js.map} +1 -1
- package/dist/{chunk-BCXI27BH.js → chunk-NVYEK4FI.js} +3 -3
- package/dist/{chunk-BCXI27BH.js.map → chunk-NVYEK4FI.js.map} +1 -1
- package/dist/{chunk-D4VHBAJC.js → chunk-O3LEV6KB.js} +4 -4
- package/dist/{chunk-D4VHBAJC.js.map → chunk-O3LEV6KB.js.map} +1 -1
- package/dist/{chunk-OOUQWDD5.js → chunk-OZZCPTOG.js} +6 -6
- package/dist/{chunk-OOUQWDD5.js.map → chunk-OZZCPTOG.js.map} +1 -1
- package/dist/{chunk-V7CKJGT4.js → chunk-PRUCNSBI.js} +4 -4
- package/dist/{chunk-V7CKJGT4.js.map → chunk-PRUCNSBI.js.map} +1 -1
- package/dist/{chunk-MGPHEYW2.js → chunk-QVQH6GV2.js} +2 -2
- package/dist/{chunk-MGPHEYW2.js.map → chunk-QVQH6GV2.js.map} +1 -1
- package/dist/{chunk-FSWYUSCP.js → chunk-RVXBOKHK.js} +3 -3
- package/dist/{chunk-FSWYUSCP.js.map → chunk-RVXBOKHK.js.map} +1 -1
- package/dist/cli/index.js +30 -30
- package/dist/connectors/index.js +3 -3
- package/dist/connectors/src/connector-manager.d.ts +34 -2
- package/dist/connectors/src/connector-manager.d.ts.map +1 -1
- package/dist/connectors/src/connector-prompt.d.ts.map +1 -1
- package/dist/connectors/src/connector-types.d.ts +25 -0
- package/dist/connectors/src/connector-types.d.ts.map +1 -1
- package/dist/connectors/src/credential-helper-script.d.ts +48 -0
- package/dist/connectors/src/credential-helper-script.d.ts.map +1 -1
- package/dist/connectors/src/index.d.ts +3 -3
- package/dist/connectors/src/index.d.ts.map +1 -1
- package/dist/connectors/src/managed-gitconfig.d.ts +80 -0
- package/dist/connectors/src/managed-gitconfig.d.ts.map +1 -1
- package/dist/{connectors-DY7UBBV5.js → connectors-2OHRZDJZ.js} +3 -3
- package/dist/{connectors-DY7UBBV5.js.map → connectors-2OHRZDJZ.js.map} +1 -1
- package/dist/{ensure-sources-TSHLAVNU.js → ensure-sources-42LTJ5QI.js} +4 -4
- package/dist/{ensure-sources-TSHLAVNU.js.map → ensure-sources-42LTJ5QI.js.map} +1 -1
- package/dist/factory-assets/connectors/box.js +3 -3
- package/dist/factory-assets/connectors/deploy.js +3 -3
- package/dist/factory-assets/connectors/devserver.js +3 -3
- package/dist/factory-assets/connectors/flow/adapter.js +3 -3
- package/dist/factory-assets/connectors/flow/run-flow.js +4 -4
- package/dist/factory-assets/connectors/flow.js +3 -3
- package/dist/factory-assets/connectors/git/driver.d.ts +28 -0
- package/dist/factory-assets/connectors/git/driver.d.ts.map +1 -1
- package/dist/factory-assets/connectors/git.js +3 -3
- package/dist/factory-assets/connectors/gmail.js +3 -3
- package/dist/factory-assets/connectors/googledrive.js +3 -3
- package/dist/factory-assets/connectors/local.js +3 -3
- package/dist/factory-assets/connectors/mattermost.js +3 -3
- package/dist/factory-assets/connectors/memory.js +3 -3
- package/dist/factory-assets/connectors/minio.js +3 -3
- package/dist/factory-assets/connectors/postgres.js +3 -3
- package/dist/factory-assets/connectors/s3.js +3 -3
- package/dist/factory-assets/connectors/sharepoint.js +3 -3
- package/dist/factory-assets/connectors/sqlite.js +3 -3
- package/dist/factory-assets/connectors/static-server.js +3 -3
- package/dist/factory-assets/connectors/tunnel.js +3 -3
- package/dist/factory-assets/connectors/webdav.js +3 -3
- package/dist/factory-assets/connectors/xstate-store.js +3 -3
- package/dist/factory-assets/connectors/xstate.js +3 -3
- package/dist/{helpers-PVM4MQV2.js → helpers-BP7FX6CO.js} +4 -4
- package/dist/{helpers-PVM4MQV2.js.map → helpers-BP7FX6CO.js.map} +1 -1
- package/dist/{library-publish-P7ZVEY6I.js → library-publish-X5Z32WJY.js} +6 -6
- package/dist/{library-publish-P7ZVEY6I.js.map → library-publish-X5Z32WJY.js.map} +1 -1
- package/dist/{open-library-QG4LHFQQ.js → open-library-PHUMBYHP.js} +5 -5
- package/dist/{open-library-QG4LHFQQ.js.map → open-library-PHUMBYHP.js.map} +1 -1
- package/dist/{project-YE6GKQTZ.js → project-HAMCXPNE.js} +5 -5
- package/dist/{project-YE6GKQTZ.js.map → project-HAMCXPNE.js.map} +1 -1
- package/dist/runner/index.js +7 -7
- package/dist/runner/src/ai-provider-switch.d.ts +95 -0
- package/dist/runner/src/ai-provider-switch.d.ts.map +1 -0
- package/dist/runner/src/credential-rotated.d.ts +23 -4
- package/dist/runner/src/credential-rotated.d.ts.map +1 -1
- package/dist/runner/src/serve.d.ts.map +1 -1
- package/dist/runner/src/session-builder.d.ts +19 -0
- package/dist/runner/src/session-builder.d.ts.map +1 -1
- package/dist/sdk/asset-manager.js +1 -1
- package/dist/sdk/bridge.js +2 -2
- package/dist/sdk/index.js +7 -7
- package/dist/sdk/runner.js +7 -7
- package/dist/sdk/session.js +2 -2
- package/dist/sdk/types.js +1 -1
- package/dist/session/index.js +2 -2
- package/dist/{setup-UYLZFES6.js → setup-IOOGR3EL.js} +4 -4
- package/dist/{setup-UYLZFES6.js.map → setup-IOOGR3EL.js.map} +1 -1
- package/dist/{source-5GTHOFT6.js → source-772GLNM6.js} +6 -6
- package/dist/{source-5GTHOFT6.js.map → source-772GLNM6.js.map} +1 -1
- package/dist/{source-manifest-publish-DK3CPBSI.js → source-manifest-publish-LAUGDQ5G.js} +7 -7
- package/dist/{source-manifest-publish-DK3CPBSI.js.map → source-manifest-publish-LAUGDQ5G.js.map} +1 -1
- package/dist/tui/index.js +7 -7
- package/dist/types/index.js +1 -1
- package/dist/types/src/events.d.ts +34 -1
- package/dist/types/src/events.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +1 -1
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/lifecycle.d.ts +13 -0
- package/dist/types/src/lifecycle.d.ts.map +1 -1
- package/dist/types/src/version.d.ts +22 -1
- package/dist/types/src/version.d.ts.map +1 -1
- package/dist/workspace-plugin/adapters/mcp.js +2 -2
- package/dist/workspace-plugin/adapters/mcp.js.map +1 -1
- package/dist/workspace-plugin/adapters/omp.js +8 -8
- package/dist/workspace-plugin/adapters/omp.js.map +1 -1
- package/dist/workspace-plugin/index.js +1 -1
- package/dist/workspace-plugin/src/adapters/mcp.d.ts.map +1 -1
- package/dist/workspace-plugin/src/adapters/omp.d.ts.map +1 -1
- package/dist/workspace-plugin/src/tools/connectors.d.ts +7 -3
- package/dist/workspace-plugin/src/tools/connectors.d.ts.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-26L2B6P6.js.map +0 -1
- package/dist/chunk-6GTPLCMO.js.map +0 -1
- package/dist/chunk-DGWJAPRC.js.map +0 -1
- package/dist/chunk-ML6OHA3S.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,108 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 3.8.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#582](https://github.com/skaile-ai/workspaces/pull/582) [`befe90c`](https://github.com/skaile-ai/workspaces/commit/befe90cf4bc0981639ece7defffeb2504404394d) Thanks [@peteralbert](https://github.com/peteralbert)! - fix(connectors): tell the agent — and git — when a required mount failed to connect
|
|
8
|
+
|
|
9
|
+
A git connector whose backend credential cannot be minted (an `auth: backend`
|
|
10
|
+
provider link with no refresh token) threw out of `connect()`. The connector then
|
|
11
|
+
never entered the manager's active map, so every agent-facing surface read from
|
|
12
|
+
that map showed nothing at all: no prompt row, no warning, no tool entry. The
|
|
13
|
+
agent went on believing the path was a backend-managed git worktree while the
|
|
14
|
+
whole lifecycle automation — auto-commit, push-after-commit, push-on-hibernate,
|
|
15
|
+
commit-on-hibernate, push-on-close — ran into the void. Because the mount also
|
|
16
|
+
never reached `exposeManagedCredential`, no credential helper was registered for
|
|
17
|
+
the host, so `git push` failed with `fatal: could not read Username for
|
|
18
|
+
'https://github.com'` while `git fetch` on a public remote kept succeeding — the
|
|
19
|
+
one failure mode that makes auth look fine.
|
|
20
|
+
|
|
21
|
+
Three changes:
|
|
22
|
+
|
|
23
|
+
- **The failure is now stated.** `ConnectorManager` remembers connectors that
|
|
24
|
+
failed to connect (`listFailedConnectors()`), and `buildConnectorPromptSection`
|
|
25
|
+
emits a `## Mount failures — read this first` section ahead of `## Mounts`.
|
|
26
|
+
It names the mount and its verbatim error, explicitly supersedes any claim
|
|
27
|
+
elsewhere in the prompt that the path is a managed or synced workspace, warns
|
|
28
|
+
that work written there is neither committed nor pushed, and says the
|
|
29
|
+
credential is minted by the host platform — there is nothing to fix in project
|
|
30
|
+
settings. `connector_list` now returns `{ connected, failed }` on all three
|
|
31
|
+
surfaces — the SDK tool and the MCP and OMP adapters.
|
|
32
|
+
- **git stops with the reason instead of prompting.** A failed git connect
|
|
33
|
+
best-effort registers an "unavailable" credential-helper block. The rendered
|
|
34
|
+
helper writes `skaile: mount not connected: <reason>` to stderr and emits
|
|
35
|
+
`quit=1`, which is the only response that actually stops git's helper chain.
|
|
36
|
+
A stale unavailable block is cleared on the next successful connect — by URL
|
|
37
|
+
prefix, not just by mount id, so a sibling mount that failed earlier under the
|
|
38
|
+
same owner cannot answer a healthy mount's fetch or push — and including for
|
|
39
|
+
Tier-1 mounts, which write no block of their own. The `quit=1` response now
|
|
40
|
+
covers any missing credential, not only a known-dead mount: exiting 0 there was
|
|
41
|
+
what produced `could not read Username` in the first place.
|
|
42
|
+
- **The refresh dance is bounded.** An empty or missing credentials file already
|
|
43
|
+
triggers a refresh request; that request is now rate-limited per mount by a
|
|
44
|
+
cooldown stamp the helper writes after a dance that produced nothing, so a
|
|
45
|
+
refresh that legitimately keeps failing no longer costs every git network op a
|
|
46
|
+
fresh timeout. The driver pre-creates that stamp group-writable
|
|
47
|
+
(`ensureRefreshAttemptStamp`), because `.skaile/` belongs to the runner while
|
|
48
|
+
the helper runs as the agent uid and can only truncate a file that exists.
|
|
49
|
+
|
|
50
|
+
Also hardens the rendered helper: every interpolated value (paths, credential
|
|
51
|
+
tag, and the backend-supplied reason text) is shell-quoted, and the reason is
|
|
52
|
+
collapsed to one line.
|
|
53
|
+
|
|
54
|
+
Also fixes a shell trap the rendered helper already carried: `:` is a POSIX
|
|
55
|
+
special built-in, so a redirection failure on `: > "$FILE"` terminates a
|
|
56
|
+
non-interactive `dash`/`ash` outright — `|| true` never runs. Both flag writes are
|
|
57
|
+
now redirected inside a subshell, so an unwritable flag or stamp costs the
|
|
58
|
+
cooldown rather than killing the helper before it can report `quit=1`.
|
|
59
|
+
|
|
60
|
+
New exports: `FailedConnectorInfo`, `writeUnavailableMountBlock`,
|
|
61
|
+
`removeUnavailableMountBlock`, `ensureRefreshAttemptStamp`,
|
|
62
|
+
`WriteUnavailableMountBlockOptions`, `RemoveUnavailableMountBlockOptions`;
|
|
63
|
+
`RenderHelperOpts` gains `unavailableReason`, `refreshCooldownSec`, and
|
|
64
|
+
`refreshAttemptPath`.
|
|
65
|
+
|
|
66
|
+
- [#583](https://github.com/skaile-ai/workspaces/pull/583) [`de36d38`](https://github.com/skaile-ai/workspaces/commit/de36d389f2f0ccf37f5b4203c7a4e4da16452870) Thanks [@peteralbert](https://github.com/peteralbert)! - Switch AI provider config on a live session, and report the 401/429s that motivate it.
|
|
67
|
+
|
|
68
|
+
A session pinned to a subscription seat that hits its five-hour ceiling used to sit
|
|
69
|
+
there. The platform's re-resolution was ready and working, but it was reachable only
|
|
70
|
+
through a token refresh: `handleRateLimitEvent` logged the `{"status":"rejected"}`
|
|
71
|
+
telemetry and returned, and `onAuthError` is 401-only. In production on 2026-08-29 a
|
|
72
|
+
session watched its seat get parked, hit the ceiling twenty minutes later, and made
|
|
73
|
+
zero credential calls for the next fifty minutes — while three sibling sessions
|
|
74
|
+
failed over cleanly, purely because their tokens happened to expire on schedule.
|
|
75
|
+
|
|
76
|
+
Three paths now reach the same single-flight re-pull, which restashes
|
|
77
|
+
`aiProviderConfigId` and queues a driver restart when the platform mints for a
|
|
78
|
+
different seat:
|
|
79
|
+
|
|
80
|
+
- a `credentialRotated` broadcast carrying a `configId`;
|
|
81
|
+
- any mint result carrying a `configId` that differs from the stashed one — so an
|
|
82
|
+
ordinary refresh self-heals with no broadcast delivered at all;
|
|
83
|
+
- a new `AgentConfig.onLimitBlocked` driver callback, fired once per logical turn on
|
|
84
|
+
an upstream usage-limit rejection. A different seat back means the driver respawns
|
|
85
|
+
its CLI and replays the prompt once; the same seat back means the original limit
|
|
86
|
+
error surfaces unchanged. The respawn is the point: a resident CLI caches "blocked
|
|
87
|
+
until <reset>" and short-circuits locally, so rewriting the credentials file alone
|
|
88
|
+
changes nothing.
|
|
89
|
+
|
|
90
|
+
Alongside it, `AgentConfig.onProviderResponse` reports observed 401s and 429s, which
|
|
91
|
+
the runner forwards as a new `provider_response_seen` event so the platform can bump
|
|
92
|
+
the owning config's health counters — the cron probe samples auth, never capacity, so
|
|
93
|
+
a seat exhausted in live traffic still read as green.
|
|
94
|
+
|
|
95
|
+
Protocol 3.8.0 → 3.9.0. Everything is additive and optional in both directions: the
|
|
96
|
+
platform half ships separately and either side works against the other unchanged.
|
|
97
|
+
|
|
98
|
+
Two couplings worth knowing, both recorded in `MIGRATION.md`. The seat switch can only
|
|
99
|
+
fire once the platform returns `configId` on the mint — a re-resolution otherwise names
|
|
100
|
+
the new seat nowhere on the wire, so a limit-blocked turn repairs the credentials file
|
|
101
|
+
but cannot know to replay. And the limit re-pull deliberately sends `reason: "retry-401"`
|
|
102
|
+
rather than `"refresh"`: `refresh` is served from the platform's per-config cache without
|
|
103
|
+
ever reaching the resolver, and the resolver is the only place that notices the seat is
|
|
104
|
+
parked.
|
|
105
|
+
|
|
3
106
|
## 3.7.3
|
|
4
107
|
|
|
5
108
|
### Patch Changes
|
package/dist/app-sdk/index.js
CHANGED
|
@@ -65,7 +65,7 @@ async function libraryAssets() {
|
|
|
65
65
|
const domains = [];
|
|
66
66
|
try {
|
|
67
67
|
const [{ openLibraryManager }, { skaileHomeDir }, { getGlobalCacheDir }] = await Promise.all([
|
|
68
|
-
import('./open-library-
|
|
68
|
+
import('./open-library-PHUMBYHP.js'),
|
|
69
69
|
import('./library/index.js'),
|
|
70
70
|
import('./core/index.js')
|
|
71
71
|
]);
|
|
@@ -98,7 +98,7 @@ async function libraryAssets() {
|
|
|
98
98
|
async function storeAssets(projectDir) {
|
|
99
99
|
const notes = [];
|
|
100
100
|
try {
|
|
101
|
-
const { resolveCatalogSource } = await import('./open-library-
|
|
101
|
+
const { resolveCatalogSource } = await import('./open-library-PHUMBYHP.js');
|
|
102
102
|
const { source, close } = await resolveCatalogSource({ projectDir });
|
|
103
103
|
try {
|
|
104
104
|
const assets = await source.listAssets();
|
|
@@ -202,5 +202,5 @@ function dedupeBySlug(domains) {
|
|
|
202
202
|
}
|
|
203
203
|
|
|
204
204
|
export { gatherAssetFeeds };
|
|
205
|
-
//# sourceMappingURL=asset-feeds-
|
|
206
|
-
//# sourceMappingURL=asset-feeds-
|
|
205
|
+
//# sourceMappingURL=asset-feeds-7HM3HONM.js.map
|
|
206
|
+
//# sourceMappingURL=asset-feeds-7HM3HONM.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../cli/src/asset-feeds.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;AAqFA,SAAS,QAAQ,CAAA,EAAmB;AAClC,EAAA,OAAO,CAAA,CAAE,KAAA,CAAW,IAAA,CAAA,GAAG,CAAA,CAAE,KAAK,GAAG,CAAA;AACnC;AAGA,eAAe,cAAc,IAAA,EAAmC;AAC9D,EAAA,MAAM,MAAkB,EAAC;AACzB,EAAA,WAAA,MAAiB,EAAE,IAAA,EAAM,QAAA,EAAU,MAAK,IAAK,OAAA,CAAQ,IAAI,CAAA,EAAG;AAC1D,IAAA,IAAI,SAAS,kBAAA,EAAoB;AACjC,IAAA,IAAI;AACF,MAAA,MAAM,EAAE,IAAA,EAAK,GAAI,iBAAiB,YAAA,CAAa,QAAA,EAAU,MAAM,CAAC,CAAA;AAChE,MAAA,GAAA,CAAI,IAAA,CAAK,cAAc,OAAA,CAAa,IAAA,CAAA,QAAA,CAAS,MAAM,QAAQ,CAAC,CAAA,EAAG,IAAI,CAAC,CAAA;AAAA,IACtE,CAAA,CAAA,MAAQ;AAAA,IAAC;AAAA,EACX;AACA,EAAA,OAAO,GAAA;AACT;AAcA,eAAe,YAAA,CACb,IAAA,EACA,YAAA,EACA,OAAA,EACA,qBAAqB,IAAA,EACK;AAC1B,EAAA,MAAM,SAAA,GAAY,MAAM,aAAA,CAAc,IAAI,CAAA;AAC1C,EAAA,MAAM,WAAA,GAAc,OAAA,CAAQ,GAAA,CAAI,CAAC,KAAA,MAAW;AAAA,IAC1C,KAAA;AAAA,IACA,KAAK,CAAA,EAAG,KAAA,CAAM,IAAI,CAAA,CAAA,EAAI,MAAM,IAAI,CAAA,CAAA;AAAA,IAChC,cAAc,OAAA,CAAa,IAAA,CAAA,QAAA,CAAS,IAAA,EAAM,KAAA,CAAM,MAAM,CAAC;AAAA,GACzD,CAAE,CAAA;AAEF,EAAA,IAAI,YAAA,GAAe,KAAA;AACnB,EAAA,KAAA,MAAW,KAAK,WAAA,EAAa;AAC3B,IAAA,MAAM,IAAA,GAAO,YAAA,CAAa,CAAA,CAAE,YAAA,EAAc,SAAS,CAAA;AACnD,IAAA,IAAI,IAAA,EAAM,CAAA,CAAE,KAAA,CAAM,UAAA,GAAa,IAAA;AAAA,SAAA,IACtB,kBAAA,EAAoB;AAC3B,MAAA,CAAA,CAAE,MAAM,UAAA,GAAa,YAAA;AACrB,MAAA,YAAA,GAAe,IAAA;AAAA,IACjB,CAAA,MAAO,CAAA,CAAE,KAAA,CAAM,UAAA,GAAa,EAAA;AAAA,EAC9B;AAEA,EAAA,MAAM,EAAE,KAAA,EAAM,GAAI,gBAAA,CAAiB,aAAa,SAAS,CAAA;AACzD,EAAA,MAAM,UAA2B,MAAA,CAAO,MAAA,CAAO,KAAK,CAAA,CAAE,GAAA,CAAI,CAAC,CAAA,MAAO;AAAA,IAChE,MAAM,CAAA,CAAE,IAAA;AAAA,IACR,OAAO,CAAA,CAAE,KAAA;AAAA,IACT,GAAI,EAAE,WAAA,GAAc,EAAE,aAAa,CAAA,CAAE,WAAA,KAAgB,EAAC;AAAA,IACtD,GAAI,EAAE,QAAA,GAAW,EAAE,UAAU,CAAA,CAAE,QAAA,KAAa,EAAC;AAAA,IAC7C,GAAI,EAAE,KAAA,KAAU,MAAA,GAAY,EAAE,KAAA,EAAO,CAAA,CAAE,KAAA,EAAM,GAAI,EAAC;AAAA,IAClD,GAAI,CAAA,CAAE,WAAA,CAAY,CAAC,CAAA,GAAI,EAAE,cAAA,EAAgB,CAAA,CAAE,WAAA,CAAY,CAAC,CAAA,EAAE,GAAI;AAAC,GACjE,CAAE,CAAA;AACF,EAAA,IAAI,YAAA,UAAsB,IAAA,CAAK,EAAE,MAAM,YAAA,EAAc,KAAA,EAAO,cAAc,CAAA;AAC1E,EAAA,OAAO,OAAA;AACT;AAUA,eAAe,aAAA,GAIZ;AACD,EAAA,MAAM,QAAoB,EAAC;AAC3B,EAAA,MAAM,UAA2B,EAAC;AAClC,EAAA,IAAI;AACF,IAAA,MAAM,CAAC,EAAE,kBAAA,EAAmB,EAAG,EAAE,aAAA,EAAc,EAAG,EAAE,iBAAA,EAAmB,CAAA,GAAI,MAAM,QAAQ,GAAA,CAAI;AAAA,MAC3F,OAAO,4BAAmB,CAAA;AAAA,MAC1B,OAAO,oBAA4B,CAAA;AAAA,MACnC,OAAO,iBAAyB;AAAA,KACjC,CAAA;AACD,IAAA,MAAM,EAAE,OAAA,EAAS,KAAA,EAAM,GAAI,MAAM,kBAAA,EAAmB;AACpD,IAAA,IAAI;AAIF,MAAA,MAAM,UAAU,iBAAA,EAAkB;AAClC,MAAA,MAAM,UAAA,GAAkB,IAAA,CAAA,IAAA,CAAK,aAAA,EAAc,EAAG,SAAS,CAAA;AACvD,MAAA,MAAM,IAAA,GAAA,CAAQ,MAAM,OAAA,CAAQ,aAAA,EAAc,EAAG,MAAA;AAAA,QAC3C,CAAC,CAAA,KAAM,CAAC,CAAA,CAAE,IAAA,CAAK,UAAA,CAAW,OAAO,CAAA,IAAK,CAAC,CAAA,CAAE,IAAA,CAAK,UAAA,CAAW,UAAU;AAAA,OACrE;AACA,MAAA,MAAM,UAAyB,EAAC;AAChC,MAAA,KAAA,MAAW,OAAO,IAAA,EAAM;AACtB,QAAA,IAAI,CAAC,UAAA,CAAW,GAAA,CAAI,IAAI,CAAA,EAAG;AAI3B,QAAA,MAAM,UAAA,GAAa,CAAC,GAAI,MAAM,SAAS,GAAA,CAAI,IAAA,EAAM,GAAA,CAAI,IAAI,CAAE,CAAA;AAC3D,QAAA,OAAA,CAAQ,IAAA,CAAK,GAAI,MAAM,YAAA,CAAa,GAAA,CAAI,MAAM,GAAA,CAAI,IAAA,EAAM,UAAA,EAAY,KAAK,CAAE,CAAA;AAC3E,QAAA,KAAA,MAAW,KAAA,IAAS,UAAA,EAAY,OAAA,CAAQ,IAAA,CAAK,EAAE,MAAA,EAAQ,SAAA,EAAW,KAAA,EAAO,KAAA,EAAO,GAAA,CAAI,IAAA,EAAM,CAAA;AAAA,MAC5F;AACA,MAAA,OAAO,EAAE,OAAA,EAAS,OAAA,EAAS,KAAA,EAAM;AAAA,IACnC,CAAA,SAAE;AACA,MAAA,KAAA,EAAM;AAAA,IACR;AAAA,EACF,SAAS,GAAA,EAAK;AACZ,IAAA,KAAA,CAAM,IAAA,CAAK;AAAA,MACT,IAAA,EAAM,WAAA;AAAA,MACN,SAAS,GAAA,YAAe,KAAA,GAAQ,GAAA,CAAI,OAAA,GAAU,OAAO,GAAG;AAAA,KACzD,CAAA;AACD,IAAA,OAAO,EAAE,OAAA,EAAS,EAAC,EAAG,SAAS,KAAA,EAAM;AAAA,EACvC;AACF;AAYA,eAAe,YAAY,UAAA,EAIxB;AACD,EAAA,MAAM,QAAoB,EAAC;AAC3B,EAAA,IAAI;AACF,IAAA,MAAM,EAAE,oBAAA,EAAqB,GAAI,MAAM,OAAO,4BAAmB,CAAA;AACjE,IAAA,MAAM,EAAE,QAAQ,KAAA,EAAM,GAAI,MAAM,oBAAA,CAAqB,EAAE,YAAY,CAAA;AACnE,IAAA,IAAI;AACF,MAAA,MAAM,MAAA,GAAS,MAAM,MAAA,CAAO,UAAA,EAAW;AACvC,MAAA,MAAM,OAAA,GAA0B,MAAA,CAAO,GAAA,CAAI,CAAC,CAAA,MAAO;AAAA,QACjD,MAAM,CAAA,CAAE,IAAA;AAAA,QACR,IAAA,EAAO,EAAE,IAAA,IAAsB,OAAA;AAAA,QAC/B,WAAA,EAAa,EAAE,WAAA,IAAe,EAAA;AAAA,QAC9B,MAAA,EAAQ,CAAA,QAAA,EAAW,CAAA,CAAE,EAAE,CAAA,CAAA;AAAA,QACvB,WAAW,CAAA,CAAE,SAAA;AAAA,QACb,MAAM,CAAA,CAAE,IAAA;AAAA,QACR,YAAY,CAAA,CAAE,UAAA;AAAA,QACd,UAAU,CAAA,CAAE,QAAA;AAAA,QACZ,SAAS,CAAA,CAAE,OAAA;AAAA,QACX,UAAU,EAAC;AAAA,QACX,cAAc;AAAC,OACjB,CAAE,CAAA;AAIF,MAAA,MAAM,OAAA,GAAU,MAAM,WAAA,CAAY,MAAM,CAAA;AACxC,MAAA,OAAO,EAAE,OAAA,EAAS,OAAA,EAAS,KAAA,EAAM;AAAA,IACnC,CAAA,SAAE;AACA,MAAA,KAAA,EAAM;AAAA,IACR;AAAA,EACF,SAAS,GAAA,EAAK;AACZ,IAAA,MAAM,MAAM,GAAA,YAAe,KAAA,GAAQ,GAAA,CAAI,OAAA,GAAU,OAAO,GAAG,CAAA;AAG3D,IAAA,IAAI,CAAC,QAAA,CAAS,IAAA,CAAK,GAAG,CAAA,EAAG;AACvB,MAAA,KAAA,CAAM,KAAK,EAAE,IAAA,EAAM,OAAA,EAAS,OAAA,EAAS,KAAK,CAAA;AAAA,IAC5C;AACA,IAAA,OAAO,EAAE,OAAA,EAAS,IAAI,OAAA,EAAS,IAAI,KAAA,EAAM;AAAA,EAC3C;AACF;AAGA,eAAe,YAAY,MAAA,EAA2C;AACpE,EAAA,MAAM,KAAM,MAAA,CAA4D,WAAA;AACxE,EAAA,IAAI,OAAO,EAAA,KAAO,UAAA,EAAY,OAAO,EAAC;AACtC,EAAA,MAAM,OAAA,GAAU,MAAM,EAAA,CAAG,IAAA,CAAK,MAAM,CAAA;AACpC,EAAA,OAAO,KAAA,CAAM,OAAA,CAAQ,OAAO,CAAA,GAAI,UAAU,EAAC;AAC7C;AAcA,eAAe,aACb,EAAA,EAC+D;AAI/D,EAAA,MAAM,eAAA,GAAkB,iBAAA,EAAkB,CAAE,GAAA,CAAI,CAAC,CAAA,KAAO,CAAA,CAAE,QAAA,CAAc,IAAA,CAAA,GAAG,CAAA,GAAI,CAAA,GAAI,CAAA,GAAS,IAAA,CAAA,GAAI,CAAA;AAChG,EAAA,MAAM,gBAAgB,CAAC,CAAA,KACrB,eAAA,CAAgB,IAAA,CAAK,CAAC,EAAA,KAAO,CAAA,KAAM,EAAA,CAAG,KAAA,CAAM,GAAG,EAAE,CAAA,IAAK,CAAA,CAAE,UAAA,CAAW,EAAE,CAAC,CAAA;AAExE,EAAA,MAAM,GAAA,GAAA,CAAO,MAAM,EAAA,CAAG,MAAA,EAAO,EAAG,MAAA,CAAO,CAAC,CAAA,KAAM,CAAC,aAAA,CAAc,CAAA,CAAE,MAAM,CAAC,CAAA;AACtE,EAAA,MAAM,UAAyB,EAAC;AAChC,EAAA,MAAM,UAA2B,EAAC;AAMlC,EAAA,KAAA,MAAW,EAAE,QAAA,EAAU,IAAA,EAAK,IAAK,EAAA,CAAG,iBAAgB,EAAG;AACrD,IAAA,IAAI,aAAA,CAAc,IAAI,CAAA,EAAG;AACzB,IAAA,MAAM,SAAS,IAAA,CAAK,QAAA,CAAc,IAAA,CAAA,GAAG,CAAA,GAAI,OAAO,IAAA,GAAY,IAAA,CAAA,GAAA;AAC5D,IAAA,MAAM,KAAA,GAAQ,GAAA,CAAI,MAAA,CAAO,CAAC,CAAA,KAAM,CAAA,CAAE,MAAA,KAAW,IAAA,IAAQ,CAAA,CAAE,MAAA,CAAO,UAAA,CAAW,MAAM,CAAC,CAAA;AAChF,IAAA,IAAI,KAAA,CAAM,WAAW,CAAA,EAAG;AACxB,IAAA,MAAM,aAAa,QAAA,CAAS,KAAA,CAAM,GAAG,CAAA,CAAE,KAAI,IAAK,QAAA;AAGhD,IAAA,OAAA,CAAQ,IAAA,CAAK,GAAI,MAAM,YAAA,CAAa,MAAM,UAAA,EAAY,KAAA,EAAO,KAAK,CAAE,CAAA;AACpE,IAAA,KAAA,MAAW,KAAA,IAAS,KAAA,EAAO,OAAA,CAAQ,IAAA,CAAK,EAAE,QAAQ,QAAA,EAAU,KAAA,EAAO,KAAA,EAAO,UAAA,EAAY,CAAA;AAAA,EACxF;AACA,EAAA,OAAO,EAAE,SAAS,OAAA,EAAQ;AAC5B;AAUA,eAAe,aAAA,GAIZ;AACD,EAAA,MAAM,QAAoB,EAAC;AAC3B,EAAA,MAAM,UAA2B,EAAC;AAClC,EAAA,IAAI;AACF,IAAA,MAAM,UAA0B,EAAC;AACjC,IAAA,KAAA,MAAW,IAAA,IAAQ,mBAAkB,EAAG;AACtC,MAAA,MAAM,cAAc,CAAC,GAAI,MAAM,QAAA,CAAS,IAAA,EAAM,gBAAgB,CAAE,CAAA;AAChE,MAAA,OAAA,CAAQ,KAAK,GAAI,MAAM,aAAa,IAAA,EAAM,gBAAA,EAAkB,WAAW,CAAE,CAAA;AACzE,MAAA,OAAA,CAAQ,IAAA,CAAK,GAAG,WAAW,CAAA;AAAA,IAC7B;AACA,IAAA,OAAO,EAAE,OAAA,EAAS,OAAA,EAAS,KAAA,EAAM;AAAA,EACnC,SAAS,GAAA,EAAK;AACZ,IAAA,KAAA,CAAM,IAAA,CAAK;AAAA,MACT,IAAA,EAAM,SAAA;AAAA,MACN,SAAS,GAAA,YAAe,KAAA,GAAQ,GAAA,CAAI,OAAA,GAAU,OAAO,GAAG;AAAA,KACzD,CAAA;AACD,IAAA,OAAO,EAAE,OAAA,EAAS,EAAC,EAAG,SAAS,KAAA,EAAM;AAAA,EACvC;AACF;AAGA,SAAS,GAAA,CAAI,QAAqB,OAAA,EAAwC;AACxE,EAAA,OAAO,QAAQ,GAAA,CAAI,CAAC,WAAW,EAAE,MAAA,EAAQ,OAAM,CAAE,CAAA;AACnD;AASA,eAAsB,gBAAA,CACpB,IACA,UAAA,EAC0B;AAC1B,EAAA,MAAM,CAAC,IAAA,EAAM,KAAK,CAAA,GAAI,MAAM,OAAA,CAAQ,GAAA,CAAI,CAAC,aAAA,EAAc,EAAG,WAAA,CAAY,UAAU,CAAC,CAAC,CAAA;AAClF,EAAA,MAAM,OAAA,GAAU,MAAM,aAAA,EAAc;AACpC,EAAA,MAAM,GAAA,GAAM,MAAM,YAAA,CAAa,EAAE,CAAA;AACjC,EAAA,OAAO;AAAA,IACL,OAAA,EAAS;AAAA,MACP,GAAG,GAAA,CAAI,OAAA;AAAA,MACP,GAAG,IAAA,CAAK,OAAA;AAAA,MACR,GAAG,GAAA,CAAI,SAAA,EAAW,OAAA,CAAQ,OAAO,CAAA;AAAA,MACjC,GAAG,GAAA,CAAI,OAAA,EAAS,KAAA,CAAM,OAAO;AAAA,KAC/B;AAAA;AAAA;AAAA,IAGA,OAAA,EAAS,YAAA,CAAa,CAAC,GAAG,MAAM,OAAA,EAAS,GAAG,GAAA,CAAI,OAAA,EAAS,GAAG,IAAA,CAAK,OAAA,EAAS,GAAG,OAAA,CAAQ,OAAO,CAAC,CAAA;AAAA,IAC7F,KAAA,EAAO,CAAC,GAAG,IAAA,CAAK,KAAA,EAAO,GAAG,KAAA,CAAM,KAAA,EAAO,GAAG,OAAA,CAAQ,KAAK;AAAA,GACzD;AACF;AAGA,SAAS,aAAa,OAAA,EAA2C;AAC/D,EAAA,MAAM,IAAA,uBAAW,GAAA,EAAY;AAC7B,EAAA,MAAM,MAAuB,EAAC;AAC9B,EAAA,KAAA,MAAW,KAAK,OAAA,EAAS;AACvB,IAAA,IAAI,IAAA,CAAK,GAAA,CAAI,CAAA,CAAE,IAAI,CAAA,EAAG;AACtB,IAAA,IAAA,CAAK,GAAA,CAAI,EAAE,IAAI,CAAA;AACf,IAAA,GAAA,CAAI,KAAK,CAAC,CAAA;AAAA,EACZ;AACA,EAAA,OAAO,GAAA;AACT","file":"asset-feeds-6XDLVHQJ.js","sourcesContent":["/**\n * asset-feeds.ts — gather installable assets from every feed the manage TUI\n * surfaces.\n *\n * Four feeds, normalized into `CatalogEntry`:\n * - **Project sources** — `skaile.yaml` `sources:` entries (scanned via\n * `AssetManager.search()`; the new `sources:` field is merged into the\n * legacy repositories map in `AssetManager.loadConfig`).\n * - **Local libraries** — user authoring places from `LocalIndex` (filesystem\n * scan of each library's `path`).\n * - **Factory assets** — the bundled first-party `factory-assets` tree\n * (`factoryAssetRoots()`), scanned from disk. Always present; no network.\n * - **Remote store** — the configured catalog source when `catalog.url` is\n * not `local`. Failures are non-fatal — they surface as notes.\n */\n\nimport { existsSync, readFileSync } from \"node:fs\";\nimport * as path from \"node:path\";\nimport {\n type AssetKind,\n type CatalogDomain,\n type CatalogEntry,\n factoryAssetRoots,\n parseFrontmatter,\n scanRepo,\n walkDir,\n} from \"@skaile/workspaces/core\";\nimport {\n assignDomain,\n buildDomainIndex,\n DOMAIN_MD_FILENAME,\n type DomainMd,\n parseDomainMd,\n} from \"@skaile/workspaces/discovery\";\n\n/** One non-fatal note returned to the caller for surfacing in the UI. */\nexport interface FeedNote {\n feed: \"sources\" | \"libraries\" | \"store\" | \"factory\";\n message: string;\n}\n\n/**\n * Which feed an asset came from. This is the top-level grouping axis in the\n * manage TUI (\"where is this skill coming from?\") and is intentionally distinct\n * from `publisher` (asset-intrinsic identity): the same publisher can legitimately\n * appear under more than one origin (e.g. a github repo you added as a `source`\n * that is also listed in the remote `store`).\n */\nexport type AssetOrigin = \"source\" | \"library\" | \"factory\" | \"store\";\n\n/** A catalog entry tagged with the feed it was gathered from. */\nexport interface OriginEntry {\n origin: AssetOrigin;\n entry: CatalogEntry;\n /**\n * Second-level grouping label for the manage tree. Defaults (when unset) to the\n * entry's `publisher`; the source feed sets it to the source's short repo name\n * so the Sources band groups by source — \"which repo did this come from\" — not\n * by the asset-intrinsic publisher.\n */\n group?: string;\n}\n\nexport interface AssetFeedResult {\n entries: OriginEntry[];\n /**\n * Display-only domain metadata, keyed (by the consumer) on `slug`. Populated\n * from the store's `catalog.listDomains` when the connected catalog source\n * exposes it; empty otherwise. The manage TUI joins `entry.domainSlug` → a\n * domain for grouping headers, descriptions, and relation badges.\n */\n domains: CatalogDomain[];\n notes: FeedNote[];\n}\n\n// ── Domain stamping (on-disk feeds) ──────────────────────────────────────────\n//\n// `scanRepo` (the source / library / factory scan path) does not read the\n// `DOMAIN.md` sidecars, so its entries arrive with no `domainSlug` and render\n// flat under their publisher band. We re-derive the grouping here from the\n// root's `DOMAIN.md` files (the same sidecars the remote store computes\n// server-side), and fall back to the repo/source name for any entry no domain\n// claims so it still groups instead of landing in the synthetic \"no domain\" pile.\n\n/** Repo-relative POSIX path (DOMAIN.md slugs are matched whole-segment, `/`-joined). */\nfunction toPosix(p: string): string {\n return p.split(path.sep).join(\"/\");\n}\n\n/** Parse every `DOMAIN.md` sidecar under `root` into a {@link DomainMd}. */\nasync function loadDomainMds(root: string): Promise<DomainMd[]> {\n const out: DomainMd[] = [];\n for await (const { path: filePath, name } of walkDir(root)) {\n if (name !== DOMAIN_MD_FILENAME) continue;\n try {\n const { data } = parseFrontmatter(readFileSync(filePath, \"utf8\"));\n out.push(parseDomainMd(toPosix(path.relative(root, filePath)), data));\n } catch {}\n }\n return out;\n}\n\n/**\n * Stamp `domainSlug` onto every entry scanned from `root` using the root's\n * `DOMAIN.md` sidecars. Returns the `CatalogDomain` registry for the real\n * domains, plus a synthetic fallback domain when any entry used the fallback.\n * Mutates `entries` in place (sets `entry.domainSlug`).\n *\n * `synthesizeFallback` (default true): entries no domain claims fall back to\n * `fallbackSlug` (the repo/source name). Pass `false` when the caller already\n * groups these entries one level up (e.g. the source feed groups by source\n * node) — unmatched entries then get no domain (`\"\"`) and hang directly under\n * that parent, instead of nesting under a same-named fallback domain.\n */\nasync function stampDomains(\n root: string,\n fallbackSlug: string,\n entries: CatalogEntry[],\n synthesizeFallback = true,\n): Promise<CatalogDomain[]> {\n const domainMds = await loadDomainMds(root);\n const assignables = entries.map((entry) => ({\n entry,\n ref: `${entry.kind}:${entry.name}`,\n relativePath: toPosix(path.relative(root, entry.source)),\n }));\n\n let usedFallback = false;\n for (const a of assignables) {\n const slug = assignDomain(a.relativePath, domainMds);\n if (slug) a.entry.domainSlug = slug;\n else if (synthesizeFallback) {\n a.entry.domainSlug = fallbackSlug;\n usedFallback = true;\n } else a.entry.domainSlug = \"\";\n }\n\n const { index } = buildDomainIndex(assignables, domainMds);\n const catalog: CatalogDomain[] = Object.values(index).map((d) => ({\n slug: d.slug,\n title: d.title,\n ...(d.description ? { description: d.description } : {}),\n ...(d.relation ? { relation: d.relation } : {}),\n ...(d.order !== undefined ? { order: d.order } : {}),\n ...(d.recommended[0] ? { recommendedRef: d.recommended[0] } : {}),\n }));\n if (usedFallback) catalog.push({ slug: fallbackSlug, title: fallbackSlug });\n return catalog;\n}\n\n/**\n * Pull assets from local libraries (the authoring places under\n * `~/.skaile/libraries/<name>/`, not the `~/.skaile/cache/sources/` cache). Each\n * library's `path` is scanned with `scanRepo` and grouped under the bare library\n * name (`group: <name>`) so the manage TUI shows a clean `<name>` band — no\n * `library:` prefix and no redundant same-named fallback domain (mirrors the\n * source feed).\n */\nasync function libraryAssets(): Promise<{\n entries: OriginEntry[];\n domains: CatalogDomain[];\n notes: FeedNote[];\n}> {\n const notes: FeedNote[] = [];\n const domains: CatalogDomain[] = [];\n try {\n const [{ openLibraryManager }, { skaileHomeDir }, { getGlobalCacheDir }] = await Promise.all([\n import(\"./open-library.ts\"),\n import(\"@skaile/workspaces/library\"),\n import(\"@skaile/workspaces/core\"),\n ]);\n const { manager, close } = await openLibraryManager();\n try {\n // Authoring libraries are the rows that are NOT source clones. index.db\n // rows predating the cache migration still carry the legacy\n // ~/.skaile/sources path, so exclude both roots.\n const newRoot = getGlobalCacheDir();\n const legacyRoot = path.join(skaileHomeDir(), \"sources\");\n const libs = (await manager.listLibraries()).filter(\n (l) => !l.path.startsWith(newRoot) && !l.path.startsWith(legacyRoot),\n );\n const entries: OriginEntry[] = [];\n for (const lib of libs) {\n if (!existsSync(lib.path)) continue;\n // Scan + group under the bare library name. `synthesizeFallback: false`\n // (as the source feed does) keeps domainless assets directly under the\n // library band instead of a redundant same-named fallback domain.\n const libEntries = [...(await scanRepo(lib.path, lib.name))];\n domains.push(...(await stampDomains(lib.path, lib.name, libEntries, false)));\n for (const entry of libEntries) entries.push({ origin: \"library\", entry, group: lib.name });\n }\n return { entries, domains, notes };\n } finally {\n close();\n }\n } catch (err) {\n notes.push({\n feed: \"libraries\",\n message: err instanceof Error ? err.message : String(err),\n });\n return { entries: [], domains, notes };\n }\n}\n\n/**\n * Pull a (best-effort) snapshot of the connected remote store catalog.\n *\n * - Returns no entries when the resolved catalog is local-only or the host\n * is unreachable (the failure becomes a note, not an exception).\n * - Maps `CatalogAsset` → `CatalogEntry`, carrying `publisher`, `repo`, and\n * `category` straight through and using `source = \"store://<id>\"`; per-file\n * `source` paths are not available for remote assets so the canonical ref is\n * a stable placeholder.\n */\nasync function storeAssets(projectDir: string): Promise<{\n entries: CatalogEntry[];\n domains: CatalogDomain[];\n notes: FeedNote[];\n}> {\n const notes: FeedNote[] = [];\n try {\n const { resolveCatalogSource } = await import(\"./open-library.ts\");\n const { source, close } = await resolveCatalogSource({ projectDir });\n try {\n const assets = await source.listAssets();\n const entries: CatalogEntry[] = assets.map((a) => ({\n name: a.name,\n kind: (a.kind as AssetKind) ?? \"skill\",\n description: a.description ?? \"\",\n source: `store://${a.id}`,\n publisher: a.publisher,\n repo: a.repo,\n domainSlug: a.domainSlug,\n category: a.category,\n version: a.version,\n requires: [],\n dependencies: [],\n }));\n // Domains are optional on the catalog source — only the store backend\n // (post Part 4 `catalog.listDomains`) exposes them. Duck-type the call so\n // older sources and the local catalog degrade to no domains, not an error.\n const domains = await loadDomains(source);\n return { entries, domains, notes };\n } finally {\n close();\n }\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n // Local-only catalog throws a recognizable message — silence it; absence\n // is the user's choice, not an error.\n if (!/local/i.test(msg)) {\n notes.push({ feed: \"store\", message: msg });\n }\n return { entries: [], domains: [], notes };\n }\n}\n\n/** Optional `listDomains` capability — returns `[]` when the source lacks it. */\nasync function loadDomains(source: unknown): Promise<CatalogDomain[]> {\n const fn = (source as { listDomains?: () => Promise<CatalogDomain[]> }).listDomains;\n if (typeof fn !== \"function\") return [];\n const domains = await fn.call(source);\n return Array.isArray(domains) ? domains : [];\n}\n\n/** The slice of `AssetManager` the source feed needs (kept narrow for tests). */\ninterface AssetManagerLike {\n search: () => Promise<CatalogEntry[]>;\n searchRepoRoots: () => { repoName: string; root: string }[];\n}\n\n/**\n * Project-source feed: leans on `AssetManager.search()`, which now merges\n * `skaile.yaml`'s `sources:` field into the repositories map, then stamps\n * `DOMAIN.md`-derived domain grouping onto the scanned entries via\n * `searchRepoRoots()`.\n */\nasync function sourceAssets(\n am: AssetManagerLike,\n): Promise<{ entries: OriginEntry[]; domains: CatalogDomain[] }> {\n // The bundled factory tree is injected into the repositories map as an implicit\n // source, so `am.search()` returns it too. It has its own `factory` origin band,\n // so drop it here — otherwise the same first-party skills list twice under Sources.\n const factoryPrefixes = factoryAssetRoots().map((r) => (r.endsWith(path.sep) ? r : r + path.sep));\n const isFactoryPath = (p: string) =>\n factoryPrefixes.some((fp) => p === fp.slice(0, -1) || p.startsWith(fp));\n\n const all = (await am.search()).filter((e) => !isFactoryPath(e.source));\n const entries: OriginEntry[] = [];\n const domains: CatalogDomain[] = [];\n // Group each source's entries under the source node (its short repo name), not\n // the publisher: under Sources the navigational question is \"which repo did\n // this come from\". Each entry's `source` (absolute manifest path) lives under\n // exactly one declared root, so group + stamp by prefix. The cache slug is\n // URL-keyed `github.com/<org>/<repo>` — take the trailing segment.\n for (const { repoName, root } of am.searchRepoRoots()) {\n if (isFactoryPath(root)) continue;\n const prefix = root.endsWith(path.sep) ? root : root + path.sep;\n const owned = all.filter((e) => e.source === root || e.source.startsWith(prefix));\n if (owned.length === 0) continue;\n const sourceName = repoName.split(\"/\").pop() || repoName;\n // No synthetic fallback domain — the source node already groups these, so a\n // same-named fallback would just nest the source under itself.\n domains.push(...(await stampDomains(root, sourceName, owned, false)));\n for (const entry of owned) entries.push({ origin: \"source\", entry, group: sourceName });\n }\n return { entries, domains };\n}\n\n/**\n * Factory-asset feed: scan the bundled first-party `factory-assets` tree(s)\n * resolved by `factoryAssetRoots()` (the `SKAILE_FACTORY_ASSETS_DIR` override\n * layer ahead of the bundled tree). Local disk only, no network — the entries are\n * the built-in skills / connectors / bundles that ship with `@skaile/workspaces`.\n * `factoryAssetRoots()` already dedupes, so the env override pointing at the\n * bundled dir collapses to one root.\n */\nasync function factoryAssets(): Promise<{\n entries: CatalogEntry[];\n domains: CatalogDomain[];\n notes: FeedNote[];\n}> {\n const notes: FeedNote[] = [];\n const domains: CatalogDomain[] = [];\n try {\n const entries: CatalogEntry[] = [];\n for (const root of factoryAssetRoots()) {\n const rootEntries = [...(await scanRepo(root, \"skaile-factory\"))];\n domains.push(...(await stampDomains(root, \"skaile-factory\", rootEntries)));\n entries.push(...rootEntries);\n }\n return { entries, domains, notes };\n } catch (err) {\n notes.push({\n feed: \"factory\",\n message: err instanceof Error ? err.message : String(err),\n });\n return { entries: [], domains, notes };\n }\n}\n\n/** Tag a flat entry list with a single origin. */\nfunction tag(origin: AssetOrigin, entries: CatalogEntry[]): OriginEntry[] {\n return entries.map((entry) => ({ origin, entry }));\n}\n\n/**\n * Run all feeds and return a single normalized result.\n *\n * Feeds run in parallel where possible — the store fetch is the slow one\n * (one HTTP call) and is fired alongside the local library walk. The factory\n * scan is local disk only, so it runs inline.\n */\nexport async function gatherAssetFeeds(\n am: AssetManagerLike,\n projectDir: string,\n): Promise<AssetFeedResult> {\n const [libs, store] = await Promise.all([libraryAssets(), storeAssets(projectDir)]);\n const factory = await factoryAssets();\n const src = await sourceAssets(am);\n return {\n entries: [\n ...src.entries,\n ...libs.entries,\n ...tag(\"factory\", factory.entries),\n ...tag(\"store\", store.entries),\n ],\n // Store domains first (server-curated), then the on-disk feeds; dedupe by\n // slug, first-wins, so a store-served domain shadows a same-slug local one.\n domains: dedupeBySlug([...store.domains, ...src.domains, ...libs.domains, ...factory.domains]),\n notes: [...libs.notes, ...store.notes, ...factory.notes],\n };\n}\n\n/** First-wins dedupe of a domain list by `slug`. */\nfunction dedupeBySlug(domains: CatalogDomain[]): CatalogDomain[] {\n const seen = new Set<string>();\n const out: CatalogDomain[] = [];\n for (const d of domains) {\n if (seen.has(d.slug)) continue;\n seen.add(d.slug);\n out.push(d);\n }\n return out;\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../cli/src/asset-feeds.ts"],"names":[],"mappings":"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","file":"asset-feeds-7HM3HONM.js","sourcesContent":["/**\n * asset-feeds.ts — gather installable assets from every feed the manage TUI\n * surfaces.\n *\n * Four feeds, normalized into `CatalogEntry`:\n * - **Project sources** — `skaile.yaml` `sources:` entries (scanned via\n * `AssetManager.search()`; the new `sources:` field is merged into the\n * legacy repositories map in `AssetManager.loadConfig`).\n * - **Local libraries** — user authoring places from `LocalIndex` (filesystem\n * scan of each library's `path`).\n * - **Factory assets** — the bundled first-party `factory-assets` tree\n * (`factoryAssetRoots()`), scanned from disk. Always present; no network.\n * - **Remote store** — the configured catalog source when `catalog.url` is\n * not `local`. Failures are non-fatal — they surface as notes.\n */\n\nimport { existsSync, readFileSync } from \"node:fs\";\nimport * as path from \"node:path\";\nimport {\n type AssetKind,\n type CatalogDomain,\n type CatalogEntry,\n factoryAssetRoots,\n parseFrontmatter,\n scanRepo,\n walkDir,\n} from \"@skaile/workspaces/core\";\nimport {\n assignDomain,\n buildDomainIndex,\n DOMAIN_MD_FILENAME,\n type DomainMd,\n parseDomainMd,\n} from \"@skaile/workspaces/discovery\";\n\n/** One non-fatal note returned to the caller for surfacing in the UI. */\nexport interface FeedNote {\n feed: \"sources\" | \"libraries\" | \"store\" | \"factory\";\n message: string;\n}\n\n/**\n * Which feed an asset came from. This is the top-level grouping axis in the\n * manage TUI (\"where is this skill coming from?\") and is intentionally distinct\n * from `publisher` (asset-intrinsic identity): the same publisher can legitimately\n * appear under more than one origin (e.g. a github repo you added as a `source`\n * that is also listed in the remote `store`).\n */\nexport type AssetOrigin = \"source\" | \"library\" | \"factory\" | \"store\";\n\n/** A catalog entry tagged with the feed it was gathered from. */\nexport interface OriginEntry {\n origin: AssetOrigin;\n entry: CatalogEntry;\n /**\n * Second-level grouping label for the manage tree. Defaults (when unset) to the\n * entry's `publisher`; the source feed sets it to the source's short repo name\n * so the Sources band groups by source — \"which repo did this come from\" — not\n * by the asset-intrinsic publisher.\n */\n group?: string;\n}\n\nexport interface AssetFeedResult {\n entries: OriginEntry[];\n /**\n * Display-only domain metadata, keyed (by the consumer) on `slug`. Populated\n * from the store's `catalog.listDomains` when the connected catalog source\n * exposes it; empty otherwise. The manage TUI joins `entry.domainSlug` → a\n * domain for grouping headers, descriptions, and relation badges.\n */\n domains: CatalogDomain[];\n notes: FeedNote[];\n}\n\n// ── Domain stamping (on-disk feeds) ──────────────────────────────────────────\n//\n// `scanRepo` (the source / library / factory scan path) does not read the\n// `DOMAIN.md` sidecars, so its entries arrive with no `domainSlug` and render\n// flat under their publisher band. We re-derive the grouping here from the\n// root's `DOMAIN.md` files (the same sidecars the remote store computes\n// server-side), and fall back to the repo/source name for any entry no domain\n// claims so it still groups instead of landing in the synthetic \"no domain\" pile.\n\n/** Repo-relative POSIX path (DOMAIN.md slugs are matched whole-segment, `/`-joined). */\nfunction toPosix(p: string): string {\n return p.split(path.sep).join(\"/\");\n}\n\n/** Parse every `DOMAIN.md` sidecar under `root` into a {@link DomainMd}. */\nasync function loadDomainMds(root: string): Promise<DomainMd[]> {\n const out: DomainMd[] = [];\n for await (const { path: filePath, name } of walkDir(root)) {\n if (name !== DOMAIN_MD_FILENAME) continue;\n try {\n const { data } = parseFrontmatter(readFileSync(filePath, \"utf8\"));\n out.push(parseDomainMd(toPosix(path.relative(root, filePath)), data));\n } catch {}\n }\n return out;\n}\n\n/**\n * Stamp `domainSlug` onto every entry scanned from `root` using the root's\n * `DOMAIN.md` sidecars. Returns the `CatalogDomain` registry for the real\n * domains, plus a synthetic fallback domain when any entry used the fallback.\n * Mutates `entries` in place (sets `entry.domainSlug`).\n *\n * `synthesizeFallback` (default true): entries no domain claims fall back to\n * `fallbackSlug` (the repo/source name). Pass `false` when the caller already\n * groups these entries one level up (e.g. the source feed groups by source\n * node) — unmatched entries then get no domain (`\"\"`) and hang directly under\n * that parent, instead of nesting under a same-named fallback domain.\n */\nasync function stampDomains(\n root: string,\n fallbackSlug: string,\n entries: CatalogEntry[],\n synthesizeFallback = true,\n): Promise<CatalogDomain[]> {\n const domainMds = await loadDomainMds(root);\n const assignables = entries.map((entry) => ({\n entry,\n ref: `${entry.kind}:${entry.name}`,\n relativePath: toPosix(path.relative(root, entry.source)),\n }));\n\n let usedFallback = false;\n for (const a of assignables) {\n const slug = assignDomain(a.relativePath, domainMds);\n if (slug) a.entry.domainSlug = slug;\n else if (synthesizeFallback) {\n a.entry.domainSlug = fallbackSlug;\n usedFallback = true;\n } else a.entry.domainSlug = \"\";\n }\n\n const { index } = buildDomainIndex(assignables, domainMds);\n const catalog: CatalogDomain[] = Object.values(index).map((d) => ({\n slug: d.slug,\n title: d.title,\n ...(d.description ? { description: d.description } : {}),\n ...(d.relation ? { relation: d.relation } : {}),\n ...(d.order !== undefined ? { order: d.order } : {}),\n ...(d.recommended[0] ? { recommendedRef: d.recommended[0] } : {}),\n }));\n if (usedFallback) catalog.push({ slug: fallbackSlug, title: fallbackSlug });\n return catalog;\n}\n\n/**\n * Pull assets from local libraries (the authoring places under\n * `~/.skaile/libraries/<name>/`, not the `~/.skaile/cache/sources/` cache). Each\n * library's `path` is scanned with `scanRepo` and grouped under the bare library\n * name (`group: <name>`) so the manage TUI shows a clean `<name>` band — no\n * `library:` prefix and no redundant same-named fallback domain (mirrors the\n * source feed).\n */\nasync function libraryAssets(): Promise<{\n entries: OriginEntry[];\n domains: CatalogDomain[];\n notes: FeedNote[];\n}> {\n const notes: FeedNote[] = [];\n const domains: CatalogDomain[] = [];\n try {\n const [{ openLibraryManager }, { skaileHomeDir }, { getGlobalCacheDir }] = await Promise.all([\n import(\"./open-library.ts\"),\n import(\"@skaile/workspaces/library\"),\n import(\"@skaile/workspaces/core\"),\n ]);\n const { manager, close } = await openLibraryManager();\n try {\n // Authoring libraries are the rows that are NOT source clones. index.db\n // rows predating the cache migration still carry the legacy\n // ~/.skaile/sources path, so exclude both roots.\n const newRoot = getGlobalCacheDir();\n const legacyRoot = path.join(skaileHomeDir(), \"sources\");\n const libs = (await manager.listLibraries()).filter(\n (l) => !l.path.startsWith(newRoot) && !l.path.startsWith(legacyRoot),\n );\n const entries: OriginEntry[] = [];\n for (const lib of libs) {\n if (!existsSync(lib.path)) continue;\n // Scan + group under the bare library name. `synthesizeFallback: false`\n // (as the source feed does) keeps domainless assets directly under the\n // library band instead of a redundant same-named fallback domain.\n const libEntries = [...(await scanRepo(lib.path, lib.name))];\n domains.push(...(await stampDomains(lib.path, lib.name, libEntries, false)));\n for (const entry of libEntries) entries.push({ origin: \"library\", entry, group: lib.name });\n }\n return { entries, domains, notes };\n } finally {\n close();\n }\n } catch (err) {\n notes.push({\n feed: \"libraries\",\n message: err instanceof Error ? err.message : String(err),\n });\n return { entries: [], domains, notes };\n }\n}\n\n/**\n * Pull a (best-effort) snapshot of the connected remote store catalog.\n *\n * - Returns no entries when the resolved catalog is local-only or the host\n * is unreachable (the failure becomes a note, not an exception).\n * - Maps `CatalogAsset` → `CatalogEntry`, carrying `publisher`, `repo`, and\n * `category` straight through and using `source = \"store://<id>\"`; per-file\n * `source` paths are not available for remote assets so the canonical ref is\n * a stable placeholder.\n */\nasync function storeAssets(projectDir: string): Promise<{\n entries: CatalogEntry[];\n domains: CatalogDomain[];\n notes: FeedNote[];\n}> {\n const notes: FeedNote[] = [];\n try {\n const { resolveCatalogSource } = await import(\"./open-library.ts\");\n const { source, close } = await resolveCatalogSource({ projectDir });\n try {\n const assets = await source.listAssets();\n const entries: CatalogEntry[] = assets.map((a) => ({\n name: a.name,\n kind: (a.kind as AssetKind) ?? \"skill\",\n description: a.description ?? \"\",\n source: `store://${a.id}`,\n publisher: a.publisher,\n repo: a.repo,\n domainSlug: a.domainSlug,\n category: a.category,\n version: a.version,\n requires: [],\n dependencies: [],\n }));\n // Domains are optional on the catalog source — only the store backend\n // (post Part 4 `catalog.listDomains`) exposes them. Duck-type the call so\n // older sources and the local catalog degrade to no domains, not an error.\n const domains = await loadDomains(source);\n return { entries, domains, notes };\n } finally {\n close();\n }\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n // Local-only catalog throws a recognizable message — silence it; absence\n // is the user's choice, not an error.\n if (!/local/i.test(msg)) {\n notes.push({ feed: \"store\", message: msg });\n }\n return { entries: [], domains: [], notes };\n }\n}\n\n/** Optional `listDomains` capability — returns `[]` when the source lacks it. */\nasync function loadDomains(source: unknown): Promise<CatalogDomain[]> {\n const fn = (source as { listDomains?: () => Promise<CatalogDomain[]> }).listDomains;\n if (typeof fn !== \"function\") return [];\n const domains = await fn.call(source);\n return Array.isArray(domains) ? domains : [];\n}\n\n/** The slice of `AssetManager` the source feed needs (kept narrow for tests). */\ninterface AssetManagerLike {\n search: () => Promise<CatalogEntry[]>;\n searchRepoRoots: () => { repoName: string; root: string }[];\n}\n\n/**\n * Project-source feed: leans on `AssetManager.search()`, which now merges\n * `skaile.yaml`'s `sources:` field into the repositories map, then stamps\n * `DOMAIN.md`-derived domain grouping onto the scanned entries via\n * `searchRepoRoots()`.\n */\nasync function sourceAssets(\n am: AssetManagerLike,\n): Promise<{ entries: OriginEntry[]; domains: CatalogDomain[] }> {\n // The bundled factory tree is injected into the repositories map as an implicit\n // source, so `am.search()` returns it too. It has its own `factory` origin band,\n // so drop it here — otherwise the same first-party skills list twice under Sources.\n const factoryPrefixes = factoryAssetRoots().map((r) => (r.endsWith(path.sep) ? r : r + path.sep));\n const isFactoryPath = (p: string) =>\n factoryPrefixes.some((fp) => p === fp.slice(0, -1) || p.startsWith(fp));\n\n const all = (await am.search()).filter((e) => !isFactoryPath(e.source));\n const entries: OriginEntry[] = [];\n const domains: CatalogDomain[] = [];\n // Group each source's entries under the source node (its short repo name), not\n // the publisher: under Sources the navigational question is \"which repo did\n // this come from\". Each entry's `source` (absolute manifest path) lives under\n // exactly one declared root, so group + stamp by prefix. The cache slug is\n // URL-keyed `github.com/<org>/<repo>` — take the trailing segment.\n for (const { repoName, root } of am.searchRepoRoots()) {\n if (isFactoryPath(root)) continue;\n const prefix = root.endsWith(path.sep) ? root : root + path.sep;\n const owned = all.filter((e) => e.source === root || e.source.startsWith(prefix));\n if (owned.length === 0) continue;\n const sourceName = repoName.split(\"/\").pop() || repoName;\n // No synthetic fallback domain — the source node already groups these, so a\n // same-named fallback would just nest the source under itself.\n domains.push(...(await stampDomains(root, sourceName, owned, false)));\n for (const entry of owned) entries.push({ origin: \"source\", entry, group: sourceName });\n }\n return { entries, domains };\n}\n\n/**\n * Factory-asset feed: scan the bundled first-party `factory-assets` tree(s)\n * resolved by `factoryAssetRoots()` (the `SKAILE_FACTORY_ASSETS_DIR` override\n * layer ahead of the bundled tree). Local disk only, no network — the entries are\n * the built-in skills / connectors / bundles that ship with `@skaile/workspaces`.\n * `factoryAssetRoots()` already dedupes, so the env override pointing at the\n * bundled dir collapses to one root.\n */\nasync function factoryAssets(): Promise<{\n entries: CatalogEntry[];\n domains: CatalogDomain[];\n notes: FeedNote[];\n}> {\n const notes: FeedNote[] = [];\n const domains: CatalogDomain[] = [];\n try {\n const entries: CatalogEntry[] = [];\n for (const root of factoryAssetRoots()) {\n const rootEntries = [...(await scanRepo(root, \"skaile-factory\"))];\n domains.push(...(await stampDomains(root, \"skaile-factory\", rootEntries)));\n entries.push(...rootEntries);\n }\n return { entries, domains, notes };\n } catch (err) {\n notes.push({\n feed: \"factory\",\n message: err instanceof Error ? err.message : String(err),\n });\n return { entries: [], domains, notes };\n }\n}\n\n/** Tag a flat entry list with a single origin. */\nfunction tag(origin: AssetOrigin, entries: CatalogEntry[]): OriginEntry[] {\n return entries.map((entry) => ({ origin, entry }));\n}\n\n/**\n * Run all feeds and return a single normalized result.\n *\n * Feeds run in parallel where possible — the store fetch is the slow one\n * (one HTTP call) and is fired alongside the local library walk. The factory\n * scan is local disk only, so it runs inline.\n */\nexport async function gatherAssetFeeds(\n am: AssetManagerLike,\n projectDir: string,\n): Promise<AssetFeedResult> {\n const [libs, store] = await Promise.all([libraryAssets(), storeAssets(projectDir)]);\n const factory = await factoryAssets();\n const src = await sourceAssets(am);\n return {\n entries: [\n ...src.entries,\n ...libs.entries,\n ...tag(\"factory\", factory.entries),\n ...tag(\"store\", store.entries),\n ],\n // Store domains first (server-curated), then the on-disk feeds; dedupe by\n // slug, first-wins, so a store-served domain shadows a same-slug local one.\n domains: dedupeBySlug([...store.domains, ...src.domains, ...libs.domains, ...factory.domains]),\n notes: [...libs.notes, ...store.notes, ...factory.notes],\n };\n}\n\n/** First-wins dedupe of a domain list by `slug`. */\nfunction dedupeBySlug(domains: CatalogDomain[]): CatalogDomain[] {\n const seen = new Set<string>();\n const out: CatalogDomain[] = [];\n for (const d of domains) {\n if (seen.has(d.slug)) continue;\n seen.add(d.slug);\n out.push(d);\n }\n return out;\n}\n"]}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { AssetManager, FACTORY_SOURCE_URL, MissingManifestError, NON_DEPLOYABLE_KINDS, normalizeRepoUrl, parseCanonicalLockKey, parseRefTolerant, renderMcpServerConfig, softDepName } from '../chunk-
|
|
1
|
+
export { AssetManager, FACTORY_SOURCE_URL, MissingManifestError, NON_DEPLOYABLE_KINDS, normalizeRepoUrl, parseCanonicalLockKey, parseRefTolerant, renderMcpServerConfig, softDepName } from '../chunk-B7TA2XI7.js';
|
|
2
2
|
import '../chunk-OCLCDB7G.js';
|
|
3
3
|
import '../chunk-PW4SJE3X.js';
|
|
4
4
|
import '../chunk-UKJMQZV6.js';
|
|
@@ -2,7 +2,7 @@ import { scrubPoisonedTranscript, resolveClaudeConfigDir, SUPPORTED_CLOUDS, buil
|
|
|
2
2
|
import { dispatchCapability } from '../../chunk-RRVQAE5D.js';
|
|
3
3
|
import { fetchProviderModels } from '../../chunk-V34ZEXSE.js';
|
|
4
4
|
import { AuthError, classifyClaudeSdkError, reclassifyAuthAsAccountUnavailable, AccountUnavailableError } from '../../chunk-GAH4YRCI.js';
|
|
5
|
-
import { BUILTIN_DRIVER_CATALOG, AgentDriver, getBridgeLogger } from '../../chunk-
|
|
5
|
+
import { BUILTIN_DRIVER_CATALOG, AgentDriver, getBridgeLogger } from '../../chunk-BGAAZMBK.js';
|
|
6
6
|
import '../../chunk-6E6PKKAD.js';
|
|
7
7
|
import '../../chunk-AZPXCHDK.js';
|
|
8
8
|
import '../../chunk-F3HNW354.js';
|
|
@@ -159,7 +159,12 @@ if (typeof globalThis.require === "undefined") {
|
|
|
159
159
|
}
|
|
160
160
|
}
|
|
161
161
|
function freshRetryBudget() {
|
|
162
|
-
return { staleResume: false, poison: false, auth: false, queryEnded: false };
|
|
162
|
+
return { staleResume: false, poison: false, auth: false, queryEnded: false, limit: false };
|
|
163
|
+
}
|
|
164
|
+
function isSeatCapacityFailure(detail, subtype) {
|
|
165
|
+
if (detail.category !== "rate_limit") return false;
|
|
166
|
+
if (subtype === "error_max_budget_usd") return false;
|
|
167
|
+
return detail.statusCode !== 529;
|
|
163
168
|
}
|
|
164
169
|
var QueryEndedBeforeTurnError = class extends Error {
|
|
165
170
|
constructor(message) {
|
|
@@ -292,6 +297,52 @@ var ClaudeSdkDriver = class extends AgentDriver {
|
|
|
292
297
|
* reset elsewhere — the next call's set is the only legitimate transition.
|
|
293
298
|
*/
|
|
294
299
|
authSelfHealUsed = false;
|
|
300
|
+
/**
|
|
301
|
+
* Mirror of the in-flight turn's seat-failover budget — same contract as
|
|
302
|
+
* {@link authSelfHealUsed}, for the `onLimitBlocked` path.
|
|
303
|
+
*/
|
|
304
|
+
limitFailoverUsed = false;
|
|
305
|
+
/**
|
|
306
|
+
* Set when THIS attempt saw an upstream usage-limit rejection: either the
|
|
307
|
+
* SDK's `rate_limit_event` telemetry with `status: "rejected"`, or an error
|
|
308
|
+
* result the classifier calls `rate_limit`.
|
|
309
|
+
*
|
|
310
|
+
* The telemetry is the load-bearing half. A subscription ceiling arrives as
|
|
311
|
+
* `{"status":"rejected","type":"five_hour"}` followed by an error result
|
|
312
|
+
* whose text ("You've hit your session limit · resets 9:50am") matches none
|
|
313
|
+
* of the classifier's rate-limit keywords — so without this flag the
|
|
314
|
+
* rejection classifies as `unknown` and no failover is reachable at all.
|
|
315
|
+
*
|
|
316
|
+
* Reset at the top of every {@link runPrompt} attempt, so the replay after a
|
|
317
|
+
* failover observes its own outcome rather than inheriting the first one's.
|
|
318
|
+
*/
|
|
319
|
+
limitRejectionSeen = false;
|
|
320
|
+
/**
|
|
321
|
+
* True when {@link handleResultErrorPayload} rejected this turn ONLY so the
|
|
322
|
+
* seat-failover branch could run — a turn that, before this existed, would
|
|
323
|
+
* have emitted its error and resolved. If the failover then declines, the
|
|
324
|
+
* rejection is undone so a limit block with no peer available looks exactly
|
|
325
|
+
* as it always has downstream (one error event, one `agent_end`, a resolved
|
|
326
|
+
* promise) rather than also drawing the runner's fatal "prompt failed".
|
|
327
|
+
*/
|
|
328
|
+
limitTurnRejectedForFailover = false;
|
|
329
|
+
/**
|
|
330
|
+
* Whether this attempt already reported a provider rejection. One upstream
|
|
331
|
+
* failure routinely reaches the driver twice — `rejected` telemetry followed
|
|
332
|
+
* by the error result, or an `is_error` result followed by the iterator throw
|
|
333
|
+
* carrying the real text — and these observations feed counters, so counting
|
|
334
|
+
* one rejection twice would overstate the seat's pressure.
|
|
335
|
+
*/
|
|
336
|
+
providerResponseReported = false;
|
|
337
|
+
/** Limit-window metadata from this attempt's rejection telemetry, if any. */
|
|
338
|
+
lastLimitInfo = null;
|
|
339
|
+
/**
|
|
340
|
+
* The `agent-event: error` {@link failTurn} withheld because a recovery
|
|
341
|
+
* (auth self-heal or seat failover) was about to run. The recovery emits it
|
|
342
|
+
* if it declines, and drops it if it retries — so a recovered turn shows the
|
|
343
|
+
* user nothing and a failed one shows exactly one error.
|
|
344
|
+
*/
|
|
345
|
+
deferredTurnError = null;
|
|
295
346
|
/** Tracks whether a session has been started (for continue: true). */
|
|
296
347
|
hasSession = false;
|
|
297
348
|
/** Session ID from the SDK — used for streamInput messages. */
|
|
@@ -422,8 +473,14 @@ var ClaudeSdkDriver = class extends AgentDriver {
|
|
|
422
473
|
async runPrompt(message, _budget) {
|
|
423
474
|
if (!this.sdk) await this.start();
|
|
424
475
|
this.authSelfHealUsed = _budget.auth;
|
|
476
|
+
this.limitFailoverUsed = _budget.limit;
|
|
477
|
+
this.limitRejectionSeen = false;
|
|
478
|
+
this.limitTurnRejectedForFailover = false;
|
|
479
|
+
this.providerResponseReported = false;
|
|
480
|
+
this.lastLimitInfo = null;
|
|
481
|
+
this.deferredTurnError = null;
|
|
425
482
|
let retrying = false;
|
|
426
|
-
const isFirstAttempt = !_budget.staleResume && !_budget.poison && !_budget.auth && !_budget.queryEnded;
|
|
483
|
+
const isFirstAttempt = !_budget.staleResume && !_budget.poison && !_budget.auth && !_budget.queryEnded && !_budget.limit;
|
|
427
484
|
if (isFirstAttempt) {
|
|
428
485
|
this._lastTokens = null;
|
|
429
486
|
this._lastContextTokens = null;
|
|
@@ -476,12 +533,26 @@ var ClaudeSdkDriver = class extends AgentDriver {
|
|
|
476
533
|
return this.runPrompt(message, { ..._budget, poison: true });
|
|
477
534
|
}
|
|
478
535
|
}
|
|
479
|
-
const
|
|
480
|
-
if (
|
|
536
|
+
const authOwns = err instanceof AuthError && !_budget.auth && this.config.onAuthError != null;
|
|
537
|
+
if (authOwns) {
|
|
481
538
|
if (await this.attemptAuthSelfHeal(err)) {
|
|
482
539
|
retrying = true;
|
|
483
540
|
return this.runPrompt(message, { ..._budget, auth: true });
|
|
484
541
|
}
|
|
542
|
+
} else if (!_budget.limit && this.limitRejectionSeen && this.config.onLimitBlocked) {
|
|
543
|
+
if (await this.attemptLimitFailover(err)) {
|
|
544
|
+
if (this.abortController?.signal.aborted) {
|
|
545
|
+
this.log.info("aborted during the seat failover; not replaying the prompt");
|
|
546
|
+
this.emitAgentEnd();
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
retrying = true;
|
|
550
|
+
return this.runPrompt(message, { ..._budget, limit: true });
|
|
551
|
+
}
|
|
552
|
+
if (this.limitTurnRejectedForFailover) {
|
|
553
|
+
this.emitAgentEnd();
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
485
556
|
}
|
|
486
557
|
throw err;
|
|
487
558
|
} finally {
|
|
@@ -490,6 +561,10 @@ var ClaudeSdkDriver = class extends AgentDriver {
|
|
|
490
561
|
this.turnResolve = null;
|
|
491
562
|
this.turnReject = null;
|
|
492
563
|
this.running = false;
|
|
564
|
+
this.limitRejectionSeen = false;
|
|
565
|
+
this.limitTurnRejectedForFailover = false;
|
|
566
|
+
this.lastLimitInfo = null;
|
|
567
|
+
this.deferredTurnError = null;
|
|
493
568
|
}
|
|
494
569
|
}
|
|
495
570
|
}
|
|
@@ -803,18 +878,60 @@ var ClaudeSdkDriver = class extends AgentDriver {
|
|
|
803
878
|
this.running = false;
|
|
804
879
|
return true;
|
|
805
880
|
}
|
|
881
|
+
this.emitDeferredTurnError({ type: "error", error: err.message, detail: err.detail });
|
|
882
|
+
return false;
|
|
883
|
+
}
|
|
884
|
+
/**
|
|
885
|
+
* Side-effects of the seat-failover path: ask the host to re-resolve the AI
|
|
886
|
+
* provider config behind this session. On `switched: true` drop the query and
|
|
887
|
+
* return `true` (the caller recurses onto a fresh CLI); otherwise emit the
|
|
888
|
+
* deferred error and return `false`. Mirrors {@link attemptAuthSelfHeal} —
|
|
889
|
+
* this performs the state mutation, the caller owns the control flow.
|
|
890
|
+
*
|
|
891
|
+
* Dropping `this.query` is the whole point of the retry, not bookkeeping: a
|
|
892
|
+
* resident CLI caches "blocked until <reset>" and short-circuits the next
|
|
893
|
+
* prompt locally with no API attempt at all (skaile-ai/workspaces#566), so a
|
|
894
|
+
* rewritten credentials file alone would change nothing. Only a fresh spawn
|
|
895
|
+
* clears it. `sessionId` / `hasSession` are preserved so the new query
|
|
896
|
+
* resumes the same conversation.
|
|
897
|
+
*/
|
|
898
|
+
async attemptLimitFailover(err) {
|
|
899
|
+
this.log.warn("turn blocked by an upstream usage limit; invoking onLimitBlocked", {
|
|
900
|
+
configId: this.config.aiProviderConfigId,
|
|
901
|
+
limitType: this.lastLimitInfo?.limitType,
|
|
902
|
+
resetsAt: this.lastLimitInfo?.resetsAt
|
|
903
|
+
});
|
|
904
|
+
let outcome;
|
|
806
905
|
try {
|
|
807
|
-
this.
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
detail: err.detail
|
|
906
|
+
outcome = await this.config.onLimitBlocked({
|
|
907
|
+
configId: this.config.aiProviderConfigId ?? "",
|
|
908
|
+
...this.lastLimitInfo ?? {}
|
|
811
909
|
});
|
|
812
|
-
} catch (
|
|
813
|
-
this.log.warn(
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
910
|
+
} catch (failoverErr) {
|
|
911
|
+
this.log.warn("onLimitBlocked callback threw; surfacing the original limit error", {
|
|
912
|
+
error: failoverErr instanceof Error ? failoverErr.message : String(failoverErr)
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
if (outcome?.switched === true) {
|
|
916
|
+
this.log.info("switched AI provider config; respawning the CLI and replaying the prompt", {
|
|
917
|
+
configId: outcome.configId
|
|
918
|
+
});
|
|
919
|
+
this.query = null;
|
|
920
|
+
this.endInputStream();
|
|
921
|
+
this.turnResolve = null;
|
|
922
|
+
this.turnReject = null;
|
|
923
|
+
this.running = false;
|
|
924
|
+
return true;
|
|
817
925
|
}
|
|
926
|
+
this.log.info("no alternative AI provider config available; surfacing the limit error", {
|
|
927
|
+
reason: outcome?.reason
|
|
928
|
+
});
|
|
929
|
+
const detail = err?.detail;
|
|
930
|
+
this.emitDeferredTurnError({
|
|
931
|
+
type: "error",
|
|
932
|
+
error: err instanceof Error ? err.message : String(err),
|
|
933
|
+
...detail ? { detail } : {}
|
|
934
|
+
});
|
|
818
935
|
return false;
|
|
819
936
|
}
|
|
820
937
|
/**
|
|
@@ -1261,6 +1378,12 @@ var ClaudeSdkDriver = class extends AgentDriver {
|
|
|
1261
1378
|
category: detail.category,
|
|
1262
1379
|
retryable: detail.retryable
|
|
1263
1380
|
});
|
|
1381
|
+
if (detail.category === "auth") {
|
|
1382
|
+
this.reportProviderResponse({ status: 401 });
|
|
1383
|
+
} else if (isSeatCapacityFailure(detail)) {
|
|
1384
|
+
this.limitRejectionSeen = true;
|
|
1385
|
+
this.reportProviderResponse({ status: 429, ...this.lastLimitInfo ?? {} });
|
|
1386
|
+
}
|
|
1264
1387
|
const surfaced = detail.category === "auth" ? new AuthError(detail) : detail.category === "account" ? new AccountUnavailableError(detail) : error;
|
|
1265
1388
|
this.failTurn(surfaced, detail);
|
|
1266
1389
|
}
|
|
@@ -1281,10 +1404,14 @@ var ClaudeSdkDriver = class extends AgentDriver {
|
|
|
1281
1404
|
this.turnResolve = null;
|
|
1282
1405
|
this.turnReject = null;
|
|
1283
1406
|
}
|
|
1407
|
+
this.emitAgentEnd();
|
|
1408
|
+
}
|
|
1409
|
+
/** Emit the turn-end signal, swallowing a throwing listener. */
|
|
1410
|
+
emitAgentEnd() {
|
|
1284
1411
|
try {
|
|
1285
1412
|
this.emit("agent-event", { type: "agent_end" });
|
|
1286
1413
|
} catch (emitErr) {
|
|
1287
|
-
this.log.warn("agent-event listener threw
|
|
1414
|
+
this.log.warn("agent-event listener threw while ending the turn; suppressing", {
|
|
1288
1415
|
error: emitErr instanceof Error ? emitErr.message : String(emitErr)
|
|
1289
1416
|
});
|
|
1290
1417
|
}
|
|
@@ -1344,29 +1471,62 @@ var ClaudeSdkDriver = class extends AgentDriver {
|
|
|
1344
1471
|
*/
|
|
1345
1472
|
failTurn(err, detail) {
|
|
1346
1473
|
this.disarmTurnStallWatchdog();
|
|
1474
|
+
const hadPendingTurn = this.turnReject != null;
|
|
1347
1475
|
if (this.turnReject) {
|
|
1348
1476
|
this.turnReject(err);
|
|
1349
1477
|
this.turnResolve = null;
|
|
1350
1478
|
this.turnReject = null;
|
|
1351
1479
|
}
|
|
1352
1480
|
this.turnCompleted = true;
|
|
1481
|
+
const event = {
|
|
1482
|
+
type: "error",
|
|
1483
|
+
error: err.message,
|
|
1484
|
+
...detail ? { detail } : {}
|
|
1485
|
+
};
|
|
1353
1486
|
const willSelfHeal = err instanceof AuthError && !this.authSelfHealUsed && this.config.onAuthError != null;
|
|
1354
|
-
if (willSelfHeal) {
|
|
1487
|
+
if (hadPendingTurn && willSelfHeal) {
|
|
1355
1488
|
this.log.info("deferring auth-error event; self-heal will attempt refresh");
|
|
1489
|
+
this.deferredTurnError = event;
|
|
1356
1490
|
return;
|
|
1357
1491
|
}
|
|
1492
|
+
if (hadPendingTurn && this.willFailoverOnLimit()) {
|
|
1493
|
+
this.log.info("deferring limit error event; seat failover will be attempted");
|
|
1494
|
+
this.deferredTurnError = event;
|
|
1495
|
+
return;
|
|
1496
|
+
}
|
|
1497
|
+
this.emitTurnErrorEvent(event);
|
|
1498
|
+
}
|
|
1499
|
+
/**
|
|
1500
|
+
* Whether a usage-limit block on the current attempt still has a seat
|
|
1501
|
+
* failover available. Evaluated identically by {@link failTurn} (to decide
|
|
1502
|
+
* whether to withhold the error event) and by {@link handleResultErrorPayload}
|
|
1503
|
+
* (to decide whether to reject the turn at all) — one predicate so the two
|
|
1504
|
+
* can never disagree and strand a deferred event.
|
|
1505
|
+
*/
|
|
1506
|
+
willFailoverOnLimit() {
|
|
1507
|
+
return this.limitRejectionSeen && !this.limitFailoverUsed && this.config.onLimitBlocked != null;
|
|
1508
|
+
}
|
|
1509
|
+
/** Emit one `agent-event: error`, swallowing a throwing listener. */
|
|
1510
|
+
emitTurnErrorEvent(event) {
|
|
1358
1511
|
try {
|
|
1359
|
-
this.emit("agent-event",
|
|
1360
|
-
type: "error",
|
|
1361
|
-
error: err.message,
|
|
1362
|
-
...detail ? { detail } : {}
|
|
1363
|
-
});
|
|
1512
|
+
this.emit("agent-event", event);
|
|
1364
1513
|
} catch (emitErr) {
|
|
1365
|
-
this.log.warn("agent-event listener threw
|
|
1514
|
+
this.log.warn("agent-event listener threw while surfacing a turn error; suppressing", {
|
|
1366
1515
|
error: emitErr instanceof Error ? emitErr.message : String(emitErr)
|
|
1367
1516
|
});
|
|
1368
1517
|
}
|
|
1369
1518
|
}
|
|
1519
|
+
/**
|
|
1520
|
+
* Emit the error {@link failTurn} withheld, exactly once. Called by a
|
|
1521
|
+
* recovery path that decided NOT to retry, so every failure produces one
|
|
1522
|
+
* downstream error event and a recovered one produces none. `fallback` covers
|
|
1523
|
+
* the paths that reject without going through `failTurn`.
|
|
1524
|
+
*/
|
|
1525
|
+
emitDeferredTurnError(fallback) {
|
|
1526
|
+
const event = this.deferredTurnError ?? fallback;
|
|
1527
|
+
this.deferredTurnError = null;
|
|
1528
|
+
this.emitTurnErrorEvent(event);
|
|
1529
|
+
}
|
|
1370
1530
|
// ---------------------------------------------------------------------------
|
|
1371
1531
|
// Message handling
|
|
1372
1532
|
// ---------------------------------------------------------------------------
|
|
@@ -1383,9 +1543,17 @@ var ClaudeSdkDriver = class extends AgentDriver {
|
|
|
1383
1543
|
}
|
|
1384
1544
|
}
|
|
1385
1545
|
/**
|
|
1386
|
-
* Surface subscription rate-limit telemetry as a structured warning log
|
|
1546
|
+
* Surface subscription rate-limit telemetry as a structured warning log, and
|
|
1547
|
+
* — on `status: "rejected"` — arm the seat failover and report the rejection
|
|
1548
|
+
* to the host for per-seat health attribution.
|
|
1549
|
+
*
|
|
1387
1550
|
* Returns `true` when the message was a rate-limit event (and fully handled),
|
|
1388
1551
|
* so the caller can stop dispatching.
|
|
1552
|
+
*
|
|
1553
|
+
* This used to only log. That is what left a session pinned to a parked seat:
|
|
1554
|
+
* the rejection was fully handled here and nothing propagated, so the
|
|
1555
|
+
* platform's re-resolution — which was ready and working — was reachable only
|
|
1556
|
+
* through a token refresh that happened to be due (skaile-ai/platform#2976).
|
|
1389
1557
|
*/
|
|
1390
1558
|
handleRateLimitEvent(msg) {
|
|
1391
1559
|
const maybeRateLimit = msg;
|
|
@@ -1397,10 +1565,39 @@ var ClaudeSdkDriver = class extends AgentDriver {
|
|
|
1397
1565
|
utilization: info.utilization,
|
|
1398
1566
|
resetsAt: info.resetsAt
|
|
1399
1567
|
});
|
|
1568
|
+
if (info.status === "rejected") {
|
|
1569
|
+
this.limitRejectionSeen = true;
|
|
1570
|
+
this.lastLimitInfo = {
|
|
1571
|
+
...info.rateLimitType ? { limitType: info.rateLimitType } : {},
|
|
1572
|
+
...typeof info.resetsAt === "number" ? { resetsAt: info.resetsAt } : {},
|
|
1573
|
+
...typeof info.utilization === "number" ? { utilization: info.utilization } : {}
|
|
1574
|
+
};
|
|
1575
|
+
this.reportProviderResponse({ status: 429, ...this.lastLimitInfo });
|
|
1576
|
+
}
|
|
1400
1577
|
return true;
|
|
1401
1578
|
}
|
|
1402
1579
|
return false;
|
|
1403
1580
|
}
|
|
1581
|
+
/**
|
|
1582
|
+
* Report an observed 401 / 429 to the host so the owning AI provider config's
|
|
1583
|
+
* live health counters can be bumped (skaile-ai/workspaces#368). Strictly
|
|
1584
|
+
* fire-and-forget: a throwing host callback must never fail the turn that
|
|
1585
|
+
* produced the observation.
|
|
1586
|
+
*/
|
|
1587
|
+
reportProviderResponse(observation) {
|
|
1588
|
+
if (!this.config.onProviderResponse || this.providerResponseReported) return;
|
|
1589
|
+
this.providerResponseReported = true;
|
|
1590
|
+
try {
|
|
1591
|
+
this.config.onProviderResponse({
|
|
1592
|
+
...observation,
|
|
1593
|
+
...this.config.aiProviderConfigId ? { configId: this.config.aiProviderConfigId } : {}
|
|
1594
|
+
});
|
|
1595
|
+
} catch (err) {
|
|
1596
|
+
this.log.warn("onProviderResponse threw; suppressing", {
|
|
1597
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1598
|
+
});
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1404
1601
|
handleAssistantMessage(msg) {
|
|
1405
1602
|
this.captureAssistantContext(msg);
|
|
1406
1603
|
const agentMsg = this.mapAssistantMessage(msg);
|
|
@@ -1594,11 +1791,20 @@ var ClaudeSdkDriver = class extends AgentDriver {
|
|
|
1594
1791
|
category: detail.category,
|
|
1595
1792
|
retryable: detail.retryable
|
|
1596
1793
|
});
|
|
1794
|
+
if (detail.category === "auth") {
|
|
1795
|
+
this.reportProviderResponse({ status: 401 });
|
|
1796
|
+
} else if (isSeatCapacityFailure(detail, msg.subtype)) {
|
|
1797
|
+
this.limitRejectionSeen = true;
|
|
1798
|
+
this.reportProviderResponse({ status: 429, ...this.lastLimitInfo ?? {} });
|
|
1799
|
+
}
|
|
1597
1800
|
const errorText = errors.join("; ") || `Agent stopped: ${msg.subtype}`;
|
|
1598
1801
|
if (detail.category === "auth") {
|
|
1599
1802
|
this.failTurn(new AuthError({ ...detail, message: errorText }), detail);
|
|
1600
1803
|
} else if (detail.category === "account") {
|
|
1601
1804
|
this.failTurn(new AccountUnavailableError({ ...detail, message: errorText }), detail);
|
|
1805
|
+
} else if (this.willFailoverOnLimit()) {
|
|
1806
|
+
this.limitTurnRejectedForFailover = true;
|
|
1807
|
+
this.failTurn(new Error(errorText), detail);
|
|
1602
1808
|
} else if (errors.length > 0) {
|
|
1603
1809
|
this.emit("agent-event", {
|
|
1604
1810
|
type: "error",
|