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
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// PS1 — `elanous persona list|add|show`: 프리셋(persona-presets/)을 보고, 골라서 «내 페르소나»를 만들고, 만든 것을 본다.
|
|
2
|
+
// 만든 페르소나는 기존 페르소나 저장소(resolveStatePersonaDir · Discord·PWA 가 읽는 곳)의 `<id>.yaml` 한 파일이다.
|
|
3
|
+
import type { Command } from 'commander';
|
|
4
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { parse as parseYaml } from 'yaml';
|
|
7
|
+
import { debug } from '../debug/log.js';
|
|
8
|
+
import { parsePersonaYaml } from '../persona/loader.js';
|
|
9
|
+
import { resolveRepositoryPersonaDir, resolveStatePersonaDir } from '../persona/global-registry.js';
|
|
10
|
+
import { choosePersonaId, describePreset, findPreset, loadPresetIndex, loadPresets, presetToProfileYaml } from '../persona/presets.js';
|
|
11
|
+
|
|
12
|
+
interface StoredPersona { personaId: string; displayName: string; description?: string; preset?: string; title?: string; path: string }
|
|
13
|
+
|
|
14
|
+
/** Personas the person made (state dir), parsed with the same loader the runtime uses. */
|
|
15
|
+
export function readStoredPersonas(dir = resolveStatePersonaDir()): StoredPersona[] {
|
|
16
|
+
if (!existsSync(dir)) return [];
|
|
17
|
+
const out: StoredPersona[] = [];
|
|
18
|
+
for (const file of readdirSync(dir).filter((f) => f.endsWith('.yaml') && !f.startsWith('_')).sort()) {
|
|
19
|
+
const path = join(dir, file);
|
|
20
|
+
const text = readFileSync(path, 'utf8');
|
|
21
|
+
const parsed = parsePersonaYaml(text, path);
|
|
22
|
+
if (!parsed.ok) continue;
|
|
23
|
+
const origin = (parseYaml(text) as { preset?: { id?: unknown; title?: unknown } } | null)?.preset;
|
|
24
|
+
const preset = typeof origin?.id === 'string' ? origin.id : undefined;
|
|
25
|
+
const title = typeof origin?.title === 'string' ? origin.title : undefined;
|
|
26
|
+
out.push({ personaId: parsed.profile.personaId, displayName: parsed.profile.displayName, ...(parsed.profile.description ? { description: parsed.profile.description } : {}), ...(preset ? { preset } : {}), ...(title ? { title } : {}), path });
|
|
27
|
+
}
|
|
28
|
+
return out.sort((a, b) => a.personaId.localeCompare(b.personaId));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function takenIds(stateDir: string): Set<string> {
|
|
32
|
+
const ids = new Set(readStoredPersonas(stateDir).map((p) => p.personaId));
|
|
33
|
+
const repo = resolveRepositoryPersonaDir();
|
|
34
|
+
if (existsSync(repo)) for (const f of readdirSync(repo)) if (f.endsWith('.yaml')) ids.add(f.slice(0, -5));
|
|
35
|
+
if (existsSync(stateDir)) for (const f of readdirSync(stateDir)) if (f.endsWith('.yaml')) ids.add(f.slice(0, -5));
|
|
36
|
+
return ids;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function registerPersonaCommands(program: Command): void {
|
|
40
|
+
const persona = program.command('persona').description('Persona presets — AI coworkers you pick and customise (list · add · show)');
|
|
41
|
+
|
|
42
|
+
persona.command('list')
|
|
43
|
+
.description('List the persona presets, then the personas you have made')
|
|
44
|
+
.option('--json', 'JSON output')
|
|
45
|
+
.action((opts: { json?: boolean }) => {
|
|
46
|
+
const presets = loadPresets();
|
|
47
|
+
const mine = readStoredPersonas();
|
|
48
|
+
debug.log('persona.cli', 'list', { presets: presets.length, mine: mine.length });
|
|
49
|
+
if (opts.json) { console.log(JSON.stringify({ presets: presets.map((p) => ({ id: p.personaId, displayName: p.displayName, role: p.role, oneLine: p.oneLine })), mine }, null, 2)); return; }
|
|
50
|
+
if (!presets.length) console.log('페르소나 프리셋을 찾지 못했습니다(persona-presets/ 가 이 설치본에 없습니다).');
|
|
51
|
+
else {
|
|
52
|
+
console.log('프리셋 — 골라서 내 페르소나로 만들기: elanous persona add <프리셋> [--name <이름>]');
|
|
53
|
+
const width = Math.max(...presets.map((p) => p.personaId.length));
|
|
54
|
+
for (const p of presets) console.log(` ${p.personaId.padEnd(width)} ${p.displayName} — ${p.role} · ${p.oneLine}`);
|
|
55
|
+
}
|
|
56
|
+
const made = mine.filter((p) => p.preset);
|
|
57
|
+
if (made.length) {
|
|
58
|
+
console.log('\n내 페르소나:');
|
|
59
|
+
for (const p of made) console.log(` ${p.personaId} ${p.displayName}${p.title ? ` (${p.title})` : ''} (프리셋 ${p.preset})`);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
persona.command('add <preset>')
|
|
64
|
+
.description('Make your own persona from a preset (id, name or role) — the preset file is not changed')
|
|
65
|
+
.option('--name <name>', 'What to call it (default: the preset’s first name)')
|
|
66
|
+
.option('--title <title>', 'Optional title, e.g. CMO')
|
|
67
|
+
.option('--json', 'JSON output')
|
|
68
|
+
.action((query: string, opts: { name?: string; title?: string; json?: boolean }) => {
|
|
69
|
+
const presets = loadPresets();
|
|
70
|
+
const preset = findPreset(presets, query);
|
|
71
|
+
if (!preset) {
|
|
72
|
+
console.error(`프리셋 «${query}» 을 찾지 못했습니다. 있는 것: ${presets.map((p) => p.personaId).join(', ')}`);
|
|
73
|
+
process.exitCode = 1;
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const dir = resolveStatePersonaDir();
|
|
77
|
+
const personaId = choosePersonaId(preset, opts.name, takenIds(dir));
|
|
78
|
+
const yaml = presetToProfileYaml(preset, loadPresetIndex().common, { personaId, ...(opts.name ? { name: opts.name } : {}), ...(opts.title ? { title: opts.title } : {}) });
|
|
79
|
+
const path = join(dir, `${personaId}.yaml`);
|
|
80
|
+
// The runtime loader must accept what we write — check before it lands on disk.
|
|
81
|
+
const check = parsePersonaYaml(yaml, path);
|
|
82
|
+
if (!check.ok) { console.error(`만든 페르소나를 읽을 수 없습니다: ${check.error.message}`); process.exitCode = 1; return; }
|
|
83
|
+
mkdirSync(dir, { recursive: true });
|
|
84
|
+
writeFileSync(path, yaml, { flag: 'wx' });
|
|
85
|
+
debug.log('persona.cli', 'added', { preset: preset.personaId, personaId, named: Boolean(opts.name) });
|
|
86
|
+
if (opts.json) { console.log(JSON.stringify({ personaId, displayName: check.profile.displayName, preset: preset.personaId, path }, null, 2)); return; }
|
|
87
|
+
console.log(`만들었습니다 — ${check.profile.displayName} (id ${personaId} · 프리셋 ${preset.personaId})`);
|
|
88
|
+
console.log(` 처음 묻는 것: ${preset.firstQuestions.join(' / ')}`);
|
|
89
|
+
console.log(` 보기: elanous persona show ${personaId}`);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
persona.command('show <name>')
|
|
93
|
+
.description('Show one of your personas (id or name), or a preset')
|
|
94
|
+
.option('--json', 'JSON output')
|
|
95
|
+
.action((query: string, opts: { json?: boolean }) => {
|
|
96
|
+
const q = query.trim().toLowerCase();
|
|
97
|
+
const mine = readStoredPersonas().find((p) => p.personaId === q || p.displayName.toLowerCase().startsWith(q));
|
|
98
|
+
const presets = loadPresets();
|
|
99
|
+
if (mine) {
|
|
100
|
+
const text = readFileSync(mine.path, 'utf8');
|
|
101
|
+
const parsed = parsePersonaYaml(text, mine.path);
|
|
102
|
+
if (opts.json) { console.log(JSON.stringify({ kind: 'persona', ...mine, systemPrompt: parsed.ok ? parsed.profile.systemPrompt : undefined }, null, 2)); return; }
|
|
103
|
+
console.log(`${mine.displayName}${mine.title ? ` (${mine.title})` : ''} (id ${mine.personaId}${mine.preset ? ` · 프리셋 ${mine.preset}` : ''})`);
|
|
104
|
+
if (mine.description) console.log(` ${mine.description}`);
|
|
105
|
+
const preset = mine.preset ? presets.find((p) => p.personaId === mine.preset) : undefined;
|
|
106
|
+
if (preset) for (const line of describePreset(preset).slice(2)) console.log(line);
|
|
107
|
+
console.log(` 파일: ${mine.path}`);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const preset = findPreset(presets, query);
|
|
111
|
+
if (!preset) { console.error(`«${query}» 페르소나도 프리셋도 없습니다. 목록: elanous persona list`); process.exitCode = 1; return; }
|
|
112
|
+
if (opts.json) { console.log(JSON.stringify({ kind: 'preset', ...preset }, null, 2)); return; }
|
|
113
|
+
console.log('(프리셋 — 아직 만들지 않았습니다. 만들기: elanous persona add ' + preset.personaId + ')');
|
|
114
|
+
for (const line of describePreset(preset)) console.log(line);
|
|
115
|
+
});
|
|
116
|
+
}
|
package/src/cli/pr-cli.ts
CHANGED
|
@@ -115,6 +115,8 @@ export interface PrLandDeps {
|
|
|
115
115
|
/** iOS 시험 게이트 심(시험 주입용). */
|
|
116
116
|
runIosGate?: (out: { log: (message: string) => void; error: (message: string) => void }, changedFiles: readonly string[]) => boolean;
|
|
117
117
|
isInteractive?: () => boolean;
|
|
118
|
+
/** Clock for the recent-landings window (test seam · default Date.now). */
|
|
119
|
+
now?: () => number;
|
|
118
120
|
requestConfirmation?: (opts: ConfirmOpts) => Promise<ConfirmResult>;
|
|
119
121
|
overlapConfirmChannels?: ConfirmChannel[];
|
|
120
122
|
overlapConfirmTimeoutMs?: number;
|
|
@@ -822,11 +824,12 @@ function emitRecentLandingRateAdvisory(
|
|
|
822
824
|
cwd: string,
|
|
823
825
|
commits: readonly { subject: string; authorEmail: string; committedAtMs: number }[] | null,
|
|
824
826
|
out: { log: (message: string) => void },
|
|
827
|
+
now: () => number = Date.now,
|
|
825
828
|
): boolean {
|
|
826
829
|
const windowMinutes = RECENT_LANDING_WINDOW_MINUTES;
|
|
827
830
|
const authorEmail = currentAuthorEmail(run, cwd);
|
|
828
831
|
if (!authorEmail || commits === null) {
|
|
829
|
-
const previous = commits === null ? undefined : previousLandingSummary(commits,
|
|
832
|
+
const previous = commits === null ? undefined : previousLandingSummary(commits, now());
|
|
830
833
|
record('recent-landing-rate', true, {
|
|
831
834
|
recentCount: 0,
|
|
832
835
|
windowMinutes,
|
|
@@ -836,7 +839,7 @@ function emitRecentLandingRateAdvisory(
|
|
|
836
839
|
});
|
|
837
840
|
return false;
|
|
838
841
|
}
|
|
839
|
-
const nowMs =
|
|
842
|
+
const nowMs = now();
|
|
840
843
|
const recentCount = countRecentLandings(commits, {
|
|
841
844
|
authorEmail,
|
|
842
845
|
nowMs,
|
|
@@ -1077,6 +1080,7 @@ export async function runPrLand(opts: PrLandOpts = {}, deps: PrLandDeps = {}): P
|
|
|
1077
1080
|
cwd,
|
|
1078
1081
|
collectResolvedBaseLandings(run, cwd, DEFAULT_GRANULARITY_SINCE, base),
|
|
1079
1082
|
out,
|
|
1083
|
+
deps.now,
|
|
1080
1084
|
);
|
|
1081
1085
|
|
|
1082
1086
|
// ⛔ local-path / no-remote 는 gh 가 원리상 못 한다. 이미 아는 자리에서 멈춘다.
|
package/src/cli/release-cli.ts
CHANGED
|
@@ -23,7 +23,8 @@ import { effectiveInstanceRoot, releaseLedgerRoot } from '../instance/resolve.js
|
|
|
23
23
|
import { getElanousConfigDirOverride } from '../elanous-config-dir.js';
|
|
24
24
|
import { envLiteral } from '../platform/env-literal.js';
|
|
25
25
|
import { userConfigPath } from '../user-config.js';
|
|
26
|
-
import { addItem, devVersion, listChecklist, removeItem, seedFromRoadmap, setItem, summarizeChecklist, type ChecklistStatus, type ChecklistDisposition } from '../release-loop/checklist.js';
|
|
26
|
+
import { addItem, devVersion, listChecklist, removeItem, seedFromRoadmap, setItem, summarizeChecklist, type ChecklistStatus, type ChecklistDisposition, type ChecklistKind } from '../release-loop/checklist.js';
|
|
27
|
+
import * as features from '../release-loop/feature-store.js';
|
|
27
28
|
import { writeStdoutJson } from './stdout-json.js';
|
|
28
29
|
import { runUnattendedRelease, type UnattendedReleaseDeps } from '../../scripts/release-loop/unattended-release.js';
|
|
29
30
|
import { releaseReadiness } from '../../scripts/release-loop/release-readiness.js';
|
|
@@ -492,7 +493,7 @@ export function registerReleaseCommands(program: Command, releaseRunDeps: Unatte
|
|
|
492
493
|
.option('--version <v>', '판 또는 별칭(기본: package.json 의 개발판에서 -dev.N 제거)')
|
|
493
494
|
.option('--json', '결과 JSON');
|
|
494
495
|
const context = (cmd: Command) => {
|
|
495
|
-
const opts = { ...(cmd.parent?.opts() as { version?: string; json?: boolean }), ...(cmd.opts() as { version?: string; json?: boolean }) };
|
|
496
|
+
const opts = { ...(cmd.parent?.parent?.opts() as { version?: string; json?: boolean }), ...(cmd.parent?.opts() as { version?: string; json?: boolean }), ...(cmd.opts() as { version?: string; json?: boolean }) };
|
|
496
497
|
const codenames = checklistCodenames();
|
|
497
498
|
return { version: checklistVersion(opts.version, codenames), codenames, json: opts.json };
|
|
498
499
|
};
|
|
@@ -505,20 +506,21 @@ export function registerReleaseCommands(program: Command, releaseRunDeps: Unatte
|
|
|
505
506
|
const { version, codenames, json } = context(cmd);
|
|
506
507
|
await checklistOutput(version, codenames, 'status', json);
|
|
507
508
|
});
|
|
508
|
-
withContext(checklist.command('add <id> <title>').description('칸 추가')).option('--owner <owner>', '담당').
|
|
509
|
+
withContext(checklist.command('add <id> <title>').description('칸 추가')).option('--owner <owner>', '담당').option('--kind <kind>', 'screen = 다섯 화면 짝 칸')
|
|
510
|
+
.action((id: string, title: string, opts: { owner?: string; kind?: string }, cmd: Command) => {
|
|
509
511
|
const { version, json } = context(cmd);
|
|
510
|
-
const data = addItem(version, { id, title, ...(opts.owner !== undefined ? { owner: opts.owner } : {}) });
|
|
512
|
+
const data = addItem(version, { id, title, ...(opts.owner !== undefined ? { owner: opts.owner } : {}), ...(opts.kind !== undefined ? { kind: opts.kind as ChecklistKind } : {}) });
|
|
511
513
|
if (json) console.log(JSON.stringify(data)); else console.log(`✅ ${id} 추가`);
|
|
512
514
|
});
|
|
513
515
|
withContext(checklist.command('set <id>').description('칸 상태·근거·담당·처분 갱신'))
|
|
514
516
|
.option('--status <status>', 'green|yellow|red|done').option('--evidence <evidence>', '근거').option('--owner <owner>', '담당')
|
|
515
|
-
.option('--disposition <disposition>', 'move|known-issue|block')
|
|
516
|
-
.action((id: string, opts: { status?: string; evidence?: string; owner?: string; disposition?: string }, cmd: Command) => {
|
|
517
|
+
.option('--disposition <disposition>', 'move|known-issue|block').option('--kind <kind>', 'screen = 다섯 화면 짝 칸')
|
|
518
|
+
.action((id: string, opts: { status?: string; evidence?: string; owner?: string; disposition?: string; kind?: string }, cmd: Command) => {
|
|
517
519
|
const { version, json } = context(cmd);
|
|
518
520
|
if (opts.status !== undefined && !['green', 'yellow', 'red', 'done'].includes(opts.status)) throw new Error(`잘못된 상태: ${opts.status}`);
|
|
519
521
|
if (opts.disposition !== undefined && !['move', 'known-issue', 'block'].includes(opts.disposition)) throw new Error(`잘못된 처분: ${opts.disposition}`);
|
|
520
|
-
if (opts.status === undefined && opts.evidence === undefined && opts.owner === undefined && opts.disposition === undefined) throw new Error('갱신할 칸을 지정하라');
|
|
521
|
-
const data = setItem(version, id, { ...(opts.status !== undefined ? { status: opts.status as ChecklistStatus } : {}), ...(opts.evidence !== undefined ? { evidence: opts.evidence } : {}), ...(opts.owner !== undefined ? { owner: opts.owner } : {}), ...(opts.disposition !== undefined ? { disposition: opts.disposition as ChecklistDisposition } : {}) }, process.env.ELANOUS_TRACK || 'cli');
|
|
522
|
+
if (opts.status === undefined && opts.evidence === undefined && opts.owner === undefined && opts.disposition === undefined && opts.kind === undefined) throw new Error('갱신할 칸을 지정하라');
|
|
523
|
+
const data = setItem(version, id, { ...(opts.status !== undefined ? { status: opts.status as ChecklistStatus } : {}), ...(opts.evidence !== undefined ? { evidence: opts.evidence } : {}), ...(opts.owner !== undefined ? { owner: opts.owner } : {}), ...(opts.disposition !== undefined ? { disposition: opts.disposition as ChecklistDisposition } : {}), ...(opts.kind !== undefined ? { kind: opts.kind as ChecklistKind } : {}) }, process.env.ELANOUS_TRACK || 'cli');
|
|
522
524
|
if (json) console.log(JSON.stringify(data)); else console.log(`✅ ${id} 갱신`);
|
|
523
525
|
});
|
|
524
526
|
withContext(checklist.command('rm <id>').description('칸 삭제')).action((id: string, _opts: unknown, cmd: Command) => {
|
|
@@ -532,6 +534,45 @@ export function registerReleaseCommands(program: Command, releaseRunDeps: Unatte
|
|
|
532
534
|
const data = seedFromRoadmap(version, readFileSync(opts.from, 'utf8'));
|
|
533
535
|
if (json) console.log(JSON.stringify(data)); else console.log(`✅ ${version} ${data.items.length}칸`);
|
|
534
536
|
});
|
|
537
|
+
const actor = () => process.env.ELANOUS_TRACK || 'cli';
|
|
538
|
+
withContext(checklist.command('move <id>').description('칸을 한 트랜잭션으로 다른 판에 옮긴다'))
|
|
539
|
+
.requiredOption('--from <v>', '현재 판').requiredOption('--to <v>', '새 판')
|
|
540
|
+
.action((id: string, opts: { from: string; to: string }, cmd: Command) => {
|
|
541
|
+
const { codenames, json } = context(cmd);
|
|
542
|
+
const from = checklistVersion(opts.from, codenames), to = checklistVersion(opts.to, codenames);
|
|
543
|
+
const data = features.move(id, from, to, actor());
|
|
544
|
+
if (json) console.log(JSON.stringify(data)); else console.log(`✅ ${id} ${from} → ${to}`);
|
|
545
|
+
});
|
|
546
|
+
withContext(checklist.command('retitle <id> <title>').description('칸 제목 수정')).action((id: string, title: string, _opts: unknown, cmd: Command) => {
|
|
547
|
+
const { version, json } = context(cmd);
|
|
548
|
+
const snapshot = listChecklist(version);
|
|
549
|
+
features.retitle(id, title, actor(), snapshot.released, snapshot.dev);
|
|
550
|
+
if (json) console.log(JSON.stringify(listChecklist(version))); else console.log(`✅ ${id} 제목 수정`);
|
|
551
|
+
});
|
|
552
|
+
const evidence = checklist.command('evidence').description('칸의 PR·커밋 근거');
|
|
553
|
+
withContext(evidence.command('add <id> <ref>').description('근거 참조 추가')).action((id: string, ref: string, _opts: unknown, cmd: Command) => {
|
|
554
|
+
const { version, json } = context(cmd);
|
|
555
|
+
const snapshot = listChecklist(version);
|
|
556
|
+
features.evidenceAdd(id, version, ref, actor(), snapshot.released, snapshot.dev);
|
|
557
|
+
if (json) console.log(JSON.stringify(features.history(id))); else console.log(`✅ ${id} 근거 ${ref}`);
|
|
558
|
+
});
|
|
559
|
+
withContext(checklist.command('history <id>').description('칸의 판 이동·변경 이력')).action((id: string, _opts: unknown, cmd: Command) => {
|
|
560
|
+
const { json } = context(cmd);
|
|
561
|
+
const rows = features.history(id);
|
|
562
|
+
if (json) { console.log(JSON.stringify(rows)); return; }
|
|
563
|
+
const info = features.details(id);
|
|
564
|
+
if (info) {
|
|
565
|
+
console.log(`${info.id} · ${info.title} · 담당 ${info.owner ?? '-'} · 종류 ${info.kind ?? '-'} · 처음 ${info.createdAt}`);
|
|
566
|
+
for (const ev of info.evidence) console.log(` 근거 ${ev.version} ${ev.ref} (${ev.at} ${ev.by})`);
|
|
567
|
+
}
|
|
568
|
+
for (const row of rows) console.log(`${row.at} ${row.version} ${row.by} ${row.field}: ${JSON.stringify(row.from)} → ${JSON.stringify(row.to)}`);
|
|
569
|
+
});
|
|
570
|
+
withContext(checklist.command('export').description('SQLite 에서 checklist.json 스냅샷 생성')).action((_opts: unknown, cmd: Command) => {
|
|
571
|
+
const { version, json } = context(cmd);
|
|
572
|
+
const snapshot = listChecklist(version);
|
|
573
|
+
const data = features.exportJson(version, snapshot.released, snapshot.dev);
|
|
574
|
+
if (json) console.log(JSON.stringify(data)); else console.log(`✅ ${version} checklist.json 내보냄`);
|
|
575
|
+
});
|
|
535
576
|
release.command('prepare')
|
|
536
577
|
.description('깨끗한 원본 → 공개본 → 공개 저장소 이력 위 커밋 → PWA → 묶음·체크섬 → 로컬 끝까지 (네트워크 쓰기 없음)')
|
|
537
578
|
.requiredOption('--version <x.y.z>', 'package.json 과 같은 버전(먼저 버전 PR 을 착지)')
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
// SK2 — `elanous skills repair`: 색인이 못 읽은 스킬을 보여 주고, 확인([y/N])한 것만 그 자리에서 고친다.
|
|
2
|
+
import type { Command } from 'commander';
|
|
3
|
+
import { createInterface } from 'node:readline/promises';
|
|
4
|
+
import { getSkillIndex, resetSkillIndex, skillIndexProblems } from '../skills/index.js';
|
|
5
|
+
import { applySkillRepair, planSkillRepair, skillRepairNotice } from '../skills/repair.js';
|
|
6
|
+
import { CONNECT_AGENTS, connectPrompt, connectSkillRoot, planConnect, withConnectedSource, withoutSources, type ConnectAgent } from '../skills/connect.js';
|
|
7
|
+
import { debug } from '../debug/log.js';
|
|
8
|
+
import { resolve } from 'node:path';
|
|
9
|
+
import { claudePluginPrompt, isClaudePluginDir, mcpEnableHint, planClaudePlugin, readClaudePlugin } from '../skills/connect-claude-plugin.js';
|
|
10
|
+
|
|
11
|
+
async function ask(question: string, defaultYes = false): Promise<boolean> {
|
|
12
|
+
if (!process.stdin.isTTY) return false;
|
|
13
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
14
|
+
try {
|
|
15
|
+
const answer = (await rl.question(question)).trim();
|
|
16
|
+
return answer === '' ? defaultYes : /^(y|yes|예|네)$/i.test(answer);
|
|
17
|
+
}
|
|
18
|
+
finally { rl.close(); }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** EN9 — `elanous connect <claude|codex>`: find the skills that agent already uses and register their folder (no copy). */
|
|
22
|
+
export function registerConnectCommand(program: Command): void {
|
|
23
|
+
program.command('connect <agent>')
|
|
24
|
+
.description(`Use the skills you already have in another agent (${CONNECT_AGENTS.join(' | ')}) or a Claude Code plugin folder — registers in place, never copies; plugin MCP servers are added switched off`)
|
|
25
|
+
.option('--yes', 'Register without asking')
|
|
26
|
+
.option('--json', 'Print the plan as JSON and change nothing')
|
|
27
|
+
.option('--undo', 'Stop using that folder (the files stay where they are)')
|
|
28
|
+
.action(async (agentArg: string, opts: { yes?: boolean; json?: boolean; undo?: boolean }) => {
|
|
29
|
+
const agent = agentArg.toLowerCase() as ConnectAgent;
|
|
30
|
+
if (!CONNECT_AGENTS.includes(agent) && isClaudePluginDir(resolve(agentArg))) {
|
|
31
|
+
await connectClaudePlugin(resolve(agentArg), opts);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (!CONNECT_AGENTS.includes(agent)) {
|
|
35
|
+
console.error(`지원하는 에이전트: ${CONNECT_AGENTS.join(', ')} · 또는 Claude 플러그인 폴더(.claude-plugin/plugin.json) (받은 값: ${agentArg})`);
|
|
36
|
+
process.exitCode = 2;
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const { defaultSkillDirs, getUserConfig, saveUserConfig } = await import('../user-config.js');
|
|
40
|
+
const cfg = getUserConfig();
|
|
41
|
+
if (opts.undo) {
|
|
42
|
+
const root = connectSkillRoot(agent);
|
|
43
|
+
const sources = withoutSources(cfg.skills.sources, [root]);
|
|
44
|
+
if (sources.length === (cfg.skills.sources ?? []).length) { console.log(`연결된 적이 없습니다(${root}).`); return; }
|
|
45
|
+
saveUserConfig({ ...cfg, skills: { ...cfg.skills, sources } });
|
|
46
|
+
resetSkillIndex();
|
|
47
|
+
debug.log('skills.connect', 'undone', { agent });
|
|
48
|
+
console.log(`연결을 끊었습니다. 파일은 그대로 있습니다(${root}).`);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const plan = planConnect(agent, defaultSkillDirs(cfg));
|
|
52
|
+
debug.log('skills.connect', 'plan', { agent, state: plan.state, count: 'skills' in plan ? plan.skills.length : 0 });
|
|
53
|
+
if (opts.json) { console.log(JSON.stringify(plan, null, 2)); return; }
|
|
54
|
+
const prompt = connectPrompt(plan);
|
|
55
|
+
if (plan.state !== 'new') { console.log(prompt); return; }
|
|
56
|
+
if (!opts.yes) {
|
|
57
|
+
if (!process.stdin.isTTY) { console.log(`${prompt.split('\n')[0]}\n가져오려면: elanous connect ${agent} --yes`); return; }
|
|
58
|
+
if (!(await ask(prompt, true))) { console.log('가져오지 않았습니다. 나중에: elanous connect ' + agent); return; }
|
|
59
|
+
} else {
|
|
60
|
+
console.log(prompt.split('\n')[0]);
|
|
61
|
+
}
|
|
62
|
+
const next = { ...cfg, skills: { ...cfg.skills, sources: withConnectedSource(cfg.skills.sources, plan.root, agent) } };
|
|
63
|
+
saveUserConfig(next);
|
|
64
|
+
resetSkillIndex();
|
|
65
|
+
const index = getSkillIndex(defaultSkillDirs(next));
|
|
66
|
+
const problems = skillIndexProblems().filter((p) => p.dir === plan.root);
|
|
67
|
+
const usable = plan.skills.length - problems.length;
|
|
68
|
+
debug.log('skills.connect', 'registered', { agent, found: plan.skills.length, problems: problems.length, indexed: index.length });
|
|
69
|
+
console.log(`연결했습니다 — ${usable}개를 바로 쓸 수 있습니다. 끊으려면: elanous connect ${agent} --undo`);
|
|
70
|
+
const notice = skillRepairNotice(problems);
|
|
71
|
+
if (notice) console.log(notice);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function registerSkillsCommands(program: Command): void {
|
|
76
|
+
const skills = program.command('skills').description('Check and repair installed skills');
|
|
77
|
+
skills.command('sources')
|
|
78
|
+
.description('List every skill folder elanous reads, in order (preset · shared · connected · package)')
|
|
79
|
+
.option('--json', 'JSON output')
|
|
80
|
+
.action(async (opts: { json?: boolean }) => {
|
|
81
|
+
const { resolveSkillSources } = await import('../user-config.js');
|
|
82
|
+
const { existsSync } = await import('node:fs');
|
|
83
|
+
const sources = resolveSkillSources().map((source) => ({ ...source, exists: existsSync(source.path) }));
|
|
84
|
+
debug.log('skills.sources', 'listed', { count: sources.length, kinds: sources.map((source) => source.kind) });
|
|
85
|
+
if (opts.json) { console.log(JSON.stringify(sources, null, 2)); return; }
|
|
86
|
+
const width = Math.max(10, ...sources.map((source) => source.id.length));
|
|
87
|
+
for (const source of sources) {
|
|
88
|
+
console.log(`${source.enabled ? '●' : '○'} ${source.id.padEnd(width)} ${source.kind.padEnd(9)} ${source.path}${source.exists ? '' : ' (없음)'}`);
|
|
89
|
+
}
|
|
90
|
+
console.log('묶음 스킬(패키지 skills/)은 늘 마지막에 읽습니다. 다른 에이전트 스킬 더하기: elanous connect claude|codex|<플러그인 폴더>');
|
|
91
|
+
});
|
|
92
|
+
skills.command('repair')
|
|
93
|
+
.description('List skills that could not be read and repair the ones that can be fixed (asks first)')
|
|
94
|
+
.option('--yes', 'Repair every fixable skill without asking')
|
|
95
|
+
.action(async (opts: { yes?: boolean }) => {
|
|
96
|
+
getSkillIndex();
|
|
97
|
+
const problems = skillIndexProblems();
|
|
98
|
+
if (!problems.length) { console.log('모든 스킬을 읽었습니다. 고칠 것이 없습니다.'); return; }
|
|
99
|
+
console.log(`스킬 ${problems.length}개를 읽지 못했습니다.`);
|
|
100
|
+
// ER1 — operations hears about it too (consented · scrubbed).
|
|
101
|
+
try {
|
|
102
|
+
const { reportError } = await import('../error-report/report.js');
|
|
103
|
+
for (const p of problems.slice(0, 3)) await reportError({ code: 'skill-index', message: `${p.name}: ${p.code ?? ''} ${p.error}`.trim(), surface: 'cli' });
|
|
104
|
+
} catch { /* reporting is advisory */ }
|
|
105
|
+
let fixed = 0;
|
|
106
|
+
for (const problem of problems) {
|
|
107
|
+
const plan = planSkillRepair(problem);
|
|
108
|
+
console.log(`\n· ${problem.name} — ${problem.error.split('\n')[0]}`);
|
|
109
|
+
if (plan.kind === 'permission' || plan.kind === 'manual') { console.log(` ${plan.hint}`); continue; }
|
|
110
|
+
const what = plan.kind === 'fixable-permission' ? '읽기 권한을 켭니다' : `${plan.keys.join(', ')} 를 한 줄로 고칩니다(원본은 .bak 으로 남김)`;
|
|
111
|
+
if (!opts.yes && !(await ask(` ${what}. 고칠까요? [y/N] `))) { console.log(' 건너뜀'); continue; }
|
|
112
|
+
const result = applySkillRepair(plan);
|
|
113
|
+
if (result.ok) { fixed += 1; console.log(` 고쳤습니다 — ${result.path}`); }
|
|
114
|
+
else console.log(` 고치지 못했습니다(${result.reason}) — 원래대로 되돌렸습니다.`);
|
|
115
|
+
}
|
|
116
|
+
console.log(`\n${fixed}개를 고쳤습니다.`);
|
|
117
|
+
if (fixed < problems.length) process.exitCode = 1;
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** EN8 — a Claude Code plugin folder: skills join `skills.sources[]` (kind connected) in place, `.mcp.json` servers join `mcp.servers[]` switched off. */
|
|
122
|
+
async function connectClaudePlugin(dir: string, opts: { yes?: boolean; json?: boolean; undo?: boolean }): Promise<void> {
|
|
123
|
+
const { defaultSkillDirs, getUserConfig, saveUserConfig } = await import('../user-config.js');
|
|
124
|
+
const cfg = getUserConfig();
|
|
125
|
+
let plugin;
|
|
126
|
+
try { plugin = readClaudePlugin(dir); }
|
|
127
|
+
catch (error) {
|
|
128
|
+
console.error(`플러그인 정보를 읽지 못했습니다(.claude-plugin/plugin.json): ${error instanceof Error ? error.message : String(error)}`);
|
|
129
|
+
process.exitCode = 1;
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
const rawMcp = (cfg.raw.mcp && typeof cfg.raw.mcp === 'object' && !Array.isArray(cfg.raw.mcp) ? cfg.raw.mcp : {}) as Record<string, unknown>;
|
|
133
|
+
const rawServers = (Array.isArray(rawMcp.servers) ? rawMcp.servers : []) as Array<Record<string, unknown>>;
|
|
134
|
+
const writeServers = (servers: Array<Record<string, unknown>>) => ({ ...cfg.raw, mcp: { ...rawMcp, servers } });
|
|
135
|
+
if (opts.undo) {
|
|
136
|
+
const ids = new Set(plugin.mcp.map((server) => server.id));
|
|
137
|
+
const sources = withoutSources(cfg.skills.sources, plugin.skillRoots);
|
|
138
|
+
const servers = rawServers.filter((row) => !ids.has(row.id as string));
|
|
139
|
+
if (sources.length === (cfg.skills.sources ?? []).length && servers.length === rawServers.length) { console.log(`연결된 적이 없습니다(${plugin.name}).`); return; }
|
|
140
|
+
saveUserConfig({ ...cfg, raw: writeServers(servers), skills: { ...cfg.skills, sources } });
|
|
141
|
+
resetSkillIndex();
|
|
142
|
+
debug.log('skills.connect', 'undone', { plugin: plugin.name });
|
|
143
|
+
console.log(`«${plugin.name}» 연결을 끊었습니다. 파일은 그대로 있습니다(${plugin.root}).`);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
const plan = planClaudePlugin(plugin, defaultSkillDirs(cfg), rawServers.map((row) => row.id as string));
|
|
147
|
+
debug.log('skills.connect', 'plan', { plugin: plugin.name, state: plan.state, skills: plugin.skills.length, mcp: plugin.mcp.length, skipped: plugin.skipped.length });
|
|
148
|
+
if (opts.json) { console.log(JSON.stringify(plan, null, 2)); return; }
|
|
149
|
+
const prompt = claudePluginPrompt(plan);
|
|
150
|
+
if (plan.state !== 'new') { console.log(prompt); return; }
|
|
151
|
+
const firstLines = prompt.slice(0, prompt.lastIndexOf('\n'));
|
|
152
|
+
if (!opts.yes) {
|
|
153
|
+
if (!process.stdin.isTTY) { console.log(`${firstLines}\n가져오려면: elanous connect ${plugin.root} --yes`); return; }
|
|
154
|
+
if (!(await ask(prompt, true))) { console.log(`가져오지 않았습니다. 나중에: elanous connect ${plugin.root}`); return; }
|
|
155
|
+
} else {
|
|
156
|
+
console.log(firstLines);
|
|
157
|
+
}
|
|
158
|
+
const sources = plan.newSkillRoots.reduce((list, root, i) => withConnectedSource(list, root, `plugin:${plugin.name}${i ? `:${i + 1}` : ''}`), cfg.skills.sources ?? []);
|
|
159
|
+
const servers = [...rawServers, ...plan.newMcp];
|
|
160
|
+
saveUserConfig({ ...cfg, raw: writeServers(servers), skills: { ...cfg.skills, sources } });
|
|
161
|
+
resetSkillIndex();
|
|
162
|
+
const next = { ...cfg, skills: { ...cfg.skills, sources } };
|
|
163
|
+
getSkillIndex(defaultSkillDirs(next));
|
|
164
|
+
const problems = skillIndexProblems().filter((p) => plugin.skillRoots.includes(p.dir));
|
|
165
|
+
debug.log('skills.connect', 'registered', { plugin: plugin.name, skills: plugin.skills.length, problems: problems.length, mcp: plan.newMcp.length });
|
|
166
|
+
console.log(`연결했습니다 — 스킬 ${plugin.skills.length - problems.length}개를 바로 쓸 수 있습니다. 끊으려면: elanous connect ${plugin.root} --undo`);
|
|
167
|
+
const notice = skillRepairNotice(problems);
|
|
168
|
+
if (notice) console.log(notice);
|
|
169
|
+
if (plan.newMcp.length) {
|
|
170
|
+
console.log(`MCP 서버 ${plan.newMcp.length}개를 꺼진 채로 등록했습니다. 믿을 수 있는 것만 켜 주세요(켠 뒤 데몬 재시작):`);
|
|
171
|
+
for (const line of mcpEnableHint(plan.newMcp, servers)) console.log(line);
|
|
172
|
+
}
|
|
173
|
+
}
|
package/src/cli/start-cli.ts
CHANGED
|
@@ -5,6 +5,8 @@ import type { DetectedProvider } from '../llm/provider-detect.js';
|
|
|
5
5
|
import { resolveUsableLlm, type UsableLlm } from '../llm/usable-llm.js';
|
|
6
6
|
import { resolveDaemonEndpoint, type DaemonEndpoint, type ResolveDaemonEndpointOpts } from '../nexus/daemon-endpoint.js';
|
|
7
7
|
import { runBgLaunch } from './bg-launch.js';
|
|
8
|
+
import { browserUnavailableReason } from '../oauth/browser-availability.js';
|
|
9
|
+
import { debug } from '../debug/log.js';
|
|
8
10
|
|
|
9
11
|
export interface StartOptions {
|
|
10
12
|
gui?: boolean;
|
|
@@ -63,6 +65,8 @@ export interface StartDeps {
|
|
|
63
65
|
openTui?: (json: boolean) => Promise<number>;
|
|
64
66
|
sleep?: (ms: number) => Promise<void>;
|
|
65
67
|
output?: (line: string) => void;
|
|
68
|
+
browserEnv?: NodeJS.ProcessEnv;
|
|
69
|
+
browserPlatform?: NodeJS.Platform;
|
|
66
70
|
/** Test seam — daemon address. `null` means this universe has no daemon (do not guess a port).
|
|
67
71
|
* Called again after a launch so the opened address is the daemon that was just started. */
|
|
68
72
|
resolveEndpoint?: (opts?: ResolveDaemonEndpointOpts) => DaemonEndpoint | null;
|
|
@@ -193,10 +197,15 @@ export async function runStart(options: StartOptions = {}, deps: StartDeps = {})
|
|
|
193
197
|
if (!openUrl) {
|
|
194
198
|
record('open-gui', 'failed', 'Daemon address unknown');
|
|
195
199
|
} else {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
+
const why = browserUnavailableReason(deps.browserEnv ?? process.env, deps.browserPlatform ?? process.platform);
|
|
201
|
+
if (why) {
|
|
202
|
+
debug.log('browser.open', 'skipped', { reason: why.includes('ssh') ? 'ssh' : 'no-display' });
|
|
203
|
+
} else {
|
|
204
|
+
try {
|
|
205
|
+
if (await (deps.openGui ?? defaultOpenGui)(openUrl)) surface = 'gui';
|
|
206
|
+
} catch { /* A browser is optional; retain the URL for manual opening. */ }
|
|
207
|
+
}
|
|
208
|
+
record('open-gui', surface === 'gui' ? 'done' : why ? 'skipped' : 'failed', surface === 'gui' ? `Opened ${openUrl}` : `Open manually: ${openUrl}${why ? ` — ${why}` : ''}`);
|
|
200
209
|
}
|
|
201
210
|
record('open-tui', 'skipped', 'GUI selected');
|
|
202
211
|
} else {
|
package/src/codex/setup.ts
CHANGED
|
@@ -82,12 +82,14 @@ export async function pickCodexAuthMode(io: WizardIO, deps: CodexAuthModeDeps =
|
|
|
82
82
|
if (existing || codexCli) {
|
|
83
83
|
if (existing) {
|
|
84
84
|
const exp = existing.tokens.expiresAt;
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
:
|
|
88
|
-
|
|
85
|
+
const left = exp != null ? exp - Date.now() : null;
|
|
86
|
+
const when = left == null ? '만료 시각 모름'
|
|
87
|
+
: left <= 0 ? '만료됨'
|
|
88
|
+
: left >= 86_400_000 ? `약 ${Math.round(left / 86_400_000)}일 남음`
|
|
89
|
+
: left >= 3_600_000 ? `약 ${Math.round(left / 3_600_000)}시간 남음` : `약 ${Math.max(1, Math.round(left / 60_000))}분 남음`;
|
|
90
|
+
io.print(` 이미 로그인돼 있습니다(${when}).`);
|
|
89
91
|
} else {
|
|
90
|
-
io.print('
|
|
92
|
+
io.print(' Codex CLI 에 있는 ChatGPT/Codex 로그인을 찾았습니다(~/.codex/auth.json).');
|
|
91
93
|
}
|
|
92
94
|
// Sprint 11 — chooseFrom Yes/No so fullScreenIO renders the arrow
|
|
93
95
|
// picker (with 'y'/'n' + 한글 자모 quick-pick from PR #986 + #988)
|
|
@@ -96,10 +98,10 @@ export async function pickCodexAuthMode(io: WizardIO, deps: CodexAuthModeDeps =
|
|
|
96
98
|
// rotation on an unnecessary refresh).
|
|
97
99
|
const keep = await chooseFrom<boolean>(
|
|
98
100
|
io,
|
|
99
|
-
'
|
|
101
|
+
'지금 로그인을 그대로 쓸까요?',
|
|
100
102
|
[
|
|
101
|
-
{ key: 'y', label: '
|
|
102
|
-
{ key: 'n', label: '
|
|
103
|
+
{ key: 'y', label: '예 — 그대로 씁니다', value: true },
|
|
104
|
+
{ key: 'n', label: '아니오 — 다시 로그인합니다', value: false },
|
|
103
105
|
],
|
|
104
106
|
{ defaultIndex: 0 },
|
|
105
107
|
undefined,
|
|
@@ -265,18 +267,18 @@ async function runCodexSetupImpl(
|
|
|
265
267
|
let apiKey: string | undefined = initial.llm.apiKey;
|
|
266
268
|
|
|
267
269
|
if (authMode === 'oauth') {
|
|
268
|
-
io.print('
|
|
270
|
+
io.print(' 코드로 로그인합니다…');
|
|
269
271
|
try {
|
|
270
272
|
await loginWithCodex({
|
|
271
273
|
onProgress: (p) => {
|
|
272
274
|
if (p.type === 'user_code') {
|
|
273
275
|
io.print('');
|
|
274
|
-
io.print(` 1)
|
|
275
|
-
io.print(` 2)
|
|
276
|
+
io.print(` 1) 아무 브라우저에서 열기: ${p.loginUrl}`);
|
|
277
|
+
io.print(` 2) 코드 입력: ${p.userCode}`);
|
|
276
278
|
io.print('');
|
|
277
|
-
io.print('
|
|
279
|
+
io.print(' 로그인을 기다리는 중…');
|
|
278
280
|
}
|
|
279
|
-
if (p.type === 'saved') io.print('
|
|
281
|
+
if (p.type === 'saved') io.print(' 로그인했습니다. 저장했습니다.');
|
|
280
282
|
},
|
|
281
283
|
fetchImpl: opts.fetchImpl,
|
|
282
284
|
sleepImpl: opts.sleepImpl,
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export interface DashboardStatusLinesInput {
|
|
2
|
+
readonly provider?: string | null;
|
|
3
|
+
readonly model?: string | null;
|
|
4
|
+
readonly reasoning?: string | null;
|
|
5
|
+
readonly daemonAddress?: string | null;
|
|
6
|
+
readonly daemonConnected: boolean;
|
|
7
|
+
readonly accountName?: string | null;
|
|
8
|
+
readonly sessionSurfaceLines: readonly string[];
|
|
9
|
+
readonly view: string;
|
|
10
|
+
readonly focus: string;
|
|
11
|
+
readonly cwd: string;
|
|
12
|
+
readonly preview: string;
|
|
13
|
+
readonly starterClosed: string;
|
|
14
|
+
readonly chatOnly: string;
|
|
15
|
+
readonly acp: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function buildDashboardStatusLines(input: DashboardStatusLinesInput): string[] {
|
|
19
|
+
const known = (value?: string | null): string => value?.trim() || '모름';
|
|
20
|
+
// A remote URL can contain credentials or query parameters. Neither belongs in /status.
|
|
21
|
+
let address = '모름';
|
|
22
|
+
if (input.daemonAddress?.trim()) {
|
|
23
|
+
try {
|
|
24
|
+
const url = new URL(input.daemonAddress);
|
|
25
|
+
address = `${url.protocol}//${url.host}`;
|
|
26
|
+
} catch {
|
|
27
|
+
address = '모름';
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return [
|
|
31
|
+
`모델: ${known(input.provider)}/${known(input.model)} (${known(input.reasoning)})`,
|
|
32
|
+
`연결: 데몬 ${address} ${input.daemonConnected ? '붙음' : '안 붙음'}`,
|
|
33
|
+
`계정: ${input.accountName?.trim() === 'default' ? '기본' : known(input.accountName)}`,
|
|
34
|
+
'',
|
|
35
|
+
'자세히',
|
|
36
|
+
...input.sessionSurfaceLines.map(line => ` ${line}`),
|
|
37
|
+
` view: ${input.view}`,
|
|
38
|
+
` focus: ${input.focus}`,
|
|
39
|
+
` cwd: ${input.cwd}`,
|
|
40
|
+
` preview: ${input.preview}`,
|
|
41
|
+
` starterClosed: ${input.starterClosed}`,
|
|
42
|
+
` chatOnly: ${input.chatOnly}`,
|
|
43
|
+
` acp: ${input.acp}`,
|
|
44
|
+
];
|
|
45
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export interface FirstScreenBandInput {
|
|
2
|
+
readonly width: number;
|
|
3
|
+
readonly daemon: boolean;
|
|
4
|
+
readonly pwa?: {
|
|
5
|
+
readonly tailnet?: string | null;
|
|
6
|
+
readonly lan?: string | null;
|
|
7
|
+
readonly loopback?: string | null;
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** A startup transcript band; never includes credentials or a partial address. */
|
|
12
|
+
export function buildFirstScreenBand(input: FirstScreenBandInput): string[] {
|
|
13
|
+
const width = Number.isFinite(input.width) ? Math.max(0, Math.floor(input.width)) : 0;
|
|
14
|
+
const fit = (text: string): string => text.slice(0, width);
|
|
15
|
+
const address = [input.pwa?.tailnet, input.pwa?.lan, input.pwa?.loopback]
|
|
16
|
+
.map((candidate) => {
|
|
17
|
+
if (!candidate) return null;
|
|
18
|
+
try {
|
|
19
|
+
const url = new URL(candidate);
|
|
20
|
+
if (!['http:', 'https:'].includes(url.protocol) || !url.hostname) return null;
|
|
21
|
+
return `${url.origin}${url.pathname === '/app/' ? '/app/' : ''}`;
|
|
22
|
+
} catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
})
|
|
26
|
+
.find((candidate) => candidate !== null);
|
|
27
|
+
const fallback = input.pwa === undefined ? 'PWA: not checked' : 'PWA: unavailable';
|
|
28
|
+
const addressLine = address ? `PWA: ${address}` : fallback;
|
|
29
|
+
|
|
30
|
+
return [
|
|
31
|
+
fit(`elanous | daemon: ${input.daemon ? 'online' : 'offline'}`),
|
|
32
|
+
addressLine.length <= width ? addressLine : fit(address ? 'PWA: address available' : fallback),
|
|
33
|
+
fit('Type a message to begin | /help for commands'),
|
|
34
|
+
];
|
|
35
|
+
}
|