elanous 0.2.7 → 0.2.8
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/chunks/4836-3084c58c1725a40b.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/8894-7317998f6cb97d8c.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-9aa47b0bc6d570ff.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-f4d2ecbbae6088d6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/board/page-2053e49930c82748.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/botlab/{page-624f7a2f89b60c8f.js → page-d4b6df905f185055.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/{page-5a5fb385a4df0ffe.js → page-a70ba27c33003dd0.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/chat/page-186fc6952f027acc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/consult/{page-7457f52683562161.js → page-18166f885d831d3e.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/control/page-1c9d6cfe68b121c2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-cbbe03c0da7b081a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/editor/page-c108ff1fdb665c97.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/exec/{page-b7dcf5d48a854b76.js → page-147a328753d983e2.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/field/{page-51480757e7d68b92.js → page-799395a9df7d95aa.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/intake/page-6a8158b71d1fe9d7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/layout-9567560270857531.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/live/{page-30533117beb53625.js → page-e7ed3d010153d818.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/market/page-c0c0d19d51342d3e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-07267f1391b96aba.js → page-2e002ad8faa43291.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/morning/{page-55d50e9b0006a91d.js → page-b672e02e4bec8deb.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-1ba4a5d857276334.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-9db2226f38c542fb.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/{page-a7e37af7d0399cb8.js → page-70ee65f5f7a151dc.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-0bb8e43203efb23b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-092e1bcf27d751cc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/sessions/{page-15e5a15db84c56aa.js → page-eb52580c4b75b905.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-a2e572e4511f2a16.js → page-7464c1987dd76e6d.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/{page-dd107bf1c7d52a90.js → page-e573437b0a1c728f.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-294300625e49e4d3.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/page-085847b088675978.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/share/page-9fe9d38c36999eb4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-d8ab4d1489223459.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-6141f67f531d8b6a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/term/page-295a02b1026b8255.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/trace/page-1eb9a0e1e054209d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/vault/{page-56adbe1a1f1379bd.js → page-82efa177a30f3741.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-d0be51cec4f62606.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-2e367075ffdbc062.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workspace/{page-5d422f38c052967e.js → page-647ed2b66c1d49f1.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-ba058c56e38f2792.js +1 -0
- package/apps/pwa/out/_next/static/css/5ef72a618a8994f3.css +1 -0
- package/apps/pwa/out/approvals/index.html +1 -1
- package/apps/pwa/out/approvals/index.txt +13 -13
- package/apps/pwa/out/autopilot/index.html +1 -1
- package/apps/pwa/out/autopilot/index.txt +13 -13
- package/apps/pwa/out/board/index.html +1 -1
- package/apps/pwa/out/board/index.txt +13 -13
- package/apps/pwa/out/botlab/index.html +1 -1
- package/apps/pwa/out/botlab/index.txt +13 -13
- package/apps/pwa/out/bots/index.html +1 -1
- package/apps/pwa/out/bots/index.txt +13 -13
- package/apps/pwa/out/chat/index.html +1 -1
- package/apps/pwa/out/chat/index.txt +13 -13
- package/apps/pwa/out/consult/index.html +1 -1
- package/apps/pwa/out/consult/index.txt +13 -13
- package/apps/pwa/out/control/index.html +1 -1
- package/apps/pwa/out/control/index.txt +13 -13
- package/apps/pwa/out/design-check/index.html +1 -1
- package/apps/pwa/out/design-check/index.txt +13 -13
- package/apps/pwa/out/editor/index.html +1 -1
- package/apps/pwa/out/editor/index.txt +13 -13
- package/apps/pwa/out/exec/index.html +1 -1
- package/apps/pwa/out/exec/index.txt +13 -13
- package/apps/pwa/out/field/index.html +1 -1
- package/apps/pwa/out/field/index.txt +13 -13
- package/apps/pwa/out/healthz +1 -1
- package/apps/pwa/out/index.html +1 -1
- package/apps/pwa/out/index.txt +13 -13
- package/apps/pwa/out/intake/index.html +1 -1
- package/apps/pwa/out/intake/index.txt +13 -13
- package/apps/pwa/out/live/index.html +1 -1
- package/apps/pwa/out/live/index.txt +13 -13
- package/apps/pwa/out/market/index.html +1 -1
- package/apps/pwa/out/market/index.txt +13 -13
- package/apps/pwa/out/missions/placeholder/index.html +1 -1
- package/apps/pwa/out/missions/placeholder/index.txt +13 -13
- package/apps/pwa/out/morning/index.html +1 -1
- package/apps/pwa/out/morning/index.txt +13 -13
- package/apps/pwa/out/observatory/index.html +1 -1
- package/apps/pwa/out/observatory/index.txt +13 -13
- package/apps/pwa/out/reflection/index.html +1 -1
- package/apps/pwa/out/reflection/index.txt +14 -14
- package/apps/pwa/out/scheduler/index.html +1 -1
- package/apps/pwa/out/scheduler/index.txt +13 -13
- package/apps/pwa/out/sessions/index.html +1 -1
- package/apps/pwa/out/sessions/index.txt +13 -13
- package/apps/pwa/out/settings/devices/index.html +1 -1
- package/apps/pwa/out/settings/devices/index.txt +13 -13
- package/apps/pwa/out/settings/index.html +1 -1
- package/apps/pwa/out/settings/index.txt +13 -13
- package/apps/pwa/out/setup/done/index.html +1 -1
- package/apps/pwa/out/setup/done/index.txt +13 -13
- package/apps/pwa/out/setup/index.html +1 -1
- package/apps/pwa/out/setup/index.txt +13 -13
- package/apps/pwa/out/share/index.html +1 -1
- package/apps/pwa/out/share/index.txt +13 -13
- package/apps/pwa/out/showroom/index.html +1 -1
- package/apps/pwa/out/showroom/index.txt +13 -13
- package/apps/pwa/out/tasks/index.html +1 -1
- package/apps/pwa/out/tasks/index.txt +13 -13
- package/apps/pwa/out/term/index.html +1 -1
- package/apps/pwa/out/term/index.txt +13 -13
- package/apps/pwa/out/trace/index.html +1 -1
- package/apps/pwa/out/trace/index.txt +13 -13
- package/apps/pwa/out/vault/index.html +1 -1
- package/apps/pwa/out/vault/index.txt +13 -13
- package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
- package/apps/pwa/out/workflows/chat-ui/index.txt +13 -13
- package/apps/pwa/out/workflows/index.html +1 -1
- package/apps/pwa/out/workflows/index.txt +13 -13
- package/apps/pwa/out/workspace/index.html +1 -1
- package/apps/pwa/out/workspace/index.txt +13 -13
- package/apps/pwa/out/worktrees/index.html +1 -1
- package/apps/pwa/out/worktrees/index.txt +13 -13
- package/graphs/test-diet/recipes.yaml +9 -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/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/gate-node.ts +27 -3
- 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 +88 -0
- package/scripts/test-diet/node.ts +102 -0
- package/src/acp/auth-methods.ts +30 -0
- package/src/acp/server.ts +5 -5
- package/src/channel-import/detect.ts +191 -0
- package/src/channel-import/run.ts +111 -0
- 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/persona-cli.ts +116 -0
- package/src/cli/release-cli.ts +8 -7
- 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/index.ts +17 -1
- package/src/dashboard/slash-runtime/dashboard-handlers.ts +2 -2
- package/src/dashboard/slash-runtime/help-from-registry.ts +4 -2
- 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 +82 -7
- 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/nexus/api/consult-requests.ts +19 -8
- package/src/nexus/api/error-reports.ts +108 -0
- package/src/nexus/api/http-server.ts +21 -1
- package/src/nexus/api/outbound-report.ts +7 -0
- package/src/nexus/api/running-runs-background.ts +68 -0
- package/src/nexus/api/running-runs-child.ts +6 -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/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/web-first.ts +8 -1
- package/src/onboarding.ts +66 -12
- package/src/persona/presets.ts +105 -0
- package/src/release-loop/checklist.ts +28 -5
- 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/trigger-extract.ts +1 -1
- package/src/task-orchestrator/surfaces/pod-command-job.ts +16 -8
- package/src/telegram-commands.ts +13 -0
- package/src/telegram.ts +34 -0
- package/src/user-config.ts +111 -13
- 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/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/chat/page-80497e93a878ddc5.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/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/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/tasks/page-c1c95739c0bff3b8.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/trace/page-71d18eec33b9c667.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-2c4e59079ee357ac.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 → lywc8m_HDzH9MKSDQhyEj}/_buildManifest.js +0 -0
- /package/apps/pwa/out/_next/static/{keajW7DDyghEs1y8oJe6T → lywc8m_HDzH9MKSDQhyEj}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { closeSync, existsSync, lstatSync, mkdtempSync, mkdirSync, openSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { homedir, platform, arch } from 'node:os';
|
|
4
|
+
import { join, resolve, sep } from 'node:path';
|
|
5
|
+
import { getElanousConfigDir } from '../elanous-config-dir.js';
|
|
6
|
+
import { resolveCurrentInstance } from '../instance/current.js';
|
|
7
|
+
import { LogStore, logsDbPath, type LogStoreRow } from '../mss/logging/log-store.js';
|
|
8
|
+
import { cliVersion, codeRevision, packageVersion } from '../version/code-revision.js';
|
|
9
|
+
import { runDoctor, defaultReadInstallPrefix, type DoctorOptions, type DoctorReport } from './doctor-cli.js';
|
|
10
|
+
|
|
11
|
+
const REDACTED = '<redacted>';
|
|
12
|
+
|
|
13
|
+
/** Recursively redact named secrets and secret-shaped text before serialization. */
|
|
14
|
+
export function redactForBundle(value: unknown, home: string = homedir(), redactNamedFields: boolean = true): unknown {
|
|
15
|
+
if (typeof value === 'string') {
|
|
16
|
+
let text = value;
|
|
17
|
+
if (home && home !== sep) {
|
|
18
|
+
// Match the home as a path component, including JSON-escaped slashes.
|
|
19
|
+
const escaped = home.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
20
|
+
text = text.replace(new RegExp(`${escaped}(?=[/\\\\]|$)`, 'g'), '~');
|
|
21
|
+
}
|
|
22
|
+
// By kind, not by the last example found: headers that carry credentials, credentials inside URLs,
|
|
23
|
+
// known token shapes, and any `name=value` / `"name": "value"` whose name says it is a credential.
|
|
24
|
+
return text
|
|
25
|
+
.replace(/\b((?:Proxy-)?Authorization\s*:\s*)[^\r\n"']+/gi, `$1${REDACTED}`)
|
|
26
|
+
.replace(/\b((?:Set-)?Cookie\s*:\s*)[^\r\n"']+/gi, `$1${REDACTED}`)
|
|
27
|
+
.replace(/\b([a-z][a-z0-9+.-]*:\/\/)[^\s/@"'<>]+@/gi, `$1${REDACTED}@`)
|
|
28
|
+
.replace(/(["']?(?:[\w-]*(?:token|key|secret|password|passwd|pwd|cookie|session|credential|auth))["']?\s*[:=]\s*["']?)[^\s"',}&]+/gi, `$1${REDACTED}`)
|
|
29
|
+
.replace(/\b(Bearer|Basic|Token)\s+[A-Za-z0-9._~+/=-]{8,}/g, `$1 ${REDACTED}`)
|
|
30
|
+
.replace(/\b(?:sk-[A-Za-z0-9_-]+|gh[pousr]_[A-Za-z0-9_]+|github_pat_[A-Za-z0-9_]+|glpat-[A-Za-z0-9_-]+|xox[a-z]-[A-Za-z0-9-]+|AKIA[0-9A-Z]{16}|AIza[0-9A-Za-z_-]{35})\b/g, REDACTED)
|
|
31
|
+
.replace(/\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g, REDACTED);
|
|
32
|
+
}
|
|
33
|
+
if (Array.isArray(value)) return value.map((entry) => redactForBundle(entry, home, redactNamedFields));
|
|
34
|
+
if (value && typeof value === 'object') {
|
|
35
|
+
const result: Record<string, unknown> = Object.create(null);
|
|
36
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
37
|
+
const safeKey = redactForBundle(key, home) as string;
|
|
38
|
+
result[safeKey] = redactNamedFields && /(?:token|key|secret|password|passwd|pwd|authorization|cookie|session|credential|auth)/i.test(key)
|
|
39
|
+
? REDACTED : redactForBundle(entry, home, redactNamedFields);
|
|
40
|
+
}
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface DoctorBundleOptions {
|
|
47
|
+
outDir: string;
|
|
48
|
+
now?: Date;
|
|
49
|
+
/** Read-only doctor seams for isolated tests. */
|
|
50
|
+
doctorOptions?: DoctorOptions;
|
|
51
|
+
home?: string;
|
|
52
|
+
configDir?: string;
|
|
53
|
+
logDbPath?: string;
|
|
54
|
+
doctorReport?: DoctorReport;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface DoctorBundleResult { path: string; files: string[]; bytes: number }
|
|
58
|
+
|
|
59
|
+
/** Bundle only generated, redacted entries. Never put an unprocessed source file into tar. */
|
|
60
|
+
export function buildDoctorBundle({ outDir, now = new Date(), doctorOptions = {}, home = homedir(), configDir = getElanousConfigDir(), logDbPath = logsDbPath(), doctorReport }: DoctorBundleOptions): DoctorBundleResult {
|
|
61
|
+
const instant = now.toISOString();
|
|
62
|
+
const stamp = instant.replace(/[-:]/g, '').slice(0, 15).replace('T', '-');
|
|
63
|
+
mkdirSync(outDir, { recursive: true, mode: 0o700 });
|
|
64
|
+
const destination = resolve(outDir, `elanous-diagnostics-${stamp}.tar.gz`);
|
|
65
|
+
if (existsSync(destination)) throw new Error('diagnostics archive already exists');
|
|
66
|
+
const work = mkdtempSync(join(outDir, '.elanous-diagnostics-'));
|
|
67
|
+
const files: string[] = [];
|
|
68
|
+
const put = (name: string, data: unknown): void => {
|
|
69
|
+
writeFileSync(join(work, name), JSON.stringify(redactForBundle(data, home, name === 'config.redacted.json' || name === 'onboarding.json'), null, 2) + '\n', { mode: 0o600 });
|
|
70
|
+
files.push(name);
|
|
71
|
+
};
|
|
72
|
+
try {
|
|
73
|
+
const report = doctorReport ?? runDoctor(doctorOptions);
|
|
74
|
+
const universe = resolveCurrentInstance({ cwd: () => process.cwd() });
|
|
75
|
+
const packageRoot = resolve(import.meta.dir, '../..');
|
|
76
|
+
const installPrefix = doctorOptions.readInstallPrefix?.() ?? defaultReadInstallPrefix(packageRoot, existsSync);
|
|
77
|
+
let logs: LogStoreRow[] = [];
|
|
78
|
+
let logStatus: 'ok' | 'no-store' = 'no-store';
|
|
79
|
+
if (existsSync(logDbPath)) {
|
|
80
|
+
try {
|
|
81
|
+
const store = LogStore.openReadOnly(logDbPath);
|
|
82
|
+
try {
|
|
83
|
+
logs = store.query({ sinceMs: now.getTime() - 86_400_000, untilMs: now.getTime(), limit: 500 });
|
|
84
|
+
logStatus = 'ok';
|
|
85
|
+
} finally { store.close(); }
|
|
86
|
+
} catch { /* Unreadable store: export no unverified log bytes. */ }
|
|
87
|
+
}
|
|
88
|
+
put('summary.json', {
|
|
89
|
+
version: packageVersion(), sha: codeRevision() ?? 'unknown', cliVersion: cliVersion(),
|
|
90
|
+
os: platform(), arch: arch(), node: process.version, bun: typeof Bun === 'undefined' ? null : Bun.version,
|
|
91
|
+
installPath: installPrefix ?? packageRoot,
|
|
92
|
+
universe: { kind: universe.kind, root: universe.root }, createdAt: instant, logs: logStatus,
|
|
93
|
+
});
|
|
94
|
+
put('doctor.json', report);
|
|
95
|
+
let config: unknown = {};
|
|
96
|
+
const configPath = doctorOptions.configPath ?? join(configDir, 'config.json');
|
|
97
|
+
try {
|
|
98
|
+
if (lstatSync(configPath).isFile()) config = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
99
|
+
} catch { config = { status: 'unavailable' }; }
|
|
100
|
+
put('config.redacted.json', config);
|
|
101
|
+
put('credentials.json', report.credentials.map(({ name, resolved, source }) => ({ name, present: resolved, source })));
|
|
102
|
+
writeFileSync(join(work, 'logs-recent.jsonl'), logs.map((row) => {
|
|
103
|
+
const entry = row as { data: string | null };
|
|
104
|
+
let data = entry.data;
|
|
105
|
+
if (data) {
|
|
106
|
+
try { data = JSON.stringify(redactForBundle(JSON.parse(data), home)); }
|
|
107
|
+
catch { /* Non-JSON log bodies are redacted as plain text below. */ }
|
|
108
|
+
}
|
|
109
|
+
return JSON.stringify(redactForBundle({ ...row, data }, home, false));
|
|
110
|
+
}).join('\n') + (logs.length ? '\n' : ''), { mode: 0o600 });
|
|
111
|
+
files.push('logs-recent.jsonl');
|
|
112
|
+
const onboardingPath = join(configDir, 'onboarding.json');
|
|
113
|
+
try {
|
|
114
|
+
if (lstatSync(onboardingPath).isFile()) put('onboarding.json', JSON.parse(readFileSync(onboardingPath, 'utf8')));
|
|
115
|
+
} catch { /* An absent or unreadable optional record is not exported. */ }
|
|
116
|
+
const archiveFd = openSync(destination, 'wx', 0o600);
|
|
117
|
+
let tarSucceeded = false;
|
|
118
|
+
try {
|
|
119
|
+
const tar = spawnSync('tar', ['-czf', '-', '-C', work, ...files], { stdio: ['ignore', archiveFd, 'pipe'], timeout: 30_000 });
|
|
120
|
+
tarSucceeded = !tar.error && tar.status === 0;
|
|
121
|
+
} finally {
|
|
122
|
+
closeSync(archiveFd);
|
|
123
|
+
if (!tarSucceeded) rmSync(destination, { force: true });
|
|
124
|
+
}
|
|
125
|
+
if (!tarSucceeded) throw new Error('could not create diagnostics archive');
|
|
126
|
+
return { path: destination, files, bytes: statSync(destination).size };
|
|
127
|
+
} finally { rmSync(work, { recursive: true, force: true }); }
|
|
128
|
+
}
|
package/src/cli/doctor-cli.ts
CHANGED
|
@@ -35,6 +35,9 @@ import { gitInstallPlan } from './git-install-plan.js';
|
|
|
35
35
|
import { detectDistroFamily, parseOsRelease } from './doctor-distro.js';
|
|
36
36
|
import { checkPythonEnv } from './python-cli.js';
|
|
37
37
|
import { describeModes, formatModeLine, type DoctorMode } from './doctor-modes.js';
|
|
38
|
+
import { formatDoctorLocalLlm, probeDoctorLocalLlm, type DoctorLocalLlm } from './doctor-local-llm.js';
|
|
39
|
+
import type { LlmInventory } from '../llm/local-manager/types.js';
|
|
40
|
+
import { buildDoctorBundle } from './doctor-bundle.js';
|
|
38
41
|
|
|
39
42
|
function defaultCheckPythonEnv(): { status: 'ok' | 'fixable' | 'manual'; evidence: string; remedy?: string } {
|
|
40
43
|
const c = checkPythonEnv(false);
|
|
@@ -102,6 +105,8 @@ export interface DoctorReport {
|
|
|
102
105
|
capabilitySummary?: DoctorCapabilitySummary;
|
|
103
106
|
/** Read-only configuration and chat-use status. Present on a successful report. */
|
|
104
107
|
modes?: DoctorMode[];
|
|
108
|
+
/** Local LLM inventory; CLI probes separately so synchronous credential reports stay read-only and unchanged. */
|
|
109
|
+
localLlm?: DoctorLocalLlm;
|
|
105
110
|
/** F1 readiness. Present on a successful report. Absent when the report itself failed. */
|
|
106
111
|
readiness?: ReadinessReport;
|
|
107
112
|
reason?: string;
|
|
@@ -186,6 +191,8 @@ export interface DoctorCliDeps extends DoctorOptions, Pick<DoctorFixDeps, 'home'
|
|
|
186
191
|
resolveAdviceProvider?: (config: UserConfig) => { provider: string; model: string; auth: string };
|
|
187
192
|
confirmAdvice?: (message: string) => Promise<boolean>;
|
|
188
193
|
adviceInteractive?: boolean;
|
|
194
|
+
/** Local-manager inventory seam; default calls getInventory(). */
|
|
195
|
+
localLlmInventory?: () => Promise<LlmInventory>;
|
|
189
196
|
}
|
|
190
197
|
|
|
191
198
|
const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../..');
|
|
@@ -1141,6 +1148,7 @@ export function formatDoctorReport(report: DoctorReport, options: { credentials?
|
|
|
1141
1148
|
...(report.externalCommandsCatalogUnavailable ? [`External commands catalog unavailable: ${report.externalCommandsCatalogReason ?? 'catalog/external-commands.yaml is unavailable.'}`] : []),
|
|
1142
1149
|
...formatCapabilitySummary(capabilitySummary),
|
|
1143
1150
|
...(report.modes ? ['모드:', ...report.modes.map((row) => ` ${formatModeLine(row)}`)] : []),
|
|
1151
|
+
...(report.localLlm ? formatDoctorLocalLlm(report.localLlm) : []),
|
|
1144
1152
|
].join('\n');
|
|
1145
1153
|
}
|
|
1146
1154
|
|
|
@@ -1208,12 +1216,37 @@ export function registerDoctorCommand(program: Command, deps: DoctorCliDeps = {}
|
|
|
1208
1216
|
.description('Reports whether credentials resolve and where each resolution comes from')
|
|
1209
1217
|
.option('--json', 'structured output')
|
|
1210
1218
|
.option('--credentials', 'show per-credential details in human-readable output')
|
|
1219
|
+
.option('--bundle', 'write a redacted diagnostics archive for support')
|
|
1220
|
+
.option('--out <dir>', 'output directory for --bundle')
|
|
1211
1221
|
.option('--fix', 'show repairs; git installation asks on a TTY (other repairs require --yes)')
|
|
1212
1222
|
.option('--advise', 'ask the configured LLM to rank catalog repairs (read-only advice)')
|
|
1213
1223
|
.option('--yes', 'apply planned doctor repairs (requires --fix)')
|
|
1214
1224
|
.option('--sudo', 'also run the planned sudo install lines — only where `sudo -n true` works (requires --fix --yes)')
|
|
1215
1225
|
.option('--restart', 'restart the nexus service when it runs a different version than this installed copy, then verify it (requires --fix --yes · interrupts bots, terminals and running turns)')
|
|
1216
|
-
.action(async (opts: { json?: boolean; credentials?: boolean; fix?: boolean; advise?: boolean; yes?: boolean; sudo?: boolean; restart?: boolean }) => {
|
|
1226
|
+
.action(async (opts: { json?: boolean; credentials?: boolean; bundle?: boolean; out?: string; fix?: boolean; advise?: boolean; yes?: boolean; sudo?: boolean; restart?: boolean }) => {
|
|
1227
|
+
if (opts.bundle) {
|
|
1228
|
+
if (opts.fix || opts.advise || opts.yes || opts.sudo || opts.restart) {
|
|
1229
|
+
err.error('--bundle cannot be combined with repair or advice options');
|
|
1230
|
+
setExitCode(1);
|
|
1231
|
+
return;
|
|
1232
|
+
}
|
|
1233
|
+
try {
|
|
1234
|
+
// Same report as `doctor --json`, local LLM section included, so support sees what the user saw.
|
|
1235
|
+
const doctorReport = runDoctor(deps);
|
|
1236
|
+
if (doctorReport.ok) doctorReport.localLlm = await probeDoctorLocalLlm(deps.localLlmInventory);
|
|
1237
|
+
const result = buildDoctorBundle({ outDir: opts.out ?? process.cwd(), doctorOptions: deps, doctorReport });
|
|
1238
|
+
out.log(opts.json ? JSON.stringify(result) : [`Diagnostics: ${result.path} (${result.bytes} bytes)`, 'Contents:', ...result.files.map((file) => ` ${file}`)].join('\n'));
|
|
1239
|
+
} catch {
|
|
1240
|
+
err.error('Could not create diagnostics archive.');
|
|
1241
|
+
setExitCode(1);
|
|
1242
|
+
}
|
|
1243
|
+
return;
|
|
1244
|
+
}
|
|
1245
|
+
if (opts.out !== undefined) {
|
|
1246
|
+
err.error('--out requires --bundle');
|
|
1247
|
+
setExitCode(1);
|
|
1248
|
+
return;
|
|
1249
|
+
}
|
|
1217
1250
|
if (opts.yes && !opts.fix) {
|
|
1218
1251
|
err.error('--yes requires --fix');
|
|
1219
1252
|
setExitCode(1);
|
|
@@ -1240,6 +1273,7 @@ export function registerDoctorCommand(program: Command, deps: DoctorCliDeps = {}
|
|
|
1240
1273
|
setExitCode(1);
|
|
1241
1274
|
return;
|
|
1242
1275
|
}
|
|
1276
|
+
report.localLlm = await probeDoctorLocalLlm(deps.localLlmInventory);
|
|
1243
1277
|
if (!opts.fix && !opts.advise) {
|
|
1244
1278
|
out.log(opts.json ? JSON.stringify(report, null, 2) : formatDoctorReport(report, { credentials: opts.credentials }));
|
|
1245
1279
|
if (report.requiredMissing?.length) setExitCode(1);
|
|
@@ -1358,6 +1392,7 @@ export function registerDoctorCommand(program: Command, deps: DoctorCliDeps = {}
|
|
|
1358
1392
|
: undefined;
|
|
1359
1393
|
// PATH cannot change in this process; re-probe readiness rather than treating a saved block as PATH=ok.
|
|
1360
1394
|
const currentReport = results || gitInstall?.ran ? runDoctor(deps) : report;
|
|
1395
|
+
if (currentReport.ok) currentReport.localLlm = report.localLlm;
|
|
1361
1396
|
if (gitInstall?.ran && gitInstall.ok && currentReport.requiredMissing?.includes('git')) {
|
|
1362
1397
|
gitInstall.detail = 'git init 성공 · 현재 doctor PATH 에서 git 을 찾지 못했습니다. 새 셸에서 다시 확인하세요.';
|
|
1363
1398
|
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { debug } from '../debug/log.js';
|
|
2
|
+
import { probeDocker } from '../llm/local-manager/docker-probe.js';
|
|
3
|
+
import { probeLmstudio } from '../llm/local-manager/lmstudio-probe.js';
|
|
4
|
+
import { probeMlx } from '../llm/local-manager/mlx-probe.js';
|
|
5
|
+
import { listNodes } from '../llm/local-manager/node-registry.js';
|
|
6
|
+
import { probeOllama } from '../llm/local-manager/ollama-probe.js';
|
|
7
|
+
import type { LlmInventory, LlmModel, LlmNode, LlmRuntime } from '../llm/local-manager/types.js';
|
|
8
|
+
|
|
9
|
+
export interface DoctorLocalLlm {
|
|
10
|
+
nodes: { kind: 'ollama' | 'lmstudio' | 'mlx' | 'docker'; baseUrl: string; reachable: boolean; models: string[] }[];
|
|
11
|
+
timedOut: boolean;
|
|
12
|
+
/** Inventory lookup failed; absence of servers was not established. */
|
|
13
|
+
failed?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const kinds = ['ollama', 'lmstudio', 'mlx', 'docker'] as const;
|
|
17
|
+
type Kind = (typeof kinds)[number];
|
|
18
|
+
|
|
19
|
+
const baseUrlFields: Record<Kind, 'ollamaBaseUrl' | 'lmstudioBaseUrl' | 'mlxBaseUrl' | 'dockerBaseUrl'> = {
|
|
20
|
+
ollama: 'ollamaBaseUrl', lmstudio: 'lmstudioBaseUrl', mlx: 'mlxBaseUrl', docker: 'dockerBaseUrl',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type Probe = (node: { id: string; isLocal: boolean }, deps: { timeoutMs?: number }) => Promise<{ reachable: boolean; models: readonly LlmModel[]; warnings: readonly string[] }>;
|
|
24
|
+
|
|
25
|
+
/** Doctor shows only this machine, so it probes only the local node — the full inventory also probes every remote
|
|
26
|
+
* fleet node over ssh and took ~7 s cold, past the 3 s cap («시간 초과» with LM Studio running · 10-01). */
|
|
27
|
+
export async function localInventory(deps: {
|
|
28
|
+
nodes?: () => readonly LlmNode[];
|
|
29
|
+
probes?: Record<'lmstudio' | 'ollama' | 'mlx' | 'docker', Probe>;
|
|
30
|
+
now?: () => number;
|
|
31
|
+
} = {}): Promise<LlmInventory> {
|
|
32
|
+
const probes = deps.probes ?? { lmstudio: probeLmstudio, ollama: probeOllama, mlx: probeMlx, docker: probeDocker } as Record<'lmstudio' | 'ollama' | 'mlx' | 'docker', Probe>;
|
|
33
|
+
const read = deps.nodes ?? listNodes;
|
|
34
|
+
const nodes: LlmNode[] = [];
|
|
35
|
+
const models: LlmModel[] = [];
|
|
36
|
+
const warnings: string[] = [];
|
|
37
|
+
for (const node of read().filter((n) => n.isLocal)) {
|
|
38
|
+
const seed = { id: node.id, isLocal: true };
|
|
39
|
+
const order = ['lmstudio', 'ollama', 'mlx', 'docker'] as const;
|
|
40
|
+
const results = await Promise.all(order.map((kind) => probes[kind](seed, { timeoutMs: 2500 })));
|
|
41
|
+
const runtimes: LlmRuntime[] = order.filter((_, i) => results[i]!.reachable);
|
|
42
|
+
for (const [i, r] of results.entries()) { models.push(...r.models); for (const w of r.warnings) warnings.push(`${node.id}:${order[i]}:${w}`); }
|
|
43
|
+
// Probes record base URLs in the registry on success — read the node back for them.
|
|
44
|
+
const fresh = read().find((n) => n.id === node.id) ?? node;
|
|
45
|
+
nodes.push({ ...fresh, reachable: runtimes.length > 0, runtimes });
|
|
46
|
+
}
|
|
47
|
+
return { nodes, models, at: (deps.now ?? Date.now)(), cached: false, warnings };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function probeDoctorLocalLlm(inventory: () => Promise<LlmInventory> = () => localInventory(), timeoutMs = 3000): Promise<DoctorLocalLlm> {
|
|
51
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
52
|
+
let result: DoctorLocalLlm;
|
|
53
|
+
try {
|
|
54
|
+
const snapshot = await Promise.race([
|
|
55
|
+
Promise.resolve().then(inventory),
|
|
56
|
+
new Promise<null>((resolve) => { timer = setTimeout(() => resolve(null), timeoutMs); }),
|
|
57
|
+
]);
|
|
58
|
+
result = snapshot === null
|
|
59
|
+
? { nodes: [], timedOut: true }
|
|
60
|
+
: {
|
|
61
|
+
nodes: snapshot.nodes.filter((node) => node.isLocal).flatMap((node) => kinds.flatMap((kind) => {
|
|
62
|
+
const baseUrl = node[baseUrlFields[kind]];
|
|
63
|
+
if (!node.runtimes.includes(kind) && !baseUrl) return [];
|
|
64
|
+
return [{
|
|
65
|
+
kind,
|
|
66
|
+
baseUrl: baseUrl ?? '',
|
|
67
|
+
reachable: node.reachable === true && node.runtimes.includes(kind),
|
|
68
|
+
models: snapshot.models.filter((model) => model.nodeId === node.id && model.runtime === kind).map((model) => model.id),
|
|
69
|
+
}];
|
|
70
|
+
})),
|
|
71
|
+
timedOut: false,
|
|
72
|
+
};
|
|
73
|
+
} catch {
|
|
74
|
+
result = { nodes: [], timedOut: false, failed: true };
|
|
75
|
+
} finally {
|
|
76
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
77
|
+
}
|
|
78
|
+
debug.log('doctor.local-llm', 'probed', {
|
|
79
|
+
nodes: result.nodes.length,
|
|
80
|
+
reachable: result.nodes.filter((node) => node.reachable).length,
|
|
81
|
+
models: result.nodes.reduce((total, node) => total + node.models.length, 0),
|
|
82
|
+
timedOut: result.timedOut,
|
|
83
|
+
});
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function formatDoctorLocalLlm(result: DoctorLocalLlm): string[] {
|
|
88
|
+
if (result.timedOut) return ['Local LLM:', ' 시간 초과'];
|
|
89
|
+
if (result.failed) return ['Local LLM:', ' 로컬 LLM 인벤토리를 조회하지 못했다'];
|
|
90
|
+
if (result.nodes.length === 0) return ['Local LLM:', ' 로컬 LLM 서버를 찾지 못했다 — Ollama(:11434) · LM Studio(:1234) 를 띄우면 여기 보인다'];
|
|
91
|
+
return [
|
|
92
|
+
'Local LLM:',
|
|
93
|
+
...result.nodes.map((node) => ` ${node.kind} · ${node.baseUrl} · ${node.reachable ? '닿음' : '안 닿음'} · 모델 ${node.models.length}개${node.models.length ? ` · ${node.models.slice(0, 5).join(', ')}` : ''}`),
|
|
94
|
+
];
|
|
95
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// EN4 — `elanous import`: one read-only plan over the agents you already use. Applying stays with the sub-commands each item names.
|
|
2
|
+
import type { Command } from 'commander';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { debug } from '../debug/log.js';
|
|
5
|
+
import { formatImportPlan, planImport, type ImportPlan } from '../import/plan.js';
|
|
6
|
+
import { applyImport, formatApplied, IMPORT_MODES, undoImport, type ImportMode } from '../import/apply.js';
|
|
7
|
+
import { dirname, join } from 'node:path';
|
|
8
|
+
|
|
9
|
+
export async function buildImportPlan(home = homedir()): Promise<ImportPlan> {
|
|
10
|
+
const { defaultSkillDirs, getUserConfig } = await import('../user-config.js');
|
|
11
|
+
const cfg = getUserConfig();
|
|
12
|
+
const plan = planImport({
|
|
13
|
+
home,
|
|
14
|
+
...(process.env.CODEX_HOME ? { codexHome: process.env.CODEX_HOME } : {}),
|
|
15
|
+
activeSkillDirs: defaultSkillDirs(cfg),
|
|
16
|
+
mcpServerIds: (cfg.mcp?.servers ?? []).map((server) => server.id),
|
|
17
|
+
});
|
|
18
|
+
for (const item of plan.items) debug.log('import.plan', 'item', { source: item.source, kind: item.kind, status: item.status });
|
|
19
|
+
debug.log('import.plan', 'planned', { sources: plan.sources.filter((s) => s.detected).map((s) => s.id), items: plan.items.length });
|
|
20
|
+
return plan;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function registerImportCommand(program: Command): void {
|
|
24
|
+
program.command('import')
|
|
25
|
+
.description('Show what elanous can bring over from Claude Code, Codex, ~/.agents, OpenClaw and Hermes — a plan first; `--apply` registers skills in place and adds MCP switched off, `--undo` puts everything back')
|
|
26
|
+
.option('--json', 'Print the plan (or the apply result) as JSON')
|
|
27
|
+
.option('--apply', 'Apply the plan: skills in place (ref) or as link/copy · MCP switched off · secrets and hooks only listed')
|
|
28
|
+
.option('--mode <mode>', `How skills come over: ${IMPORT_MODES.join('|')} (default ref — nothing copied)`)
|
|
29
|
+
.option('--pick <ids>', 'Only these item ids (comma-separated, see --json)')
|
|
30
|
+
.option('--undo', 'Undo the last --apply: restore the config bytes and remove links/copies')
|
|
31
|
+
.action(async (opts: { json?: boolean; apply?: boolean; mode?: string; pick?: string; undo?: boolean }) => {
|
|
32
|
+
const { userConfigPath } = await import('../user-config.js');
|
|
33
|
+
const importRoot = join(dirname(userConfigPath()), 'import');
|
|
34
|
+
if (opts.undo) {
|
|
35
|
+
const undone = undoImport(importRoot);
|
|
36
|
+
const { resetSkillIndex } = await import('../skills/index.js');
|
|
37
|
+
resetSkillIndex();
|
|
38
|
+
debug.log('import.apply', 'undone', { found: undone !== null, created: undone?.created.length ?? 0 });
|
|
39
|
+
if (opts.json) console.log(JSON.stringify({ undone: undone !== null }));
|
|
40
|
+
else console.log(undone ? `되돌렸습니다 — 설정을 ${undone.at} 이전으로 돌리고, 만든 것 ${undone.created.length}개를 지웠습니다.` : '되돌릴 가져오기가 없습니다.');
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const plan = await buildImportPlan();
|
|
44
|
+
if (!opts.apply) { console.log(opts.json ? JSON.stringify(plan, null, 2) : formatImportPlan(plan)); return; }
|
|
45
|
+
if (opts.mode !== undefined && !IMPORT_MODES.includes(opts.mode as ImportMode)) throw new Error(`--mode 는 ${IMPORT_MODES.join('|')} 중 하나다(받은 값: ${opts.mode})`);
|
|
46
|
+
const { getUserConfig, saveUserConfig } = await import('../user-config.js');
|
|
47
|
+
const cfg = getUserConfig();
|
|
48
|
+
const rawMcp = (cfg.raw.mcp && typeof cfg.raw.mcp === 'object' && !Array.isArray(cfg.raw.mcp) ? cfg.raw.mcp : {}) as Record<string, unknown>;
|
|
49
|
+
const manifest = applyImport(plan, { ...(opts.mode ? { mode: opts.mode as ImportMode } : {}), ...(opts.pick ? { pick: opts.pick.split(',').map((id) => id.trim()).filter(Boolean) } : {}) }, {
|
|
50
|
+
home: homedir(), configPath: userConfigPath(), importRoot,
|
|
51
|
+
sources: cfg.skills.sources ?? [],
|
|
52
|
+
rawMcpServers: (Array.isArray(rawMcp.servers) ? rawMcp.servers : []) as Array<Record<string, unknown>>,
|
|
53
|
+
save: (sources, servers) => saveUserConfig({ ...cfg, raw: { ...cfg.raw, mcp: { ...rawMcp, servers } }, skills: { ...cfg.skills, sources } }),
|
|
54
|
+
});
|
|
55
|
+
const { resetSkillIndex } = await import('../skills/index.js');
|
|
56
|
+
resetSkillIndex();
|
|
57
|
+
debug.log('import.apply', 'applied', { mode: manifest.mode, applied: manifest.applied.length, kept: manifest.kept.length, created: manifest.created.length });
|
|
58
|
+
console.log(opts.json ? JSON.stringify({ mode: manifest.mode, applied: manifest.applied, kept: manifest.kept, created: manifest.created.length }, null, 2) : formatApplied(manifest));
|
|
59
|
+
});
|
|
60
|
+
}
|
package/src/cli/mcp-login.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
} from '../mcp/mcp-oauth.js';
|
|
13
13
|
import { parseWwwAuthenticate } from '../mcp/client.js';
|
|
14
14
|
import { debug } from '../debug/log.js';
|
|
15
|
+
import { browserUnavailableReason } from '../oauth/browser-availability.js';
|
|
15
16
|
|
|
16
17
|
const CALLBACK_PATH = '/oauth/callback';
|
|
17
18
|
const DEFAULT_TIMEOUT_MS = 120_000;
|
|
@@ -31,6 +32,8 @@ export interface McpLoginOpts {
|
|
|
31
32
|
readConfigFn?: () => { mcp?: { servers: McpServerSpec[] } };
|
|
32
33
|
fetch?: McpOAuthFetch;
|
|
33
34
|
openBrowser?: (url: string) => Promise<void>;
|
|
35
|
+
browserEnv?: NodeJS.ProcessEnv;
|
|
36
|
+
browserPlatform?: NodeJS.Platform;
|
|
34
37
|
createListener?: (handler: (req: IncomingMessage, res: ServerResponse) => void) => Server;
|
|
35
38
|
/** 발견한 issuer/tokenEndpoint 를 config 에 되쓰는 자리. 테스트 심. */
|
|
36
39
|
persistDiscoveryFn?: (input: PersistDiscoveryInput) => PersistDiscoveryResult;
|
|
@@ -91,10 +94,16 @@ export async function runMcpLogin(opts: McpLoginOpts): Promise<McpLoginResult> {
|
|
|
91
94
|
void callbackPromise.catch(() => undefined);
|
|
92
95
|
out.log(`Open this URL to authorize '${opts.serverId}':`);
|
|
93
96
|
out.log(authorization.request.url);
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
97
|
+
const why = browserUnavailableReason(opts.browserEnv ?? process.env, opts.browserPlatform ?? process.platform);
|
|
98
|
+
if (why) {
|
|
99
|
+
out.log(why);
|
|
100
|
+
debug.log('browser.open', 'skipped', { reason: why.includes('ssh') ? 'ssh' : 'no-display' });
|
|
101
|
+
} else {
|
|
102
|
+
try {
|
|
103
|
+
await (opts.openBrowser ?? openDefaultBrowser)(authorization.request.url);
|
|
104
|
+
} catch {
|
|
105
|
+
out.error('Could not open the default browser; open the URL above manually.');
|
|
106
|
+
}
|
|
98
107
|
}
|
|
99
108
|
const callback = await callbackPromise;
|
|
100
109
|
await exchangeAuthorizationCode(authorization.metadata, authorization.request, callback, {
|
|
@@ -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/release-cli.ts
CHANGED
|
@@ -23,7 +23,7 @@ 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
27
|
import { writeStdoutJson } from './stdout-json.js';
|
|
28
28
|
import { runUnattendedRelease, type UnattendedReleaseDeps } from '../../scripts/release-loop/unattended-release.js';
|
|
29
29
|
import { releaseReadiness } from '../../scripts/release-loop/release-readiness.js';
|
|
@@ -505,20 +505,21 @@ export function registerReleaseCommands(program: Command, releaseRunDeps: Unatte
|
|
|
505
505
|
const { version, codenames, json } = context(cmd);
|
|
506
506
|
await checklistOutput(version, codenames, 'status', json);
|
|
507
507
|
});
|
|
508
|
-
withContext(checklist.command('add <id> <title>').description('칸 추가')).option('--owner <owner>', '담당').
|
|
508
|
+
withContext(checklist.command('add <id> <title>').description('칸 추가')).option('--owner <owner>', '담당').option('--kind <kind>', 'screen = 다섯 화면 짝 칸')
|
|
509
|
+
.action((id: string, title: string, opts: { owner?: string; kind?: string }, cmd: Command) => {
|
|
509
510
|
const { version, json } = context(cmd);
|
|
510
|
-
const data = addItem(version, { id, title, ...(opts.owner !== undefined ? { owner: opts.owner } : {}) });
|
|
511
|
+
const data = addItem(version, { id, title, ...(opts.owner !== undefined ? { owner: opts.owner } : {}), ...(opts.kind !== undefined ? { kind: opts.kind as ChecklistKind } : {}) });
|
|
511
512
|
if (json) console.log(JSON.stringify(data)); else console.log(`✅ ${id} 추가`);
|
|
512
513
|
});
|
|
513
514
|
withContext(checklist.command('set <id>').description('칸 상태·근거·담당·처분 갱신'))
|
|
514
515
|
.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) => {
|
|
516
|
+
.option('--disposition <disposition>', 'move|known-issue|block').option('--kind <kind>', 'screen = 다섯 화면 짝 칸')
|
|
517
|
+
.action((id: string, opts: { status?: string; evidence?: string; owner?: string; disposition?: string; kind?: string }, cmd: Command) => {
|
|
517
518
|
const { version, json } = context(cmd);
|
|
518
519
|
if (opts.status !== undefined && !['green', 'yellow', 'red', 'done'].includes(opts.status)) throw new Error(`잘못된 상태: ${opts.status}`);
|
|
519
520
|
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');
|
|
521
|
+
if (opts.status === undefined && opts.evidence === undefined && opts.owner === undefined && opts.disposition === undefined && opts.kind === undefined) throw new Error('갱신할 칸을 지정하라');
|
|
522
|
+
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
523
|
if (json) console.log(JSON.stringify(data)); else console.log(`✅ ${id} 갱신`);
|
|
523
524
|
});
|
|
524
525
|
withContext(checklist.command('rm <id>').description('칸 삭제')).action((id: string, _opts: unknown, cmd: Command) => {
|