elanous 0.2.7 → 0.2.9
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/apps/pwa/out/404/index.html +1 -1
- package/apps/pwa/out/404.html +1 -1
- package/apps/pwa/out/_next/static/bFI5EEwg7T6aG88ibX4Zq/_buildManifest.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7032-f4ef698518fba61c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8067-d976b441a104ab74.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8519-5632377cce994f86.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8630-cdfc61e82c653ffc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8894-e263b332208e3957.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9454-139040d37c79dbc9.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9740-8f5ff96c522b5f55.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-fb7c2d458f32fcad.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-35e2713597dd24d1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/board/page-918b656f064577ba.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/botlab/{page-624f7a2f89b60c8f.js → page-f972fc1c00a62e88.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/{page-5a5fb385a4df0ffe.js → page-5f30e00693527ebd.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/chat/{page-80497e93a878ddc5.js → page-7fbae173b3c103a7.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/{page-7457f52683562161.js → page-3b4edcce364062ea.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/control/page-c5794f484c251f44.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-1130d8eefcd87e76.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/editor/page-973996d584f008c1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/exec/{page-b7dcf5d48a854b76.js → page-016e2bda04eecb1f.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/field/{page-51480757e7d68b92.js → page-b4d6d9a3b56f2940.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/intake/page-ec49a466121233ba.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/layout-f72db356744ceade.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/live/page-a6fd9b50d1b85378.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/page-8b0402a46a749a3d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-07267f1391b96aba.js → page-037f83e2a1edc8e2.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/morning/{page-55d50e9b0006a91d.js → page-adfbd2ba9598e59a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-46fedff57d4bec67.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-0cf318e9dcee2bbf.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-899b327cb8b6845c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-6c1e6ba743a29dfe.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/{page-a7e37af7d0399cb8.js → page-1a8e4807292f6b17.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-28deba6bcb834916.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-de68e2205794037d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/sessions/{page-15e5a15db84c56aa.js → page-c71c33a573230672.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-a2e572e4511f2a16.js → page-0df30e0c7e50148a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/page-4fc56790fe0d2682.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-59df2aebb38a7c1e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/page-4647be5dbcbd0795.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/share/page-e025892bda74b743.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-a0955de73b89f078.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/tasks/{page-c1c95739c0bff3b8.js → page-369846d2921af584.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-72cbc1c8cdd0f65d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/trace/{page-71d18eec33b9c667.js → page-77b28bfb0996d5ea.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-3220c4934f49c6c2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/{page-2c4e59079ee357ac.js → page-90670589b87abea8.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-7df9243ed021a4cc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workspace/{page-5d422f38c052967e.js → page-4498c3853f4f688d.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-35bedde6d885f08f.js +1 -0
- package/apps/pwa/out/_next/static/css/f92f63a5df0ba229.css +1 -0
- package/apps/pwa/out/approvals/index.html +1 -1
- package/apps/pwa/out/approvals/index.txt +30 -29
- package/apps/pwa/out/autopilot/index.html +1 -1
- package/apps/pwa/out/autopilot/index.txt +30 -29
- package/apps/pwa/out/board/index.html +1 -1
- package/apps/pwa/out/board/index.txt +30 -29
- package/apps/pwa/out/botlab/index.html +1 -1
- package/apps/pwa/out/botlab/index.txt +27 -26
- package/apps/pwa/out/bots/index.html +1 -1
- package/apps/pwa/out/bots/index.txt +27 -26
- package/apps/pwa/out/chat/index.html +1 -1
- package/apps/pwa/out/chat/index.txt +27 -26
- package/apps/pwa/out/consult/index.html +1 -1
- package/apps/pwa/out/consult/index.txt +30 -29
- package/apps/pwa/out/control/index.html +1 -1
- package/apps/pwa/out/control/index.txt +30 -29
- package/apps/pwa/out/design-check/index.html +1 -1
- package/apps/pwa/out/design-check/index.txt +30 -29
- package/apps/pwa/out/editor/index.html +1 -1
- package/apps/pwa/out/editor/index.txt +30 -29
- package/apps/pwa/out/exec/index.html +1 -1
- package/apps/pwa/out/exec/index.txt +30 -29
- package/apps/pwa/out/field/index.html +1 -1
- package/apps/pwa/out/field/index.txt +30 -29
- package/apps/pwa/out/healthz +1 -1
- package/apps/pwa/out/index.html +1 -1
- package/apps/pwa/out/index.txt +27 -26
- package/apps/pwa/out/intake/index.html +1 -1
- package/apps/pwa/out/intake/index.txt +30 -29
- package/apps/pwa/out/live/index.html +1 -1
- package/apps/pwa/out/live/index.txt +30 -29
- package/apps/pwa/out/market/index.html +1 -1
- package/apps/pwa/out/market/index.txt +27 -26
- package/apps/pwa/out/missions/placeholder/index.html +1 -1
- package/apps/pwa/out/missions/placeholder/index.txt +27 -26
- package/apps/pwa/out/morning/index.html +1 -1
- package/apps/pwa/out/morning/index.txt +30 -29
- package/apps/pwa/out/observatory/index.html +1 -1
- package/apps/pwa/out/observatory/index.txt +30 -29
- package/apps/pwa/out/ops/checklist/index.html +1 -0
- package/apps/pwa/out/ops/checklist/index.txt +30 -0
- package/apps/pwa/out/ops/release/index.html +1 -0
- package/apps/pwa/out/ops/release/index.txt +30 -0
- package/apps/pwa/out/reflection/index.html +1 -1
- package/apps/pwa/out/reflection/index.txt +28 -27
- package/apps/pwa/out/scheduler/index.html +1 -1
- package/apps/pwa/out/scheduler/index.txt +30 -29
- package/apps/pwa/out/sessions/index.html +1 -1
- package/apps/pwa/out/sessions/index.txt +27 -26
- package/apps/pwa/out/settings/devices/index.html +1 -1
- package/apps/pwa/out/settings/devices/index.txt +30 -29
- package/apps/pwa/out/settings/index.html +1 -1
- package/apps/pwa/out/settings/index.txt +30 -29
- package/apps/pwa/out/setup/done/index.html +1 -1
- package/apps/pwa/out/setup/done/index.txt +30 -29
- package/apps/pwa/out/setup/index.html +1 -1
- package/apps/pwa/out/setup/index.txt +30 -29
- package/apps/pwa/out/share/index.html +1 -1
- package/apps/pwa/out/share/index.txt +30 -29
- package/apps/pwa/out/showroom/index.html +1 -1
- package/apps/pwa/out/showroom/index.txt +27 -26
- package/apps/pwa/out/tasks/index.html +1 -1
- package/apps/pwa/out/tasks/index.txt +30 -29
- package/apps/pwa/out/term/index.html +1 -1
- package/apps/pwa/out/term/index.txt +30 -29
- package/apps/pwa/out/trace/index.html +1 -1
- package/apps/pwa/out/trace/index.txt +30 -29
- package/apps/pwa/out/vault/index.html +1 -1
- package/apps/pwa/out/vault/index.txt +30 -29
- package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
- package/apps/pwa/out/workflows/chat-ui/index.txt +30 -29
- package/apps/pwa/out/workflows/index.html +1 -1
- package/apps/pwa/out/workflows/index.txt +30 -29
- package/apps/pwa/out/workspace/index.html +1 -1
- package/apps/pwa/out/workspace/index.txt +30 -29
- package/apps/pwa/out/worktrees/index.html +1 -1
- package/apps/pwa/out/worktrees/index.txt +30 -29
- package/graphs/release/recipes.yaml +3 -0
- package/graphs/release/release-loop.yaml +3 -1
- package/graphs/test-diet/nightly-audit.yaml +18 -0
- package/graphs/test-diet/recipes.yaml +12 -0
- package/graphs/test-diet/test-diet.yaml +22 -0
- package/package.json +2 -1
- package/persona-presets/_index.yaml +10 -0
- package/persona-presets/_schema.json +146 -0
- package/persona-presets/dana.yaml +26 -0
- package/persona-presets/haru.yaml +27 -0
- package/persona-presets/kai.yaml +27 -0
- package/persona-presets/leo.yaml +27 -0
- package/persona-presets/mira.yaml +30 -0
- package/persona-presets/nari.yaml +27 -0
- package/persona-presets/sora.yaml +27 -0
- package/persona-presets/yuna.yaml +27 -0
- package/plugins/doc-draft/graphs/run-step.ts +1 -1
- package/plugins/geo-check/graphs/run-step.ts +34 -10
- package/scripts/field-feed/deliver-node.ts +8 -3
- package/scripts/field-feed/lib.ts +2 -0
- package/scripts/install.sh +20 -11
- package/scripts/persona-presets/lint.ts +79 -0
- package/scripts/release-loop/checklist-gate-node.ts +2 -2
- package/scripts/release-loop/cutoff.ts +20 -2
- package/scripts/release-loop/export-check-node.ts +85 -0
- package/scripts/release-loop/gate-node.ts +45 -7
- package/scripts/release-loop/node-verdict.ts +8 -1
- package/scripts/release-loop/npm-publish-node.ts +10 -4
- package/scripts/release-loop/ops-upgrade-node.ts +11 -3
- package/scripts/release-loop/publish-node.ts +23 -1
- package/scripts/release-loop/shard-plan.ts +17 -0
- package/scripts/storyboard/storyboard.ts +167 -0
- package/scripts/test-diet/lib.ts +89 -0
- package/scripts/test-diet/node.ts +144 -0
- package/src/acp/auth-methods.ts +30 -0
- package/src/acp/server.ts +5 -5
- package/src/auth/setup-link-tokens.fixture.ts +19 -0
- package/src/auth/setup-link-tokens.ts +153 -0
- package/src/budget/unified-usage.ts +75 -0
- package/src/channel-bot-token.ts +37 -10
- package/src/channel-import/detect.ts +191 -0
- package/src/channel-import/run.ts +111 -0
- package/src/chat/index.ts +9 -9
- package/src/cli/config-test-sync.ts +4 -0
- package/src/cli/decisions-cli.ts +13 -3
- package/src/cli/doctor-bundle.ts +128 -0
- package/src/cli/doctor-cli.ts +36 -1
- package/src/cli/doctor-local-llm.ts +95 -0
- package/src/cli/import-cli.ts +60 -0
- package/src/cli/mcp-login.ts +13 -4
- package/src/cli/op-proxy-sync.ts +83 -0
- package/src/cli/persona-cli.ts +116 -0
- package/src/cli/pr-cli.ts +6 -2
- package/src/cli/release-cli.ts +49 -8
- package/src/cli/skills-cli.ts +173 -0
- package/src/cli/start-cli.ts +13 -4
- package/src/codex/setup.ts +15 -13
- package/src/dashboard/dashboard-status-lines.ts +45 -0
- package/src/dashboard/first-screen-band.ts +35 -0
- package/src/dashboard/index.ts +87 -14
- package/src/dashboard/provider-slash-runtime.ts +28 -11
- package/src/dashboard/slash-runtime/dashboard-handlers.ts +12 -2
- package/src/dashboard/slash-runtime/help-from-registry.ts +4 -2
- package/src/dashboard/turn-stream-formatter.ts +1 -1
- package/src/decisions/decision-cards.ts +291 -0
- package/src/decisions/decision-ledger.ts +5 -2
- package/src/decisions/discord-decision-cards.ts +133 -0
- package/src/decisions/telegram-decision-cards.ts +100 -0
- package/src/directives/version-at.ts +77 -0
- package/src/discord.ts +17 -0
- package/src/domains/outbound-alert.ts +44 -1
- package/src/domains/repeated-failure.ts +1 -1
- package/src/error-report/report.ts +200 -0
- package/src/exec-requests/planner.ts +8 -2
- package/src/exec-requests/runner.ts +64 -24
- package/src/exec-requests/store.ts +2 -0
- package/src/field/field-feed-auto.ts +48 -0
- package/src/field/field-reel.ts +8 -1
- package/src/hooks/error-report.ts +210 -0
- package/src/hooks/receiver.ts +41 -1
- package/src/import/apply.ts +128 -0
- package/src/import/plan.ts +197 -0
- package/src/index.ts +98 -11
- package/src/input-core/text-entry.ts +13 -5
- package/src/intake-plane/seat-answer.ts +96 -0
- package/src/intake-plane/seat-doc-route.ts +17 -3
- package/src/maturity/feature-maturity.ts +2 -0
- package/src/nexus/api/consult-requests.ts +19 -8
- package/src/nexus/api/error-reports.ts +108 -0
- package/src/nexus/api/http-server.ts +41 -1
- package/src/nexus/api/meta-api.ts +27 -0
- package/src/nexus/api/operator.ts +61 -0
- package/src/nexus/api/ops-api.ts +139 -0
- package/src/nexus/api/ops-seats-sources.ts +65 -0
- package/src/nexus/api/ops-seats.ts +114 -0
- package/src/nexus/api/outbound-report.ts +7 -0
- package/src/nexus/api/public-routes.ts +14 -1
- package/src/nexus/api/running-runs-background.ts +68 -0
- package/src/nexus/api/running-runs-child.ts +6 -0
- package/src/nexus/api/setup-claim.ts +21 -0
- package/src/nexus/api/share-captures.ts +91 -0
- package/src/nexus/api/skill-problems.ts +56 -0
- package/src/nexus/api/terminals.ts +24 -8
- package/src/nexus/config/secrets/file-backend.ts +56 -18
- package/src/nexus/index.ts +25 -1
- package/src/nexus/setup-status.ts +7 -2
- package/src/oauth/browser-availability.ts +14 -0
- package/src/oauth/codex-quota-policy.ts +16 -0
- package/src/onboarding/continue-here.ts +13 -8
- package/src/onboarding/first-run-doctor.ts +54 -0
- package/src/onboarding/web-first.ts +8 -1
- package/src/onboarding.ts +66 -12
- package/src/persona/presets.ts +105 -0
- package/src/release-loop/checklist.ts +33 -69
- package/src/release-loop/feature-store.ts +327 -0
- package/src/release-loop/manifest.ts +59 -5
- package/src/seat-dispatch/ceo-commands.ts +65 -0
- package/src/self-dev/orchestrate.ts +18 -11
- package/src/shell-env-bootstrap.ts +44 -7
- package/src/skills/connect-claude-plugin.ts +142 -0
- package/src/skills/connect.ts +70 -0
- package/src/skills/index.ts +29 -1
- package/src/skills/repair.ts +124 -0
- package/src/skills/runner.ts +11 -2
- package/src/skills/tools/usage-slash.ts +8 -7
- package/src/skills/trigger-extract.ts +1 -1
- package/src/task-orchestrator/surfaces/pod-command-job.ts +16 -8
- package/src/telegram-commands.ts +29 -0
- package/src/telegram-run.ts +83 -13
- package/src/telegram.ts +34 -0
- package/src/thinking-line.ts +28 -20
- package/src/tui/chat/execution-badge.ts +1 -1
- package/src/user-config.ts +111 -13
- package/src/ux-sim/turn-states.ts +1 -1
- package/src/version/packed-revision.json +1 -1
- package/apps/pwa/out/_next/static/chunks/4836-2eba6079f31b88d4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8067-f74bf14d67ad98f1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8519-f6b5d8a5833a7603.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8894-e92491a164192cc1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9454-a3e0953307216c58.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9740-67051141115ce775.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9804-64c02c3c127d5014.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-d5ebde04844689e6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-10ed24f413ed66e9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-211810b6c93ab66a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/control/page-cee86394de407aa6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-4ff8edab44196b8c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/page-92e4f44428d5e290.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/intake/page-2a05dfe80c8bdf16.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-d9979633aa82f8e6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-30533117beb53625.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/market/page-f7857826c2f97ad7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-9699b1786be6171d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-43dd772aee8af75f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-973ab7af59c718b7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-a9f8d3865b91e436.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/page-dd107bf1c7d52a90.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-d42b99cdfb9f27c6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/page-2764ff40dad5a995.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/share/page-c5f1152db116ff96.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-f933bca81ea38a6f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-a7aaffcf771662d8.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-56adbe1a1f1379bd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-a81f113f3ef89d72.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-fc27b00e20733146.js +0 -1
- package/apps/pwa/out/_next/static/css/f537bdb19c69306a.css +0 -1
- package/apps/pwa/out/_next/static/keajW7DDyghEs1y8oJe6T/_buildManifest.js +0 -1
- /package/apps/pwa/out/_next/static/{keajW7DDyghEs1y8oJe6T → bFI5EEwg7T6aG88ibX4Zq}/_ssgManifest.js +0 -0
|
@@ -987,6 +987,23 @@ async function deleteLabelledJobs(runId: string, selectedTargets: readonly { con
|
|
|
987
987
|
return counts.reduce((sum, count) => sum + count, 0);
|
|
988
988
|
}
|
|
989
989
|
|
|
990
|
+
/** `self-dev-runs/<runId>/pid.json` — what `harness stop <runId>` reads (start time guards PID reuse).
|
|
991
|
+
* HS1: written as soon as the run ID exists, not only when the jobs start — pod planning and image sync can
|
|
992
|
+
* take minutes before that, and a stop in that window found «no process» (10-01 · three runs). */
|
|
993
|
+
export function writeRunPidRecord(runId: string, runsDir: string = selfDevRunsDir()): string {
|
|
994
|
+
const pidPath = join(runsDir, runId, 'pid.json');
|
|
995
|
+
mkdirSync(join(runsDir, runId), { recursive: true });
|
|
996
|
+
const temporary = `${pidPath}.${process.pid}.${randomUUID()}.tmp`;
|
|
997
|
+
const fd = openSync(temporary, 'wx', 0o600);
|
|
998
|
+
try {
|
|
999
|
+
try {
|
|
1000
|
+
writeFileSync(fd, JSON.stringify({ pid: process.pid, startedAt: Date.now() - Math.floor(process.uptime() * 1000), argv0: process.argv0 }));
|
|
1001
|
+
} finally { closeSync(fd); }
|
|
1002
|
+
renameSync(temporary, pidPath);
|
|
1003
|
+
} catch (error) { unlinkSync(temporary); throw error; }
|
|
1004
|
+
return pidPath;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
990
1007
|
/**
|
|
991
1008
|
* Run N self-dev jobs in parallel under the TOX dispatcher and resolve
|
|
992
1009
|
* once every job reaches a terminal status.
|
|
@@ -1419,17 +1436,7 @@ export function orchestrateSelfDev(opts: OrchestrateSelfDevOptions): Promise<Sel
|
|
|
1419
1436
|
signals.on('SIGTERM', onTerm);
|
|
1420
1437
|
signals.on('SIGINT', onInt);
|
|
1421
1438
|
try {
|
|
1422
|
-
if (pidPath)
|
|
1423
|
-
mkdirSync(join(opts.runsDir ?? selfDevRunsDir(), runId!), { recursive: true });
|
|
1424
|
-
const temporary = `${pidPath}.${process.pid}.${randomUUID()}.tmp`;
|
|
1425
|
-
const fd = openSync(temporary, 'wx', 0o600);
|
|
1426
|
-
try {
|
|
1427
|
-
try {
|
|
1428
|
-
writeFileSync(fd, JSON.stringify({ pid: process.pid, startedAt: Date.now() - Math.floor(process.uptime() * 1000), argv0: process.argv0 }));
|
|
1429
|
-
} finally { closeSync(fd); }
|
|
1430
|
-
renameSync(temporary, pidPath);
|
|
1431
|
-
} catch (error) { unlinkSync(temporary); throw error; }
|
|
1432
|
-
}
|
|
1439
|
+
if (pidPath) writeRunPidRecord(runId!, opts.runsDir ?? selfDevRunsDir());
|
|
1433
1440
|
} catch (error) { cleanup(); reject(error); return; }
|
|
1434
1441
|
|
|
1435
1442
|
// Current full result set: resumed-done goals + each running task's
|
|
@@ -88,7 +88,7 @@ export function parsePrintenv(raw: string): Record<string, string> {
|
|
|
88
88
|
* - HOME, USER, LOGNAME, SHELL, TERM
|
|
89
89
|
* - LANG / LC_* — locale matters for some prompts (starship, powerlevel)
|
|
90
90
|
* Everything else is left for the login shell itself to define. */
|
|
91
|
-
|
|
91
|
+
function captureSeed(): { shell: string; seedEnv: NodeJS.ProcessEnv } | null {
|
|
92
92
|
const shell = process.env.SHELL;
|
|
93
93
|
if (!shell) return null;
|
|
94
94
|
const base = basename(shell);
|
|
@@ -103,6 +103,22 @@ export function runPrintenvCapture(): Record<string, string> | null {
|
|
|
103
103
|
for (const [k, v] of Object.entries(process.env)) {
|
|
104
104
|
if (k.startsWith('LC_') && typeof v === 'string') seedEnv[k] = v;
|
|
105
105
|
}
|
|
106
|
+
return { shell, seedEnv };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** printenv output → env, or null when it does not look like a real login env. */
|
|
110
|
+
function acceptCapture(stdout: string): Record<string, string> | null {
|
|
111
|
+
if (!stdout) return null;
|
|
112
|
+
const parsed = parsePrintenv(stdout);
|
|
113
|
+
// Sanity: require PATH and at least 5 keys, else something is off.
|
|
114
|
+
if (!parsed.PATH || Object.keys(parsed).length < 5) return null;
|
|
115
|
+
return parsed;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function runPrintenvCapture(): Record<string, string> | null {
|
|
119
|
+
const seed = captureSeed();
|
|
120
|
+
if (!seed) return null;
|
|
121
|
+
const { shell, seedEnv } = seed;
|
|
106
122
|
|
|
107
123
|
let res: ReturnType<typeof spawnSync>;
|
|
108
124
|
try {
|
|
@@ -119,12 +135,33 @@ export function runPrintenvCapture(): Record<string, string> | null {
|
|
|
119
135
|
}
|
|
120
136
|
if (res.status !== 0 && res.status !== null) return null;
|
|
121
137
|
if (res.signal) return null;
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
138
|
+
return acceptCapture(typeof res.stdout === 'string' ? res.stdout : '');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Fill the login-env cache off the event loop (daemon boot). The first web-terminal spawn otherwise ran the
|
|
142
|
+
* `-l -i` printenv with spawnSync and froze the whole daemon for ~3 s (10-02 TERM1). A no-op once captured;
|
|
143
|
+
* a failed warm leaves the cache empty so `getCapturedEnv()` still tries (and falls back) as before. */
|
|
144
|
+
export async function warmCapturedEnv(spawn: typeof Bun.spawn = Bun.spawn): Promise<boolean> {
|
|
145
|
+
if (captured) return cached !== null;
|
|
146
|
+
if (process.env.ELANOUS_SKIP_LOGIN_ENV === '1' || process.env.TERM_PROGRAM === 'monad-agent-nested') return false;
|
|
147
|
+
const seed = captureSeed();
|
|
148
|
+
if (!seed) return false;
|
|
149
|
+
const start = Date.now();
|
|
150
|
+
try {
|
|
151
|
+
const proc = spawn([seed.shell, '-l', '-i', '-c', 'printenv'], { env: seed.seedEnv as Record<string, string>, stdin: 'ignore', stdout: 'pipe', stderr: 'ignore' });
|
|
152
|
+
const timer = setTimeout(() => proc.kill(), TIMEOUT_MS);
|
|
153
|
+
const [out, code] = await Promise.all([new Response(proc.stdout as ReadableStream).text(), proc.exited]);
|
|
154
|
+
clearTimeout(timer);
|
|
155
|
+
const parsed = code === 0 ? acceptCapture(out) : null;
|
|
156
|
+
if (captured) return cached !== null; // a synchronous capture won the race
|
|
157
|
+
if (!parsed) return false;
|
|
158
|
+
cached = parsed;
|
|
159
|
+
captured = true;
|
|
160
|
+
debug.log('shell.envbootstrap.capture', 'warmed', { ok: true, ms: Date.now() - start, keyCount: Object.keys(parsed).length });
|
|
161
|
+
return true;
|
|
162
|
+
} catch {
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
128
165
|
}
|
|
129
166
|
|
|
130
167
|
/** Return the captured login env, running the capture on first call.
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// EN8 — `elanous connect <플러그인 폴더>`: Claude Code 플러그인(`.claude-plugin/plugin.json`)을 «그대로» 쓴다.
|
|
2
|
+
// 스킬 폴더는 EN9 와 같은 칸(`skills.sources[]` · kind connected · id `plugin:<이름>`)에 등록하고(복사 없음), `.mcp.json` 의 서버는 «꺼진 채»(`enabled:false`)
|
|
3
|
+
// `mcp.servers[]` 에 넣는다 — 켜기는 사람이 확인한 뒤에 한다. 환경변수가 필요한 서버는 등록하지 않고 이유를 말한다.
|
|
4
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
5
|
+
import { join, resolve } from 'node:path';
|
|
6
|
+
import { findSkillFolders } from './connect.js';
|
|
7
|
+
|
|
8
|
+
export const CLAUDE_PLUGIN_MANIFEST = join('.claude-plugin', 'plugin.json');
|
|
9
|
+
|
|
10
|
+
export function isClaudePluginDir(dir: string): boolean {
|
|
11
|
+
return existsSync(join(dir, CLAUDE_PLUGIN_MANIFEST));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** What we write into `mcp.servers[]` (raw config shape — parsed by parseMcpServerSpec). */
|
|
15
|
+
export type PluginMcpServer =
|
|
16
|
+
| { id: string; command: string[]; enabled: false }
|
|
17
|
+
| { id: string; transport: 'http'; url: string; enabled: false };
|
|
18
|
+
|
|
19
|
+
export interface ClaudePlugin {
|
|
20
|
+
root: string;
|
|
21
|
+
name: string;
|
|
22
|
+
version: string;
|
|
23
|
+
/** Skill folders that hold at least one SKILL.md folder. */
|
|
24
|
+
skillRoots: string[];
|
|
25
|
+
skills: string[];
|
|
26
|
+
mcp: PluginMcpServer[];
|
|
27
|
+
skipped: { name: string; reason: string }[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function readJson(path: string): unknown {
|
|
31
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function asObject(value: unknown): Record<string, unknown> | null {
|
|
35
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function serverId(plugin: string, name: string): string {
|
|
39
|
+
return `${plugin}-${name}`.toLowerCase().replace(/[^a-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function expandRoot(value: string, root: string): string {
|
|
43
|
+
return value.split('${CLAUDE_PLUGIN_ROOT}').join(root);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** One `.mcp.json` entry → a server spec, or the reason it is not registered. */
|
|
47
|
+
export function convertMcpEntry(plugin: string, name: string, raw: unknown, root: string): PluginMcpServer | { skipped: string } {
|
|
48
|
+
const entry = asObject(raw);
|
|
49
|
+
if (!entry) return { skipped: '형식을 읽지 못했습니다' };
|
|
50
|
+
const env = asObject(entry.env);
|
|
51
|
+
if (env && Object.keys(env).length) return { skipped: `환경변수가 필요합니다(${Object.keys(env).join(', ')}) — 직접 등록해 주세요` };
|
|
52
|
+
const id = serverId(plugin, name);
|
|
53
|
+
const type = typeof entry.type === 'string' ? entry.type : undefined;
|
|
54
|
+
if (typeof entry.url === 'string' && entry.url) {
|
|
55
|
+
if (type && type !== 'http') return { skipped: `${type} 방식은 아직 지원하지 않습니다` };
|
|
56
|
+
if (entry.headers && Object.keys(asObject(entry.headers) ?? {}).length) return { skipped: '헤더(인증)가 필요합니다 — 직접 등록해 주세요' };
|
|
57
|
+
return { id, transport: 'http', url: entry.url, enabled: false };
|
|
58
|
+
}
|
|
59
|
+
if (typeof entry.command === 'string' && entry.command) {
|
|
60
|
+
const args = Array.isArray(entry.args) ? entry.args.filter((a): a is string => typeof a === 'string') : [];
|
|
61
|
+
return { id, command: [expandRoot(entry.command, root), ...args.map((a) => expandRoot(a, root))], enabled: false };
|
|
62
|
+
}
|
|
63
|
+
return { skipped: '실행 명령이나 주소가 없습니다' };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function mcpSource(root: string, manifest: Record<string, unknown>): Record<string, unknown> | null {
|
|
67
|
+
const declared = manifest.mcpServers;
|
|
68
|
+
let raw: unknown = null;
|
|
69
|
+
if (typeof declared === 'string') {
|
|
70
|
+
const path = resolve(root, declared);
|
|
71
|
+
if (path.startsWith(`${root}/`) && existsSync(path)) raw = readJson(path);
|
|
72
|
+
} else if (asObject(declared)) {
|
|
73
|
+
raw = declared;
|
|
74
|
+
} else if (existsSync(join(root, '.mcp.json'))) {
|
|
75
|
+
raw = readJson(join(root, '.mcp.json'));
|
|
76
|
+
}
|
|
77
|
+
const object = asObject(raw);
|
|
78
|
+
if (!object) return null;
|
|
79
|
+
return asObject(object.mcpServers) ?? object;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function skillRootsOf(root: string, manifest: Record<string, unknown>): string[] {
|
|
83
|
+
const declared = typeof manifest.skills === 'string' ? [manifest.skills]
|
|
84
|
+
: Array.isArray(manifest.skills) ? manifest.skills.filter((s): s is string => typeof s === 'string') : [];
|
|
85
|
+
const candidates = [join(root, 'skills'), ...declared.map((rel) => resolve(root, rel))]
|
|
86
|
+
.filter((path) => path === root || path.startsWith(`${root}/`));
|
|
87
|
+
return [...new Set(candidates)].filter((path) => findSkillFolders(path).length > 0);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Read a Claude Code plugin folder. Throws only when the manifest is not JSON. */
|
|
91
|
+
export function readClaudePlugin(dir: string): ClaudePlugin {
|
|
92
|
+
const root = resolve(dir);
|
|
93
|
+
const manifest = asObject(readJson(join(root, CLAUDE_PLUGIN_MANIFEST))) ?? {};
|
|
94
|
+
const name = typeof manifest.name === 'string' && manifest.name.trim() ? manifest.name.trim() : root.split('/').pop() ?? 'plugin';
|
|
95
|
+
const version = typeof manifest.version === 'string' ? manifest.version : '0.0.0';
|
|
96
|
+
const skillRoots = skillRootsOf(root, manifest);
|
|
97
|
+
const skills = skillRoots.flatMap(findSkillFolders).sort();
|
|
98
|
+
const mcp: PluginMcpServer[] = [];
|
|
99
|
+
const skipped: ClaudePlugin['skipped'] = [];
|
|
100
|
+
for (const [server, raw] of Object.entries(mcpSource(root, manifest) ?? {})) {
|
|
101
|
+
const converted = convertMcpEntry(name, server, raw, root);
|
|
102
|
+
if ('skipped' in converted) skipped.push({ name: server, reason: converted.skipped });
|
|
103
|
+
else mcp.push(converted);
|
|
104
|
+
}
|
|
105
|
+
return { root, name, version, skillRoots, skills, mcp, skipped };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface ClaudePluginPlan {
|
|
109
|
+
plugin: ClaudePlugin;
|
|
110
|
+
newSkillRoots: string[];
|
|
111
|
+
newMcp: PluginMcpServer[];
|
|
112
|
+
state: 'new' | 'already' | 'empty';
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function planClaudePlugin(plugin: ClaudePlugin, activeDirs: readonly string[], mcpIds: readonly string[]): ClaudePluginPlan {
|
|
116
|
+
const newSkillRoots = plugin.skillRoots.filter((dir) => !activeDirs.includes(dir));
|
|
117
|
+
const newMcp = plugin.mcp.filter((server) => !mcpIds.includes(server.id));
|
|
118
|
+
const state = !plugin.skills.length && !plugin.mcp.length ? 'empty' : newSkillRoots.length || newMcp.length ? 'new' : 'already';
|
|
119
|
+
return { plugin, newSkillRoots, newMcp, state };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function claudePluginPrompt(plan: ClaudePluginPlan): string {
|
|
123
|
+
const { plugin } = plan;
|
|
124
|
+
const head = `Claude 플러그인 «${plugin.name}» ${plugin.version}`;
|
|
125
|
+
const skipped = plugin.skipped.map((s) => `\n · MCP ${s.name}: ${s.reason}`).join('');
|
|
126
|
+
if (plan.state === 'empty') return `${head}에서 쓸 스킬이나 MCP 서버를 찾지 못했습니다.${skipped}`;
|
|
127
|
+
if (plan.state === 'already') return `${head}은 이미 연결돼 있습니다 — 스킬 ${plugin.skills.length}개 · MCP ${plugin.mcp.length}개.${skipped}`;
|
|
128
|
+
const parts = [
|
|
129
|
+
plugin.skills.length ? `스킬 ${plugin.skills.length}개(그 자리를 그대로 씁니다)` : '',
|
|
130
|
+
plan.newMcp.length ? `MCP 서버 ${plan.newMcp.length}개(꺼진 채로 등록 — 켜기는 따로 확인합니다)` : '',
|
|
131
|
+
].filter(Boolean).join(' · ');
|
|
132
|
+
return `${head}: ${parts}${skipped}\n가져올까요? [Y/n] `;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** How to turn one registered server on — shown after connecting, never done for the person. */
|
|
136
|
+
export function mcpEnableHint(servers: readonly PluginMcpServer[], all: readonly { id?: unknown }[]): string[] {
|
|
137
|
+
return servers.map((server) => {
|
|
138
|
+
const index = all.findIndex((row) => row.id === server.id);
|
|
139
|
+
const what = 'url' in server ? server.url : server.command.join(' ');
|
|
140
|
+
return ` · ${server.id} — ${what}\n 켜려면: elanous config set mcp.servers.${index}.enabled true`;
|
|
141
|
+
});
|
|
142
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// EN9 — `elanous connect <claude|codex>`: 쓰던 에이전트의 스킬을 찾아 «복사가 아니라 등록»한다(대표 «기존 플러그인을 그대로 쓸 수 있다»).
|
|
2
|
+
// 등록 = `skills.sources[]`(EN5 한 모델)에 kind `connected` 로 원본 경로를 더한다 · 원본은 읽기만 하고 고치지 않는다 · 읽지 못한 스킬은 SK2(`skills repair`)로 안내한다.
|
|
3
|
+
import { existsSync, readdirSync, statSync } from 'node:fs';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { skillSourceId, type SkillSource } from '../user-config.js';
|
|
7
|
+
|
|
8
|
+
export type ConnectAgent = 'claude' | 'codex';
|
|
9
|
+
export const CONNECT_AGENTS: readonly ConnectAgent[] = ['claude', 'codex'];
|
|
10
|
+
|
|
11
|
+
const AGENT_LABEL: Record<ConnectAgent, string> = { claude: 'Claude Code', codex: 'Codex' };
|
|
12
|
+
|
|
13
|
+
export function connectSkillRoot(agent: ConnectAgent, home = homedir()): string {
|
|
14
|
+
return agent === 'claude' ? join(home, '.claude', 'skills') : join(home, '.codex', 'skills');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Folders that hold a SKILL.md (one level, like the skill index). Hidden folders such as `.system` are skipped. */
|
|
18
|
+
export function findSkillFolders(root: string): string[] {
|
|
19
|
+
try {
|
|
20
|
+
return readdirSync(root)
|
|
21
|
+
.filter((name) => !name.startsWith('.'))
|
|
22
|
+
.filter((name) => {
|
|
23
|
+
try { return statSync(join(root, name)).isDirectory() && existsSync(join(root, name, 'SKILL.md')); }
|
|
24
|
+
catch { return false; }
|
|
25
|
+
})
|
|
26
|
+
.sort();
|
|
27
|
+
} catch { return []; }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type ConnectPlan =
|
|
31
|
+
| { agent: ConnectAgent; root: string; state: 'missing' }
|
|
32
|
+
| { agent: ConnectAgent; root: string; state: 'empty' }
|
|
33
|
+
| { agent: ConnectAgent; root: string; state: 'already'; skills: string[] }
|
|
34
|
+
| { agent: ConnectAgent; root: string; state: 'new'; skills: string[] };
|
|
35
|
+
|
|
36
|
+
/** `activeDirs` = the skill directories elanous reads today (defaultSkillDirs). */
|
|
37
|
+
export function planConnect(agent: ConnectAgent, activeDirs: readonly string[], home = homedir()): ConnectPlan {
|
|
38
|
+
const root = connectSkillRoot(agent, home);
|
|
39
|
+
if (!existsSync(root)) return { agent, root, state: 'missing' };
|
|
40
|
+
const skills = findSkillFolders(root);
|
|
41
|
+
if (!skills.length) return { agent, root, state: 'empty' };
|
|
42
|
+
return activeDirs.includes(root) ? { agent, root, state: 'already', skills } : { agent, root, state: 'new', skills };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function sample(skills: readonly string[]): string {
|
|
46
|
+
const head = skills.slice(0, 5).join(', ');
|
|
47
|
+
return skills.length > 5 ? `${head} 외 ${skills.length - 5}개` : head;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The first screen — one question, or one line saying why there is nothing to ask. */
|
|
51
|
+
export function connectPrompt(plan: ConnectPlan): string {
|
|
52
|
+
const who = AGENT_LABEL[plan.agent];
|
|
53
|
+
switch (plan.state) {
|
|
54
|
+
case 'missing': return `${who} 스킬 폴더가 없습니다(${plan.root}). 가져올 것이 없습니다.`;
|
|
55
|
+
case 'empty': return `${who} 스킬 폴더에 스킬이 없습니다(${plan.root}).`;
|
|
56
|
+
case 'already': return `${who}에서 쓰던 스킬 ${plan.skills.length}개는 이미 연결돼 있습니다 — ${sample(plan.skills)}`;
|
|
57
|
+
case 'new': return `${who}에서 쓰던 스킬 ${plan.skills.length}개를 찾았습니다 — ${sample(plan.skills)}\n복사하지 않고 그 자리(${plan.root})를 그대로 씁니다. 가져올까요? [Y/n] `;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Returns the new connected-source list (unchanged when the path is already there). */
|
|
62
|
+
export function withConnectedSource(sources: readonly SkillSource[] | undefined, path: string, id = skillSourceId(path)): SkillSource[] {
|
|
63
|
+
const list = [...(sources ?? [])];
|
|
64
|
+
if (!list.some((source) => source.path === path)) list.push({ id, path, kind: 'connected', enabled: true });
|
|
65
|
+
return list;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function withoutSources(sources: readonly SkillSource[] | undefined, paths: readonly string[]): SkillSource[] {
|
|
69
|
+
return (sources ?? []).filter((source) => !paths.includes(source.path));
|
|
70
|
+
}
|
package/src/skills/index.ts
CHANGED
|
@@ -93,6 +93,17 @@ export function nameTriggers(name: string): string[] {
|
|
|
93
93
|
return out;
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
/** A skill the index could not read — surfaced to the person (startup notice · repair offer), never swallowed. */
|
|
97
|
+
export interface SkillIndexProblem { name: string; dir: string; code: string | null; error: string }
|
|
98
|
+
const skillProblems = new Map<string, SkillIndexProblem>();
|
|
99
|
+
function recordSkillIndexProblem(problem: SkillIndexProblem): void {
|
|
100
|
+
skillProblems.set(`${problem.dir}\u0000${problem.name}`, problem);
|
|
101
|
+
}
|
|
102
|
+
/** Skills skipped by the most recent index builds (deduped by dir + name). */
|
|
103
|
+
export function skillIndexProblems(): SkillIndexProblem[] {
|
|
104
|
+
return [...skillProblems.values()];
|
|
105
|
+
}
|
|
106
|
+
|
|
96
107
|
function makeEntry(name: string, rootDir: string): SkillIndexEntry | null {
|
|
97
108
|
let m: ReturnType<typeof parseSkillMd>;
|
|
98
109
|
try {
|
|
@@ -458,9 +469,26 @@ export function buildSkillIndex(
|
|
|
458
469
|
const out: SkillIndexEntry[] = [];
|
|
459
470
|
const seenNames = new Set<string>();
|
|
460
471
|
for (const dir of dirs) {
|
|
472
|
+
// Problems reflect the latest build of each dir — a repaired skill must drop off the list (SK2).
|
|
473
|
+
for (const key of [...skillProblems.keys()]) if (key.startsWith(`${dir}\u0000`)) skillProblems.delete(key);
|
|
461
474
|
const names = listSkillNames(dir);
|
|
462
475
|
for (const name of names) {
|
|
463
|
-
|
|
476
|
+
// One malformed third-party skill must not take down the index (and with it the first screen).
|
|
477
|
+
let entry: SkillIndexEntry | null;
|
|
478
|
+
try {
|
|
479
|
+
entry = makeEntry(name, dir);
|
|
480
|
+
} catch (err) {
|
|
481
|
+
// 대표 10-01: a broken skill must never crash the product — and must never vanish silently either (G9 P5a).
|
|
482
|
+
// It is skipped, recorded in skillIndexProblems() (a startup notice offers to repair it), and logged.
|
|
483
|
+
const problem: SkillIndexProblem = {
|
|
484
|
+
name, dir,
|
|
485
|
+
code: (err as NodeJS.ErrnoException)?.code ?? null,
|
|
486
|
+
error: err instanceof Error ? err.message.slice(0, 200) : String(err).slice(0, 200),
|
|
487
|
+
};
|
|
488
|
+
recordSkillIndexProblem(problem);
|
|
489
|
+
debug.log('skills.index', 'skill-skipped', { ...problem }, { level: 'warn' });
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
464
492
|
if (!entry) continue;
|
|
465
493
|
if (seenNames.has(entry.name)) continue; // first-wins dedup
|
|
466
494
|
seenNames.add(entry.name);
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// SK2 — 남의 SKILL.md 를 색인이 못 읽으면(skillIndexProblems) 시스템 에러 대신 «안내 ⊕ 확인 뒤 수리».
|
|
2
|
+
// 대표 10-01: «시스템 에러가 아니라 별도 시스템 메시지를 받고 스킬을 수리» · «PR 이 아니라 접근한 서피스에서 간단히».
|
|
3
|
+
// 고칠 수 있는 것만 고친다: 한 줄이어야 하는 프론트매터 값이 YAML 목록·인라인 배열이면 한 줄로 편다.
|
|
4
|
+
// 권한 오류는 원인·명령만 안내한다. 고치기 전 원본은 옆에 `.bak` 으로 남기고, 다시 읽어 안 풀리면 되돌린다.
|
|
5
|
+
import { chmodSync, copyFileSync, existsSync, readFileSync, renameSync, statSync, writeFileSync } from 'node:fs';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import { debug } from '../debug/log.js';
|
|
8
|
+
import { buildSkillIndex, skillIndexProblems, type SkillIndexProblem } from './index.js';
|
|
9
|
+
|
|
10
|
+
/** Frontmatter keys the index reads as one-line text. */
|
|
11
|
+
const SCALAR_KEYS = ['name', 'description', 'version', 'when_to_use', 'when-to-use', 'argument-hint', 'model', 'category'];
|
|
12
|
+
|
|
13
|
+
export type SkillRepairPlan =
|
|
14
|
+
| { kind: 'fixable'; problem: SkillIndexProblem; path: string; keys: string[] }
|
|
15
|
+
| { kind: 'fixable-permission'; problem: SkillIndexProblem; path: string }
|
|
16
|
+
| { kind: 'permission'; problem: SkillIndexProblem; path: string; hint: string }
|
|
17
|
+
| { kind: 'manual'; problem: SkillIndexProblem; path: string; hint: string };
|
|
18
|
+
|
|
19
|
+
export function skillFilePath(problem: SkillIndexProblem): string {
|
|
20
|
+
return join(problem.dir, problem.name, 'SKILL.md');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function quote(value: string): string {
|
|
24
|
+
return `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function stripQuotes(value: string): string {
|
|
28
|
+
const v = value.trim();
|
|
29
|
+
return (v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'")) ? v.slice(1, -1) : v;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Rewrite list-valued scalar keys as one quoted line; returns null when nothing needs changing. */
|
|
33
|
+
export function flattenScalarLists(markdown: string): { text: string; keys: string[] } | null {
|
|
34
|
+
const fm = /^---\r?\n([\s\S]*?)\r?\n---/.exec(markdown);
|
|
35
|
+
if (!fm) return null;
|
|
36
|
+
const lines = fm[1]!.split(/\r?\n/);
|
|
37
|
+
const out: string[] = [];
|
|
38
|
+
const keys: string[] = [];
|
|
39
|
+
for (let i = 0; i < lines.length; i++) {
|
|
40
|
+
const line = lines[i]!;
|
|
41
|
+
const m = /^([A-Za-z][A-Za-z0-9_-]*)\s*:\s*(.*)$/.exec(line);
|
|
42
|
+
if (!m || !SCALAR_KEYS.includes(m[1]!)) { out.push(line); continue; }
|
|
43
|
+
const key = m[1]!;
|
|
44
|
+
const value = m[2]!.trim();
|
|
45
|
+
if (/^\[.*\]$/.test(value)) {
|
|
46
|
+
const items = value.slice(1, -1).split(',').map(stripQuotes).filter(Boolean);
|
|
47
|
+
out.push(`${key}: ${quote(items.join(' · '))}`); keys.push(key);
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (value === '') {
|
|
51
|
+
const items: string[] = [];
|
|
52
|
+
let j = i + 1;
|
|
53
|
+
while (j < lines.length && /^\s+-\s+/.test(lines[j]!)) { items.push(stripQuotes(lines[j]!.replace(/^\s+-\s+/, ''))); j++; }
|
|
54
|
+
if (items.length) { out.push(`${key}: ${quote(items.join(' · '))}`); keys.push(key); i = j - 1; continue; }
|
|
55
|
+
}
|
|
56
|
+
out.push(line);
|
|
57
|
+
}
|
|
58
|
+
if (!keys.length) return null;
|
|
59
|
+
const text = markdown.slice(0, fm.index) + `---\n${out.join('\n')}\n---` + markdown.slice(fm.index + fm[0].length);
|
|
60
|
+
return { text, keys };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function planSkillRepair(problem: SkillIndexProblem, read: (path: string) => string = (p) => readFileSync(p, 'utf8')): SkillRepairPlan {
|
|
64
|
+
const path = skillFilePath(problem);
|
|
65
|
+
if (problem.code === 'EACCES' || problem.code === 'EPERM') {
|
|
66
|
+
// Our own file: we may add the read bit after the person says yes. Someone else's: name the command only.
|
|
67
|
+
try {
|
|
68
|
+
if (typeof process.getuid === 'function' && statSync(path).uid === process.getuid()) return { kind: 'fixable-permission', problem, path };
|
|
69
|
+
} catch { /* fall through to the hint */ }
|
|
70
|
+
return { kind: 'permission', problem, path, hint: `읽기 권한이 없습니다 — 터미널에서: sudo chmod a+r "${path}"` };
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
const fixed = flattenScalarLists(read(path));
|
|
74
|
+
if (fixed) return { kind: 'fixable', problem, path, keys: fixed.keys };
|
|
75
|
+
} catch (error) {
|
|
76
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
77
|
+
if (code === 'EACCES' || code === 'EPERM') return { kind: 'permission', problem, path, hint: `읽기 권한이 없습니다 — 터미널에서: chmod u+r "${path}"` };
|
|
78
|
+
}
|
|
79
|
+
return { kind: 'manual', problem, path, hint: `자동으로 고칠 수 없는 형식입니다 — 파일 맨 위 «---» 사이를 확인해 주세요: ${path}` };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export type SkillRepairResult = { ok: true; path: string; keys: string[]; backup: string } | { ok: false; path: string; reason: string };
|
|
83
|
+
|
|
84
|
+
function stillUnreadable(problem: SkillIndexProblem): boolean {
|
|
85
|
+
buildSkillIndex([problem.dir]);
|
|
86
|
+
return skillIndexProblems().some((p) => p.dir === problem.dir && p.name === problem.name);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Apply a fixable plan (after the person said yes): read bit, or back up → rewrite → re-index → restore on failure. */
|
|
90
|
+
export function applySkillRepair(plan: Extract<SkillRepairPlan, { kind: 'fixable' | 'fixable-permission' }>): SkillRepairResult {
|
|
91
|
+
const { path, problem } = plan;
|
|
92
|
+
if (plan.kind === 'fixable-permission') {
|
|
93
|
+
const before = statSync(path).mode & 0o777;
|
|
94
|
+
chmodSync(path, before | 0o400);
|
|
95
|
+
if (stillUnreadable(problem)) {
|
|
96
|
+
chmodSync(path, before);
|
|
97
|
+
debug.log('skills.repair', 'reverted', { name: problem.name, kind: 'permission' });
|
|
98
|
+
return { ok: false, path, reason: 'still-unreadable' };
|
|
99
|
+
}
|
|
100
|
+
debug.log('skills.repair', 'repaired', { name: problem.name, kind: 'permission' });
|
|
101
|
+
return { ok: true, path, keys: ['permission'], backup: '' };
|
|
102
|
+
}
|
|
103
|
+
const original = readFileSync(path, 'utf8');
|
|
104
|
+
const fixed = flattenScalarLists(original);
|
|
105
|
+
if (!fixed) return { ok: false, path, reason: 'nothing-to-fix' };
|
|
106
|
+
const backup = `${path}.bak`;
|
|
107
|
+
copyFileSync(path, existsSync(backup) ? `${path}.${Date.now()}.bak` : backup);
|
|
108
|
+
const temp = `${path}.tmp-${process.pid}`;
|
|
109
|
+
writeFileSync(temp, fixed.text);
|
|
110
|
+
renameSync(temp, path);
|
|
111
|
+
if (stillUnreadable(problem)) {
|
|
112
|
+
writeFileSync(path, original);
|
|
113
|
+
debug.log('skills.repair', 'reverted', { name: problem.name, keys: fixed.keys });
|
|
114
|
+
return { ok: false, path, reason: 'still-unreadable' };
|
|
115
|
+
}
|
|
116
|
+
debug.log('skills.repair', 'repaired', { name: problem.name, keys: fixed.keys });
|
|
117
|
+
return { ok: true, path, keys: fixed.keys, backup };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** One line for a surface (TUI · REPL · PWA) — null when every skill was read. */
|
|
121
|
+
export function skillRepairNotice(problems: readonly SkillIndexProblem[]): string | null {
|
|
122
|
+
if (!problems.length) return null;
|
|
123
|
+
return `스킬 ${problems.length}개를 읽지 못했습니다 — 고치려면: elanous skills repair (확인한 뒤에만 고칩니다)`;
|
|
124
|
+
}
|
package/src/skills/runner.ts
CHANGED
|
@@ -330,6 +330,15 @@ function parseTier(raw: unknown): SkillTier | undefined {
|
|
|
330
330
|
|
|
331
331
|
const FRONTMATTER_RE = /^---\s*\n([\s\S]*?)\n---\s*\n?/;
|
|
332
332
|
|
|
333
|
+
/** A frontmatter value as one line of text. Third-party SKILL.md files (~/.codex/skills, ~/.claude/skills …)
|
|
334
|
+
* sometimes write `description:` as a YAML list; the parser then returns an array, and a 0.2.6 beta tester's
|
|
335
|
+
* first screen crashed on `description.trim is not a function`. Lists are joined; anything else non-string is dropped. */
|
|
336
|
+
export function frontmatterText(value: unknown): string {
|
|
337
|
+
if (typeof value === 'string') return value;
|
|
338
|
+
if (Array.isArray(value)) return value.filter((x): x is string => typeof x === 'string').join(' ').trim();
|
|
339
|
+
return '';
|
|
340
|
+
}
|
|
341
|
+
|
|
333
342
|
function parseFrontmatter(md: string): { fm: Record<string, any>; body: string } {
|
|
334
343
|
const match = md.match(FRONTMATTER_RE);
|
|
335
344
|
if (!match) return { fm: {}, body: md };
|
|
@@ -544,8 +553,8 @@ export function parseSkillMd(skillName: string, baseDir: string = LOCAL_SKILLS_D
|
|
|
544
553
|
const cost = rawCost === 'light' || rawCost === 'heavy' ? rawCost : undefined;
|
|
545
554
|
|
|
546
555
|
return {
|
|
547
|
-
name: fm['name'] || skillName,
|
|
548
|
-
description: fm['description']
|
|
556
|
+
name: frontmatterText(fm['name']) || skillName,
|
|
557
|
+
description: frontmatterText(fm['description']),
|
|
549
558
|
model: fm['model'] || undefined,
|
|
550
559
|
allowedTools,
|
|
551
560
|
deniedTools,
|
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
// Slash
|
|
2
|
-
//
|
|
3
|
-
// Screen does not compute. It prints the same structured report the
|
|
4
|
-
// `elanous usage` command produces.
|
|
1
|
+
// Slash commands: /usage and /remaining — compact TUI view of the unified report.
|
|
5
2
|
|
|
6
|
-
import { collectUnifiedUsage,
|
|
3
|
+
import { collectUnifiedUsage, formatUsageCompact, type UnifiedUsageDeps } from '../../budget/unified-usage.js';
|
|
4
|
+
import { visibleWidth } from '../../tui.js';
|
|
7
5
|
import type { SlashExecuteRequest, SlashExecuteResult } from './dashboard-slash.js';
|
|
8
6
|
|
|
9
7
|
export interface UsageSlashResult extends SlashExecuteResult {
|
|
@@ -12,15 +10,18 @@ export interface UsageSlashResult extends SlashExecuteResult {
|
|
|
12
10
|
}
|
|
13
11
|
|
|
14
12
|
export async function executeUsageSlash(
|
|
15
|
-
req: SlashExecuteRequest,
|
|
13
|
+
req: SlashExecuteRequest & { width?: number },
|
|
16
14
|
deps: UnifiedUsageDeps = {},
|
|
17
15
|
): Promise<UsageSlashResult | null> {
|
|
18
16
|
if (req.name !== 'usage' && req.name !== 'remaining') return null;
|
|
19
17
|
const report = await collectUnifiedUsage(deps);
|
|
18
|
+
const width = req.width ?? 120;
|
|
19
|
+
const hint = ['자세히: elanous usage', 'elanous usage', 'elanous…', '…']
|
|
20
|
+
.find((text) => visibleWidth(text) <= width) ?? '';
|
|
20
21
|
return {
|
|
21
22
|
ok: true,
|
|
22
23
|
name: req.name,
|
|
23
24
|
args: req.args,
|
|
24
|
-
logLines:
|
|
25
|
+
logLines: [...formatUsageCompact(report, { width, nowMs: Date.now() }), hint],
|
|
25
26
|
};
|
|
26
27
|
}
|
|
@@ -56,7 +56,7 @@ const BACKWARD_MARKER = /(?:등(?:의)?\s*)?(?:언급\s*)?시\s*(?:이?\s*스킬
|
|
|
56
56
|
* keywords. Empty array is a perfectly normal result (description
|
|
57
57
|
* without any recognized marker). */
|
|
58
58
|
export function extractTriggers(description: string): string[] {
|
|
59
|
-
if (
|
|
59
|
+
if (typeof description !== 'string' || !description.trim()) return [];
|
|
60
60
|
|
|
61
61
|
const segments = extractTriggerSegments(description);
|
|
62
62
|
const items: string[] = [];
|