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
|
+
// EN7 — `elanous import --apply` / `--undo`: run the EN4 plan. Skills register in place (EN9 `withConnectedSource`, `ref` default)
|
|
2
|
+
// or as link/copy under the import folder; MCP joins `mcp.servers[]` switched off (EN8 `convertMcpEntry`); hooks, logins,
|
|
3
|
+
// secrets and chat-bot tokens are only listed — never copied, never printed. Undo restores the config bytes and removes what apply made.
|
|
4
|
+
import { cpSync, existsSync, mkdirSync, readFileSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { convertMcpEntry } from '../skills/connect-claude-plugin.js';
|
|
7
|
+
import { withConnectedSource } from '../skills/connect.js';
|
|
8
|
+
import type { SkillSource } from '../user-config.js';
|
|
9
|
+
import type { ImportItem, ImportPlan } from './plan.js';
|
|
10
|
+
|
|
11
|
+
export type ImportMode = 'ref' | 'link' | 'copy';
|
|
12
|
+
export const IMPORT_MODES: readonly ImportMode[] = ['ref', 'link', 'copy'];
|
|
13
|
+
|
|
14
|
+
export interface ImportManifest {
|
|
15
|
+
at: string;
|
|
16
|
+
mode: ImportMode;
|
|
17
|
+
configPath: string;
|
|
18
|
+
/** Config bytes before apply; null when there was no config file. */
|
|
19
|
+
backup: string | null;
|
|
20
|
+
/** Files and links apply created, removed on undo. */
|
|
21
|
+
created: string[];
|
|
22
|
+
applied: Array<{ id: string; how: string }>;
|
|
23
|
+
kept: Array<{ id: string; status: string; why: string }>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ApplyDeps {
|
|
27
|
+
home: string;
|
|
28
|
+
configPath: string;
|
|
29
|
+
/** Where links/copies, the manifest and the config backup live. */
|
|
30
|
+
importRoot: string;
|
|
31
|
+
sources: readonly SkillSource[];
|
|
32
|
+
rawMcpServers: ReadonlyArray<Record<string, unknown>>;
|
|
33
|
+
/** Persist the new skills sources and raw MCP server rows (the config writer). */
|
|
34
|
+
save: (sources: SkillSource[], rawServers: Array<Record<string, unknown>>) => void;
|
|
35
|
+
now?: () => Date;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const manifestPath = (importRoot: string) => join(importRoot, 'last-apply.json');
|
|
39
|
+
const expand = (path: string, home: string) => (path.startsWith('~') ? join(home, path.slice(1)) : path);
|
|
40
|
+
|
|
41
|
+
function readMcpSpec(item: ImportItem, home: string): unknown {
|
|
42
|
+
try {
|
|
43
|
+
const json = JSON.parse(readFileSync(expand(item.path, home), 'utf8')) as Record<string, unknown>;
|
|
44
|
+
const table = item.source === 'claude' ? json.mcpServers : (json.mcp as Record<string, unknown> | undefined)?.servers;
|
|
45
|
+
return (table as Record<string, unknown> | undefined)?.[item.name];
|
|
46
|
+
} catch { return undefined; }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function readImportManifest(importRoot: string): ImportManifest | null {
|
|
50
|
+
try { return JSON.parse(readFileSync(manifestPath(importRoot), 'utf8')) as ImportManifest; } catch { return null; }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function applyImport(plan: ImportPlan, opts: { mode?: ImportMode; pick?: readonly string[] }, deps: ApplyDeps): ImportManifest {
|
|
54
|
+
if (readImportManifest(deps.importRoot)) throw new Error('이미 적용한 가져오기가 있습니다 — 먼저 `elanous import --undo`');
|
|
55
|
+
const mode = opts.mode ?? 'ref';
|
|
56
|
+
const picked = opts.pick?.length ? new Set(opts.pick) : null;
|
|
57
|
+
const manifest: ImportManifest = {
|
|
58
|
+
at: (deps.now ?? (() => new Date()))().toISOString(), mode, configPath: deps.configPath,
|
|
59
|
+
backup: existsSync(deps.configPath) ? readFileSync(deps.configPath, 'utf8') : null, created: [], applied: [], kept: [],
|
|
60
|
+
};
|
|
61
|
+
let sources = [...deps.sources];
|
|
62
|
+
const rawServers = [...deps.rawMcpServers];
|
|
63
|
+
const keep = (item: ImportItem, why: string) => manifest.kept.push({ id: item.id, status: item.status, why });
|
|
64
|
+
mkdirSync(deps.importRoot, { recursive: true });
|
|
65
|
+
|
|
66
|
+
for (const item of plan.items) {
|
|
67
|
+
if (picked && !picked.has(item.id)) continue;
|
|
68
|
+
if (item.status === 'same') continue;
|
|
69
|
+
if (item.status !== 'new') {
|
|
70
|
+
keep(item, item.status === 'secret' || item.status === 'archived-only'
|
|
71
|
+
? `보관만 — 옮기지도 켜지도 않는다${item.next ? ` · 따로: \`${item.next}\`` : ''}`
|
|
72
|
+
: item.status === 'conflict' ? '같은 이름 · 다른 내용 — 그대로 둔다' : '형식을 아직 옮기지 않는다');
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (item.kind === 'skill') {
|
|
76
|
+
const skillDir = expand(item.path, deps.home);
|
|
77
|
+
if (mode === 'ref') {
|
|
78
|
+
const root = join(skillDir, '..');
|
|
79
|
+
sources = withConnectedSource(sources, root, item.source);
|
|
80
|
+
manifest.applied.push({ id: item.id, how: `ref ${item.path}` });
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const target = join(deps.importRoot, 'skills', item.source, item.name);
|
|
84
|
+
if (existsSync(target)) { keep(item, `이미 있다: ${target}`); continue; }
|
|
85
|
+
mkdirSync(join(target, '..'), { recursive: true });
|
|
86
|
+
if (mode === 'link') symlinkSync(skillDir, target, 'dir');
|
|
87
|
+
else cpSync(skillDir, target, { recursive: true, dereference: true });
|
|
88
|
+
manifest.created.push(target);
|
|
89
|
+
sources = withConnectedSource(sources, join(deps.importRoot, 'skills', item.source), `import:${item.source}`);
|
|
90
|
+
manifest.applied.push({ id: item.id, how: `${mode} ${item.path}` });
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (item.kind === 'mcp' && (item.source === 'claude' || item.source === 'openclaw')) {
|
|
94
|
+
const converted = convertMcpEntry(item.source, item.name, readMcpSpec(item, deps.home), '');
|
|
95
|
+
if ('skipped' in converted) { keep(item, converted.skipped); continue; }
|
|
96
|
+
if (rawServers.some((row) => row.id === item.name)) { keep(item, '같은 id 가 이미 있다'); continue; }
|
|
97
|
+
rawServers.push({ ...converted, id: item.name });
|
|
98
|
+
manifest.applied.push({ id: item.id, how: 'mcp 꺼진 채로' });
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
keep(item, '이 판에서는 옮기지 않는다');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (manifest.applied.length) deps.save(sources, rawServers);
|
|
105
|
+
writeFileSync(manifestPath(deps.importRoot), `${JSON.stringify(manifest, null, 2)}\n`, { mode: 0o600 });
|
|
106
|
+
return manifest;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Restore the config bytes and remove what apply created. Returns null when nothing was applied. */
|
|
110
|
+
export function undoImport(importRoot: string): ImportManifest | null {
|
|
111
|
+
const manifest = readImportManifest(importRoot);
|
|
112
|
+
if (!manifest) return null;
|
|
113
|
+
if (manifest.backup === null) rmSync(manifest.configPath, { force: true });
|
|
114
|
+
else writeFileSync(manifest.configPath, manifest.backup);
|
|
115
|
+
for (const path of manifest.created) rmSync(path, { recursive: true, force: true });
|
|
116
|
+
rmSync(manifestPath(importRoot), { force: true });
|
|
117
|
+
return manifest;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function formatApplied(m: ImportManifest): string {
|
|
121
|
+
const lines = [`가져왔습니다(${m.mode}) — ${m.applied.length}개 · 그대로 둔 것 ${m.kept.length}개. 되돌리려면: elanous import --undo`];
|
|
122
|
+
for (const a of m.applied.slice(0, 12)) lines.push(` + ${a.id} — ${a.how}`);
|
|
123
|
+
if (m.applied.length > 12) lines.push(` … 외 ${m.applied.length - 12}개`);
|
|
124
|
+
for (const k of m.kept.slice(0, 12)) lines.push(` · ${k.id} [${k.status}] — ${k.why}`);
|
|
125
|
+
if (m.kept.length > 12) lines.push(` … 외 ${m.kept.length - 12}개`);
|
|
126
|
+
if (m.applied.some((a) => a.how.startsWith('mcp'))) lines.push('MCP 서버는 꺼진 채로 넣었습니다. 믿을 수 있는 것만 켜 주세요(켠 뒤 데몬 재시작).');
|
|
127
|
+
return lines.join('\n');
|
|
128
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
// EN4 — `elanous import`: detect → plan on one screen, writes nothing (RESEARCH-entrances-for-codex-claude-openclaw-hermes-users §5a·§5b).
|
|
2
|
+
// Each item names the existing sub-command that applies it (connect · nexus channel-bot import); `--apply` is EN7 (apply.ts).
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { findSkillFolders } from '../skills/connect.js';
|
|
7
|
+
import { detectChannelImports, SOURCE_LABEL } from '../channel-import/detect.js';
|
|
8
|
+
|
|
9
|
+
export type ImportSourceId = 'agents' | 'claude' | 'codex' | 'openclaw' | 'hermes';
|
|
10
|
+
export type ImportItemKind = 'skill' | 'instructions' | 'mcp' | 'hooks' | 'credentials' | 'channel-bot';
|
|
11
|
+
/** `same` already reachable/identical · `conflict` same name, different content · `archived-only` kept, never run · `secret` moved only on an explicit ask. */
|
|
12
|
+
export type ImportItemStatus = 'new' | 'same' | 'conflict' | 'unsupported' | 'archived-only' | 'secret';
|
|
13
|
+
|
|
14
|
+
export interface ImportSourceInfo { id: ImportSourceId; root: string; detected: boolean }
|
|
15
|
+
export interface ImportItem {
|
|
16
|
+
/** `<source>:<kind>:<name>` — what `import --apply --pick` takes. */
|
|
17
|
+
id: string;
|
|
18
|
+
source: ImportSourceId;
|
|
19
|
+
kind: ImportItemKind;
|
|
20
|
+
name: string;
|
|
21
|
+
/** `~`-relative path the item was found in. */
|
|
22
|
+
path: string;
|
|
23
|
+
status: ImportItemStatus;
|
|
24
|
+
/** The existing command that applies this item; absent when nothing applies it yet. */
|
|
25
|
+
next?: string;
|
|
26
|
+
note?: string;
|
|
27
|
+
}
|
|
28
|
+
export interface ImportPlan { sources: ImportSourceInfo[]; items: ImportItem[] }
|
|
29
|
+
|
|
30
|
+
export interface ImportPlanInput {
|
|
31
|
+
home: string;
|
|
32
|
+
/** `$CODEX_HOME` when set. */
|
|
33
|
+
codexHome?: string;
|
|
34
|
+
/** Skill directories elanous reads today (defaultSkillDirs). */
|
|
35
|
+
activeSkillDirs: readonly string[];
|
|
36
|
+
/** Ids of MCP servers already in elanous config. */
|
|
37
|
+
mcpServerIds: readonly string[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const IMPORT_STATUSES: readonly ImportItemStatus[] = ['new', 'same', 'conflict', 'unsupported', 'archived-only', 'secret'];
|
|
41
|
+
|
|
42
|
+
function sourceRoots(input: ImportPlanInput): Record<ImportSourceId, string> {
|
|
43
|
+
const { home } = input;
|
|
44
|
+
const openclaw = [join(home, '.openclaw'), join(home, '.clawdbot'), join(home, '.moltbot')].find((p) => existsSync(p)) ?? join(home, '.openclaw');
|
|
45
|
+
return {
|
|
46
|
+
agents: join(home, '.agents'),
|
|
47
|
+
claude: join(home, '.claude'),
|
|
48
|
+
codex: input.codexHome || join(home, '.codex'),
|
|
49
|
+
openclaw,
|
|
50
|
+
hermes: join(home, '.hermes'),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const tilde = (path: string, home: string) => (path.startsWith(home) ? `~${path.slice(home.length)}` : path);
|
|
55
|
+
const hash = (path: string) => { try { return createHash('sha256').update(readFileSync(path)).digest('hex'); } catch { return null; } };
|
|
56
|
+
const readText = (path: string) => { try { return readFileSync(path, 'utf8'); } catch { return null; } };
|
|
57
|
+
const readJson = (path: string): Record<string, unknown> | null => {
|
|
58
|
+
const raw = readText(path);
|
|
59
|
+
if (raw === null) return null;
|
|
60
|
+
try { const v = JSON.parse(raw) as unknown; return v && typeof v === 'object' && !Array.isArray(v) ? v as Record<string, unknown> : null; } catch { return null; }
|
|
61
|
+
};
|
|
62
|
+
const keysOf = (value: unknown): string[] => (value && typeof value === 'object' && !Array.isArray(value) ? Object.keys(value as object) : []);
|
|
63
|
+
const hasSecretFields = (spec: unknown): boolean => {
|
|
64
|
+
if (!spec || typeof spec !== 'object') return false;
|
|
65
|
+
const s = spec as Record<string, unknown>;
|
|
66
|
+
return keysOf(s.env).length > 0 || keysOf(s.headers).length > 0 || keysOf(s.http_headers).length > 0;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/** Skill folders of one root, each judged against what elanous already reads. */
|
|
70
|
+
function skillItems(source: ImportSourceId, root: string, input: ImportPlanInput, next?: string): Array<Omit<ImportItem, 'id'>> {
|
|
71
|
+
const names = findSkillFolders(root);
|
|
72
|
+
const rootActive = input.activeSkillDirs.includes(root);
|
|
73
|
+
return names.map((name): Omit<ImportItem, 'id'> => {
|
|
74
|
+
const path = tilde(join(root, name), input.home);
|
|
75
|
+
if (rootActive) return { source, kind: 'skill', name, path, status: 'same', note: '이미 읽는 폴더' };
|
|
76
|
+
const mine = hash(join(root, name, 'SKILL.md'));
|
|
77
|
+
const twin = input.activeSkillDirs.map((dir) => join(dir, name, 'SKILL.md')).find((p) => existsSync(p));
|
|
78
|
+
if (twin) {
|
|
79
|
+
return hash(twin) === mine
|
|
80
|
+
? { source, kind: 'skill', name, path, status: 'same', note: `같은 내용이 이미 있다: ${tilde(twin, input.home)}` }
|
|
81
|
+
: { source, kind: 'skill', name, path, status: 'conflict', note: `같은 이름 · 다른 내용: ${tilde(twin, input.home)}` };
|
|
82
|
+
}
|
|
83
|
+
return { source, kind: 'skill', name, path, status: 'new', ...(next ? { next } : { note: '적용 명령은 0.2.9 `import --apply`' }) };
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function mcpItem(source: ImportSourceId, name: string, path: string, secret: boolean, input: ImportPlanInput): Omit<ImportItem, 'id'> {
|
|
88
|
+
if (secret) return { source, kind: 'mcp', name, path, status: 'secret', note: 'env·헤더 값이 있다 — 묻기 전까지 옮기지 않는다' };
|
|
89
|
+
if (input.mcpServerIds.includes(name)) return { source, kind: 'mcp', name, path, status: 'same', note: '같은 id 가 이미 있다' };
|
|
90
|
+
return { source, kind: 'mcp', name, path, status: 'new', note: '0.2.9 `import --apply` 가 꺼진 채로 넣는다' };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** `[mcp_servers.<name>]` tables of a Codex config.toml; `env`/`headers` keys or sub-tables count as secret. */
|
|
94
|
+
function codexMcp(raw: string): Array<{ name: string; secret: boolean }> {
|
|
95
|
+
const out = new Map<string, boolean>();
|
|
96
|
+
let current: string | null = null;
|
|
97
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
98
|
+
const table = /^\s*\[([^\[\]]+)\]\s*(?:#.*)?$/.exec(line)?.[1]?.trim();
|
|
99
|
+
if (table !== undefined) {
|
|
100
|
+
const m = /^mcp_servers\.(?:"([^"]+)"|([A-Za-z0-9_-]+))(?:\.([A-Za-z_]+))?$/.exec(table);
|
|
101
|
+
current = m ? (m[1] ?? m[2]!) : null;
|
|
102
|
+
if (current) out.set(current, (out.get(current) ?? false) || ['env', 'headers', 'http_headers'].includes(m![3] ?? ''));
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (current && /^\s*(env|headers|http_headers)\s*=/.test(line)) out.set(current, true);
|
|
106
|
+
}
|
|
107
|
+
return [...out].map(([name, secret]) => ({ name, secret }));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Top-level keys under `mcp_servers:` in a Hermes config.yaml (two-space indent). */
|
|
111
|
+
function hermesMcp(raw: string): string[] {
|
|
112
|
+
const block = /^mcp_servers:\s*\n((?:[ \t]+.*\n?|\s*\n)*)/m.exec(raw)?.[1] ?? '';
|
|
113
|
+
return [...block.matchAll(/^ {2}([A-Za-z0-9_.-]+):/gm)].map((m) => m[1]!);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function planImport(input: ImportPlanInput): ImportPlan {
|
|
117
|
+
const roots = sourceRoots(input);
|
|
118
|
+
const sources = (Object.keys(roots) as ImportSourceId[]).map((id) => ({ id, root: tilde(roots[id], input.home), detected: existsSync(roots[id]) }));
|
|
119
|
+
const items: Array<Omit<ImportItem, 'id'>> = [];
|
|
120
|
+
const home = input.home;
|
|
121
|
+
const at = (p: string) => tilde(p, home);
|
|
122
|
+
|
|
123
|
+
if (existsSync(roots.agents)) {
|
|
124
|
+
items.push(...skillItems('agents', join(roots.agents, 'skills'), input));
|
|
125
|
+
if (existsSync(join(roots.agents, 'AGENTS.md'))) items.push({ source: 'agents', kind: 'instructions', name: 'AGENTS.md', path: at(join(roots.agents, 'AGENTS.md')), status: 'unsupported', note: '전역 지시 파일은 아직 싣지 않는다' });
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (existsSync(roots.claude)) {
|
|
129
|
+
items.push(...skillItems('claude', join(roots.claude, 'skills'), input, 'elanous connect claude'));
|
|
130
|
+
if (existsSync(join(roots.claude, 'CLAUDE.md'))) items.push({ source: 'claude', kind: 'instructions', name: 'CLAUDE.md', path: at(join(roots.claude, 'CLAUDE.md')), status: 'unsupported', note: '전역 지시 파일은 아직 싣지 않는다' });
|
|
131
|
+
const claudeJson = join(home, '.claude.json');
|
|
132
|
+
for (const [name, spec] of Object.entries((readJson(claudeJson)?.mcpServers ?? {}) as Record<string, unknown>)) items.push(mcpItem('claude', name, at(claudeJson), hasSecretFields(spec), input));
|
|
133
|
+
const settings = join(roots.claude, 'settings.json');
|
|
134
|
+
for (const event of keysOf(readJson(settings)?.hooks)) items.push({ source: 'claude', kind: 'hooks', name: event, path: at(settings), status: 'archived-only', note: '훅은 보관만 · 켜지 않는다' });
|
|
135
|
+
if (existsSync(join(roots.claude, '.credentials.json'))) items.push({ source: 'claude', kind: 'credentials', name: 'login', path: at(join(roots.claude, '.credentials.json')), status: 'secret', note: '로그인은 옮기지 않는다 · 엘라누스에서 따로 한 번' });
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (existsSync(roots.codex)) {
|
|
139
|
+
items.push(...skillItems('codex', join(roots.codex, 'skills'), input, 'elanous connect codex'));
|
|
140
|
+
if (existsSync(join(roots.codex, 'AGENTS.md'))) items.push({ source: 'codex', kind: 'instructions', name: 'AGENTS.md', path: at(join(roots.codex, 'AGENTS.md')), status: 'unsupported', note: '전역 지시 파일은 아직 싣지 않는다' });
|
|
141
|
+
const toml = join(roots.codex, 'config.toml');
|
|
142
|
+
const raw = readText(toml);
|
|
143
|
+
for (const server of raw ? codexMcp(raw) : []) items.push(mcpItem('codex', server.name, at(toml), server.secret, input));
|
|
144
|
+
if (existsSync(join(roots.codex, 'auth.json'))) items.push({ source: 'codex', kind: 'credentials', name: 'login', path: at(join(roots.codex, 'auth.json')), status: 'secret', note: '로그인은 옮기지 않는다 · 엘라누스에서 따로 한 번' });
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (existsSync(roots.openclaw)) {
|
|
148
|
+
items.push(...skillItems('openclaw', join(roots.openclaw, 'skills'), input));
|
|
149
|
+
const cfg = join(roots.openclaw, 'openclaw.json');
|
|
150
|
+
const servers = (readJson(cfg)?.mcp as Record<string, unknown> | undefined)?.servers;
|
|
151
|
+
for (const [name, spec] of Object.entries((servers && typeof servers === 'object' ? servers : {}) as Record<string, unknown>)) items.push(mcpItem('openclaw', name, at(cfg), hasSecretFields(spec), input));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (existsSync(roots.hermes)) {
|
|
155
|
+
items.push(...skillItems('hermes', join(roots.hermes, 'skills'), input));
|
|
156
|
+
const yaml = join(roots.hermes, 'config.yaml');
|
|
157
|
+
const raw = readText(yaml);
|
|
158
|
+
for (const name of raw ? hermesMcp(raw) : []) items.push({ source: 'hermes', kind: 'mcp', name, path: at(yaml), status: 'unsupported', note: 'YAML 형식은 아직 변환하지 않는다' });
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// EN13 — chat bots: token values stay masked; the existing import command stores them after the user agrees.
|
|
162
|
+
const bots = detectChannelImports(home);
|
|
163
|
+
for (const c of bots.candidates) {
|
|
164
|
+
const source: ImportSourceId | null = c.source === 'openclaw' ? 'openclaw' : c.source === 'hermes' ? 'hermes' : null;
|
|
165
|
+
if (!source) continue;
|
|
166
|
+
items.push({ source, kind: 'channel-bot', name: c.platform, path: c.file, status: 'secret', next: 'elanous nexus channel-bot import', note: `${SOURCE_LABEL[c.source]} 봇 토큰(가림)` });
|
|
167
|
+
}
|
|
168
|
+
for (const u of bots.unreadable) {
|
|
169
|
+
const source: ImportSourceId | null = u.file.includes('.hermes') ? 'hermes' : u.file.includes('.openclaw') || u.file.includes('.clawdbot') || u.file.includes('.moltbot') ? 'openclaw' : null;
|
|
170
|
+
if (source) items.push({ source, kind: 'channel-bot', name: 'bot', path: u.file, status: 'unsupported', note: u.reason });
|
|
171
|
+
}
|
|
172
|
+
return { sources, items: items.map((item) => ({ ...item, id: `${item.source}:${item.kind}:${item.name}` })) };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const KIND_LABEL: Record<ImportItemKind, string> = { skill: '스킬', instructions: '지시 파일', mcp: 'MCP', hooks: '훅', credentials: '로그인', 'channel-bot': '채팅 봇' };
|
|
176
|
+
|
|
177
|
+
/** One screen: per source, counts by status, then the items that need a decision. */
|
|
178
|
+
export function formatImportPlan(plan: ImportPlan): string {
|
|
179
|
+
const lines: string[] = ['가져오기 계획 — 아무것도 쓰지 않았습니다.', ''];
|
|
180
|
+
const detected = plan.sources.filter((s) => s.detected);
|
|
181
|
+
if (!detected.length) return [...lines, '쓰던 에이전트를 찾지 못했습니다(~/.agents · ~/.claude · ~/.codex · ~/.openclaw · ~/.hermes).'].join('\n');
|
|
182
|
+
for (const source of detected) {
|
|
183
|
+
const mine = plan.items.filter((i) => i.source === source.id);
|
|
184
|
+
const counts = IMPORT_STATUSES.map((s) => [s, mine.filter((i) => i.status === s).length] as const).filter(([, n]) => n > 0);
|
|
185
|
+
lines.push(`■ ${source.id} (${source.root}) — ${mine.length ? counts.map(([s, n]) => `${s} ${n}`).join(' · ') : '항목 없음'}`);
|
|
186
|
+
for (const kind of Object.keys(KIND_LABEL) as ImportItemKind[]) {
|
|
187
|
+
const group = mine.filter((i) => i.kind === kind && i.status !== 'same');
|
|
188
|
+
if (!group.length) continue;
|
|
189
|
+
const shown = group.slice(0, 6).map((i) => `${i.name}[${i.status}]`).join(', ');
|
|
190
|
+
lines.push(` ${KIND_LABEL[kind]}: ${shown}${group.length > 6 ? ` 외 ${group.length - 6}개` : ''}`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
const nexts = [...new Set(plan.items.filter((i) => i.next && i.status !== 'same').map((i) => i.next!))];
|
|
194
|
+
lines.push('', '한 번에: `elanous import --apply` (스킬은 그 자리 등록 · MCP 는 꺼진 채로 · 비밀·훅은 보관만 · 되돌리기 `--undo`)');
|
|
195
|
+
lines.push(nexts.length ? `따로: ${nexts.map((n) => `\`${n}\``).join(' · ')}` : '따로 적용할 명령이 있는 항목은 없습니다.', '상태: new · same · conflict · unsupported · archived-only · secret — 자세히는 `elanous import --json`');
|
|
196
|
+
return lines.join('\n');
|
|
197
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -9,6 +9,9 @@ import { LOGS_SINCE_OPTION, registerLogsCommands } from './cli/logs-cli.js';
|
|
|
9
9
|
import { registerDocsCommands } from './cli/docs-cli.js';
|
|
10
10
|
import { registerA2ACommands } from './cli/a2a-cli.js';
|
|
11
11
|
import { registerPluginCommands } from './cli/plugin-cli.js';
|
|
12
|
+
import { registerConnectCommand, registerSkillsCommands } from './cli/skills-cli.js';
|
|
13
|
+
import { registerImportCommand } from './cli/import-cli.js';
|
|
14
|
+
import { registerPersonaCommands } from './cli/persona-cli.js';
|
|
12
15
|
import { registerFleetCommands } from './cli/fleet-cli.js';
|
|
13
16
|
import { readPipedStdin } from './cli/piped-stdin.js';
|
|
14
17
|
import { writeStdoutJson } from './cli/stdout-json.js';
|
|
@@ -885,6 +888,10 @@ registerCardCommand(program);
|
|
|
885
888
|
registerLaunchHeadCommands(program);
|
|
886
889
|
registerA2ACommands(program);
|
|
887
890
|
registerPluginCommands(program);
|
|
891
|
+
registerSkillsCommands(program);
|
|
892
|
+
registerConnectCommand(program);
|
|
893
|
+
registerImportCommand(program);
|
|
894
|
+
registerPersonaCommands(program);
|
|
888
895
|
|
|
889
896
|
const marketCmd = program.command('market').description('Signed plugin marketplace');
|
|
890
897
|
marketCmd.command('publish').description('Publish a local signed marketplace index and archives')
|
|
@@ -4177,6 +4184,9 @@ const selfOrchestrateCmd = selfCmd
|
|
|
4177
4184
|
});
|
|
4178
4185
|
process.env[HARNESS_RUN_ID_ENV] = runId;
|
|
4179
4186
|
reportRunId = runId;
|
|
4187
|
+
// HS1 — `harness stop <runId>` must find this process from the moment the run ID exists.
|
|
4188
|
+
try { (await import('./self-dev/orchestrate.js')).writeRunPidRecord(runId); }
|
|
4189
|
+
catch (error) { debug.log('self-dev.orchestrate', 'early-pid-failed', { runId, error: String(error) }, { level: 'warn' }); }
|
|
4180
4190
|
const createdAt = prior?.createdAt ?? Date.now();
|
|
4181
4191
|
const { bindOrchestrateRunLedger } = await import('./self-dev/self-orchestrate-runtime.js');
|
|
4182
4192
|
const { checkpoint } = bindOrchestrateRunLedger({
|
|
@@ -4302,7 +4312,26 @@ const selfOrchestrateCmd = selfCmd
|
|
|
4302
4312
|
grokApiKeyOptIn = getUserConfig().harness?.pod?.grokApiKeyOptIn === true;
|
|
4303
4313
|
const credential = resolveGrokCredential();
|
|
4304
4314
|
const { resolveCodexQuotaPolicy, codexPolicyAllowsCredits, codexPolicyAllowsFallback } = await import('./oauth/codex-quota-policy.js');
|
|
4305
|
-
const
|
|
4315
|
+
const resolvedQuota = resolveCodexQuotaPolicy(getUserConfig().llm);
|
|
4316
|
+
const quotaPolicy = resolvedQuota.policy;
|
|
4317
|
+
// POL1 — say which policy this launch uses and where it came from; warn when a test universe disagrees with production.
|
|
4318
|
+
try {
|
|
4319
|
+
const { effectiveInstanceRoot, prodInstanceRoot } = await import('./instance/resolve.js');
|
|
4320
|
+
const { describeLaunchQuotaPolicy } = await import('./oauth/codex-quota-policy.js');
|
|
4321
|
+
const root = effectiveInstanceRoot();
|
|
4322
|
+
const kind = root === prodInstanceRoot() ? 'prod' as const : 'test' as const;
|
|
4323
|
+
let production: ReturnType<typeof resolveCodexQuotaPolicy> | undefined;
|
|
4324
|
+
if (kind === 'test') {
|
|
4325
|
+
try {
|
|
4326
|
+
const { readFileSync } = await import('node:fs');
|
|
4327
|
+
const prodLlm = (JSON.parse(readFileSync(`${prodInstanceRoot()}/config.json`, 'utf8')) as { llm?: Record<string, unknown> }).llm;
|
|
4328
|
+
production = resolveCodexQuotaPolicy(prodLlm);
|
|
4329
|
+
} catch { /* no production config on this machine */ }
|
|
4330
|
+
}
|
|
4331
|
+
const described = describeLaunchQuotaPolicy({ current: resolvedQuota, universe: { kind, root }, ...(production ? { production } : {}) });
|
|
4332
|
+
debug.log('self-implement.pod', 'quota-policy', { policy: resolvedQuota.policy, source: resolvedQuota.source, universe: kind, productionPolicy: production?.policy ?? null });
|
|
4333
|
+
if (!opts.json) { ui.info(described.line); if (described.warning) ui.warn(described.warning); }
|
|
4334
|
+
} catch { /* the line is advice; the launch goes on */ }
|
|
4306
4335
|
const plan = planPodProvider({
|
|
4307
4336
|
codexCandidates: inspectCodexRotation().candidates,
|
|
4308
4337
|
// Per-account caps (대표 default:97) override the Pod default cap — without them default was dropped at 60%.
|
|
@@ -7258,16 +7287,31 @@ loginCmd
|
|
|
7258
7287
|
.description('Sign in to OpenAI Codex via the ChatGPT device-code flow')
|
|
7259
7288
|
.option('--replace-codex-cli-login', 'if the Codex CLI (~/.codex/auth.json) is signed in to a different account, replace it with this login')
|
|
7260
7289
|
.action(async (opts: { replaceCodexCliLogin?: boolean }) => {
|
|
7261
|
-
|
|
7290
|
+
// OB5d — say why there is no browser (ssh · no display) and what to do instead; otherwise try the browser first.
|
|
7291
|
+
const { browserUnavailableReason } = await import('./oauth/browser-availability.js');
|
|
7292
|
+
const noBrowser = browserUnavailableReason();
|
|
7293
|
+
let browserState: Awaited<ReturnType<typeof import('./oauth/codex-browser-login.js')['loginWithCodexBrowser']>> | null = null;
|
|
7294
|
+
if (!noBrowser) {
|
|
7295
|
+
ui.header('OpenAI Codex — 브라우저 로그인');
|
|
7296
|
+
console.log(' 브라우저에서 로그인을 마치세요 — 끝나면 자동으로 이어집니다.');
|
|
7297
|
+
try {
|
|
7298
|
+
const { loginWithCodexBrowser } = await import('./oauth/codex-browser-login.js');
|
|
7299
|
+
browserState = await loginWithCodexBrowser();
|
|
7300
|
+
} catch {
|
|
7301
|
+
console.log(' 브라우저 로그인이 안 됐습니다 — 코드로 이어갑니다.');
|
|
7302
|
+
}
|
|
7303
|
+
}
|
|
7304
|
+
if (!browserState) ui.header('OpenAI Codex — 코드로 로그인');
|
|
7305
|
+
if (noBrowser) console.log(` ${noBrowser}`);
|
|
7262
7306
|
try {
|
|
7263
|
-
const state = await loginWithCodex({
|
|
7307
|
+
const state = browserState ?? await loginWithCodex({
|
|
7264
7308
|
onProgress: (p) => {
|
|
7265
7309
|
if (p.type === 'user_code' && p.userCode) {
|
|
7266
7310
|
console.log('');
|
|
7267
|
-
console.log(` 1)
|
|
7268
|
-
console.log(` 2)
|
|
7311
|
+
console.log(` 1) 아무 브라우저에서 열기: ${p.loginUrl}`);
|
|
7312
|
+
console.log(` 2) 코드 입력: ${p.userCode}`);
|
|
7269
7313
|
console.log('');
|
|
7270
|
-
console.log('
|
|
7314
|
+
console.log(' 로그인을 기다리는 중… (취소: Ctrl+C)');
|
|
7271
7315
|
}
|
|
7272
7316
|
if (p.type === 'polling' && p.pollAttempt === 1) {
|
|
7273
7317
|
process.stdout.write(' .');
|
|
@@ -9797,6 +9841,23 @@ nexusChannelBotCmd
|
|
|
9797
9841
|
process.exit(result.exitCode);
|
|
9798
9842
|
});
|
|
9799
9843
|
|
|
9844
|
+
// EN13 — bring an existing Telegram/Discord bot over from OpenClaw · Hermes · the previous elanous (~/.monad).
|
|
9845
|
+
nexusChannelBotCmd
|
|
9846
|
+
.command('import')
|
|
9847
|
+
.description('Find Telegram/Discord bot settings you already use (OpenClaw, Hermes, previous elanous), preview them with the token masked, and store them after you agree.')
|
|
9848
|
+
.option('--yes', 'Import without asking')
|
|
9849
|
+
.action(async (opts: { yes?: boolean }) => {
|
|
9850
|
+
const { runChannelImport } = await import('./channel-import/run.js');
|
|
9851
|
+
const { createInterface } = await import('node:readline/promises');
|
|
9852
|
+
const ask = process.stdin.isTTY ? async (question: string): Promise<boolean> => {
|
|
9853
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
9854
|
+
try { const answer = (await rl.question(question)).trim(); return answer === '' || /^(y|yes|예|네)$/i.test(answer); }
|
|
9855
|
+
finally { rl.close(); }
|
|
9856
|
+
} : undefined;
|
|
9857
|
+
const result = await runChannelImport({ yes: opts.yes, ...(ask ? { ask } : {}) });
|
|
9858
|
+
process.exit(result.exitCode);
|
|
9859
|
+
});
|
|
9860
|
+
|
|
9800
9861
|
// FU A6-real P5 (2026-05-11) — interactive Firecrawl setup.
|
|
9801
9862
|
// Walks the user through CLI detection + API key entry + persist to
|
|
9802
9863
|
// `registry.discovery.firecrawl.apiKey`. `--api-key <value>` skips
|
|
@@ -10461,6 +10522,14 @@ async function main(): Promise<void> {
|
|
|
10461
10522
|
// elanous --acp-server --transport=unix-socket --socket-path=/tmp/x.sock
|
|
10462
10523
|
// elanous --acp-server --transport=websocket --port=31415
|
|
10463
10524
|
// elanous --acp-server --transport=websocket --no-auth # skip token check
|
|
10525
|
+
if (rawArgs.includes('--acp-server') && rawArgs.includes('--login')) {
|
|
10526
|
+
// EN10a — an ACP client's terminal auth re-launches `elanous --acp-server --login`: run the sign-in in that terminal and exit with its status.
|
|
10527
|
+
const { ACP_LOGIN_SUBCOMMAND, currentLaunch } = await import('./acp/auth-methods.js');
|
|
10528
|
+
const launch = currentLaunch();
|
|
10529
|
+
const { spawnSync } = await import('node:child_process');
|
|
10530
|
+
const run = spawnSync(launch.command, [...launch.args, ...ACP_LOGIN_SUBCOMMAND], { stdio: 'inherit' });
|
|
10531
|
+
process.exit(run.status ?? 1);
|
|
10532
|
+
}
|
|
10464
10533
|
if (rawArgs.includes('--acp-server')) {
|
|
10465
10534
|
const { bootAcpServer, parseAcpBootArgs } = await import('./boot/acp-server.js');
|
|
10466
10535
|
const { createDaemonRuntime } = await import('./boot/daemon-runtime.js');
|
|
@@ -10751,12 +10820,18 @@ async function main(): Promise<void> {
|
|
|
10751
10820
|
* 회귀 방어 = `src/cli-entry.test.ts`(실물 `bin/elanous.mjs` 를 spawn 한다).
|
|
10752
10821
|
*/
|
|
10753
10822
|
export function runCli(): void {
|
|
10754
|
-
main().catch((err) => {
|
|
10823
|
+
main().catch(async (err) => {
|
|
10755
10824
|
closeTui();
|
|
10756
10825
|
if (isCliUserError(err)) {
|
|
10757
10826
|
console.error(formatCliUserError(err));
|
|
10758
10827
|
process.exit(1);
|
|
10759
10828
|
}
|
|
10829
|
+
// ER1 — a real crash is reported once (consented · scrubbed · never blocks more than the send timeout).
|
|
10830
|
+
try {
|
|
10831
|
+
const { reportError } = await import('./error-report/report.js');
|
|
10832
|
+
const outcome = await reportError({ code: 'cli-crash', message: err instanceof Error ? `${err.name}: ${err.message}` : String(err), ...(err instanceof Error && err.stack ? { stack: err.stack } : {}), surface: 'cli' });
|
|
10833
|
+
if (outcome.sent) console.error(`오류를 운영팀에 알렸습니다${outcome.reportId ? ` (보고 번호 ${outcome.reportId})` : ''}.`);
|
|
10834
|
+
} catch { /* the crash itself is what matters */ }
|
|
10760
10835
|
throw err;
|
|
10761
10836
|
});
|
|
10762
10837
|
}
|
|
@@ -40,15 +40,23 @@ export function keyEventToTextInsertion(
|
|
|
40
40
|
return preservePrintableText(ev, name);
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
/** Hangul Compatibility Jamo (ㄱ…ㅣ · ㅋㅋ · ㅠㅠ). The key parser maps a lone jamo keystroke to the QWERTY key
|
|
44
|
+
* on the same position so hotkeys work under the Korean IME — but as TEXT it must stay the jamo the person typed
|
|
45
|
+
* (U1 · 2026-10-02: «ㅋㅋㅋ ㅠㅠ» came out as «zzz bb» in the chat input). */
|
|
46
|
+
const COMPAT_JAMO = /^[\u3131-\u318E]$/;
|
|
47
|
+
|
|
43
48
|
function preservePrintableText(ev: KeyEvent, name: string): string {
|
|
49
|
+
// The chat composer passes the TUI key (original bytes on `raw`); other composers pass a KeyEvent (`sequence`).
|
|
50
|
+
const sequence = ev.sequence ?? (ev as { raw?: string }).raw;
|
|
51
|
+
if (sequence && name.length === 1 && COMPAT_JAMO.test(sequence)) return sequence;
|
|
44
52
|
if (
|
|
45
|
-
|
|
46
|
-
&&
|
|
53
|
+
sequence
|
|
54
|
+
&& sequence.length === 1
|
|
47
55
|
&& name.length === 1
|
|
48
|
-
&&
|
|
49
|
-
&&
|
|
56
|
+
&& sequence !== name
|
|
57
|
+
&& sequence.toLowerCase() === name.toLowerCase()
|
|
50
58
|
) {
|
|
51
|
-
return
|
|
59
|
+
return sequence;
|
|
52
60
|
}
|
|
53
61
|
if (name.length === 1) return ev.shift ? name.toUpperCase() : name;
|
|
54
62
|
return name;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// EV12d — 계획기가 맞는 그래프를 못 고르면(«요청에 맞는 설치된 실행 그래프가 없습니다») «문서 요청 실패» 대신
|
|
2
|
+
// 그 자리가 직접 답한다: 역할 파일(내부 문서 `<id>`) ⊕ 그 자리가 owner 인 체크리스트 칸 ⊕ 최근 요청 몇 줄을
|
|
3
|
+
// 맥락으로 LLM 한 번 → 같은 채널 회신(본문). 맥락에 없는 것은 «모른다»고 답하게 한다(지어내지 않는다).
|
|
4
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { debug } from '../debug/log.js';
|
|
7
|
+
import { ExecRequestStore, type ExecRequest } from '../exec-requests/store.js';
|
|
8
|
+
import { devVersion, listChecklist, type ChecklistItem } from '../release-loop/checklist.js';
|
|
9
|
+
import { resolveSeat } from '../seat-address/seat-address.js';
|
|
10
|
+
|
|
11
|
+
const ROLE_CHARS = 6_000;
|
|
12
|
+
|
|
13
|
+
/** Cut by code points, never inside a surrogate pair. A `.slice` through an emoji (🎉 · 🟢) leaves a lone surrogate and
|
|
14
|
+
* the model API rejects the whole request («Codex API 400: Bad Request» · 10-02 07:4x — every «@CMO …» question failed). */
|
|
15
|
+
function cut(text: string, max: number): string {
|
|
16
|
+
const chars = Array.from(text);
|
|
17
|
+
return chars.length > max ? chars.slice(0, max).join('') : text;
|
|
18
|
+
}
|
|
19
|
+
const ITEM_LIMIT = 25;
|
|
20
|
+
const RECENT_LIMIT = 5;
|
|
21
|
+
|
|
22
|
+
export interface SeatAnswerDeps {
|
|
23
|
+
readRole?: (seatId: string) => string | null;
|
|
24
|
+
ownedItems?: (seatId: string) => ChecklistItem[];
|
|
25
|
+
recentRequests?: () => ExecRequest[];
|
|
26
|
+
complete?: (prompt: string) => Promise<string>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function defaultReadRole(seatId: string): string | null {
|
|
30
|
+
const path = fileURLToPath(new URL(`../../docs/roles/${seatId}.md`, import.meta.url));
|
|
31
|
+
return existsSync(path) ? readFileSync(path, 'utf8') : null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function nextPatch(version: string): string | null {
|
|
35
|
+
const m = /^(\d+)\.(\d+)\.(\d+)/.exec(version);
|
|
36
|
+
return m ? `${m[1]}.${m[2]}.${Number(m[3]) + 1}` : null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Items this seat owns in the dev version's checklist and the next one (where 10-28 work lives). */
|
|
40
|
+
function defaultOwnedItems(seatId: string): ChecklistItem[] {
|
|
41
|
+
const versions = [devVersion().replace(/-.*$/, '')];
|
|
42
|
+
const next = nextPatch(versions[0]!);
|
|
43
|
+
if (next) versions.push(next);
|
|
44
|
+
const items: ChecklistItem[] = [];
|
|
45
|
+
for (const version of versions) {
|
|
46
|
+
try { items.push(...listChecklist(version).items.filter((item) => item.owner === seatId)); }
|
|
47
|
+
catch { /* a version without a checklist contributes nothing */ }
|
|
48
|
+
}
|
|
49
|
+
return items;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function defaultComplete(prompt: string): Promise<string> {
|
|
53
|
+
const { streamLLM } = await import('../llm.js');
|
|
54
|
+
const { tierModel } = await import('../llm/model-defaults.js');
|
|
55
|
+
return streamLLM([{ role: 'user', content: prompt }], () => {}, { model: tierModel('better') });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const STATUS_MARK: Record<string, string> = { green: '🟢', yellow: '🟡', red: '🔴', done: '✅' };
|
|
59
|
+
|
|
60
|
+
export function seatAnswerPrompt(input: { title: string; seatId: string; question: string; role: string | null; items: ChecklistItem[]; recent: ExecRequest[] }): string {
|
|
61
|
+
const items = input.items.slice(0, ITEM_LIMIT)
|
|
62
|
+
.map((item) => `${STATUS_MARK[item.status] ?? item.status} ${item.id} ${cut(item.title, 140)}${item.evidence ? ` — 근거: ${cut(item.evidence, 160)}` : ''}`)
|
|
63
|
+
.join('\n') || '(담당 칸 없음)';
|
|
64
|
+
const recent = input.recent.slice(0, RECENT_LIMIT)
|
|
65
|
+
.map((request) => `${request.createdAt.slice(0, 16)} ${request.status} · ${cut(request.text.replace(/\s+/g, ' '), 120)}`)
|
|
66
|
+
.join('\n') || '(최근 요청 없음)';
|
|
67
|
+
return [
|
|
68
|
+
`너는 엘라누스 팀의 ${input.title}(${input.seatId}) 자리다. 아래 «역할 문서 · 담당 칸 · 최근 요청»만 근거로 질문에 답한다.`,
|
|
69
|
+
'규칙: 한국어 존댓말 · 12줄 이내 · 맥락에 없는 사실·수치·일정은 지어내지 말고 «모른다/확인 필요»라고 쓴다 · 담당 칸을 인용할 땐 칸 ID 를 붙인다 · 내부 파일 경로·토큰·계정은 쓰지 않는다.',
|
|
70
|
+
`\n## 역할 문서\n${cut(input.role ?? '(역할 문서 없음)', ROLE_CHARS)}`,
|
|
71
|
+
`\n## 담당 칸(체크리스트 owner=${input.seatId})\n${items}`,
|
|
72
|
+
`\n## 최근 자리 요청\n${recent}`,
|
|
73
|
+
`\n## 질문\n${input.question}`,
|
|
74
|
+
].join('\n');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** The seat's own answer, or null when the seat is unknown or the model gave nothing usable. */
|
|
78
|
+
export async function answerAsSeat(seatAddress: string, question: string, deps: SeatAnswerDeps = {}): Promise<{ title: string; text: string } | null> {
|
|
79
|
+
const seat = resolveSeat(seatAddress);
|
|
80
|
+
if (!seat?.title) return null;
|
|
81
|
+
const role = (deps.readRole ?? defaultReadRole)(seat.id);
|
|
82
|
+
const items = (deps.ownedItems ?? defaultOwnedItems)(seat.id);
|
|
83
|
+
const recent = (deps.recentRequests ?? (() => new ExecRequestStore().list()))();
|
|
84
|
+
const raw = await (deps.complete ?? defaultComplete)(seatAnswerPrompt({ title: seat.title, seatId: seat.id, question, role, items, recent }));
|
|
85
|
+
const text = raw.trim();
|
|
86
|
+
debug.log('intake.seat-doc', 'seat-answer', { seat: seat.id, roleFile: role !== null, items: items.length, recent: Math.min(recent.length, RECENT_LIMIT), chars: text.length });
|
|
87
|
+
return text ? { title: seat.title, text: cut(text, 3_500) } : null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** «No installed graph fits» — the planner's own failure wording, so the seat should answer instead. */
|
|
91
|
+
export function isNoGraphFailure(result: { status: string; summary?: string; seats?: Array<{ graphId: string; reason?: string }> } | null): boolean {
|
|
92
|
+
if (!result || result.status !== 'failed' || !result.seats?.length) return false;
|
|
93
|
+
if (!result.seats.every((seat) => !seat.graphId)) return false;
|
|
94
|
+
const NO_GRAPH = /설치된 실행 그래프가 없습니다/;
|
|
95
|
+
return NO_GRAPH.test(result.summary ?? '') || result.seats.some((seat) => NO_GRAPH.test(seat.reason ?? ''));
|
|
96
|
+
}
|