agentlas 1.0.40 → 1.0.42

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,24 +1,60 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.42 — 2026-08-11
4
+
5
+ Projects, automations and firms reach the interactive shell; the shell is
6
+ enabled with `AGENTLAS_TUI=1`.
7
+
8
+ - `/projects` — every connected project with chat/task counts, and a clear
9
+ marker for the one this folder belongs to (or an explicit warning when the
10
+ folder is not connected).
11
+ - `/automations` — the full list with schedule, next run, run count and failure
12
+ count; `/automations <name>` opens the detail view with recent run outcomes
13
+ and the exact commands to run or toggle it.
14
+ - `/firms` — teams with member counts; `/firms <slug>` shows the roster.
15
+ - Table layout no longer inflates narrow status columns when a row overflows
16
+ the terminal width.
17
+ - The shell is now enabled with `AGENTLAS_TUI=1`. Renderer internals are an
18
+ implementation detail and no longer appear in commands, environment
19
+ variables, or on screen.
20
+
21
+ ## 1.0.41 — 2026-08-11
22
+
23
+ Desktop surfaces, measured against the desktop inventory and rebuilt for the terminal.
24
+
25
+ - `/dashboard` now mirrors the desktop control panel: a **Needs attention**
26
+ section (waiting approvals and runs that stalled without a terminal state),
27
+ run activity that states failures out loud (e.g. "8/11 failed" rather than a
28
+ quiet list), automations with next-run times, most-used agents, and evolution
29
+ proposal counts.
30
+ - `/library` — installed agents with role/kind, MCP server count, and pointers
31
+ to env, credentials, and plugins.
32
+ - `/marketplace` (also `/bookmarks`) — Hub bookmarks and borrowed-agent careers
33
+ held locally, with search/install/credit pointers.
34
+ - `/settings` — language, permission, active runtime, installed CLIs, and the
35
+ resolved orchestrator/worker model roles; names what remains Desktop-only
36
+ instead of implying parity.
37
+ - Those five honest stops now point at the interactive shell instead of claiming the
38
+ surface is unreachable from the terminal.
39
+
3
40
  ## 1.0.40 — 2026-08-11
4
41
 
5
- Desktop surfaces reach the pi shell: dashboard and graph view.
42
+ Desktop surfaces reach the interactive shell: dashboard and graph view.
6
43
 
7
- - `/dashboard` (pi shell): agents/firms/telegram badges + automations panel
44
+ - `/dashboard` (interactive shell): agents/firms/telegram badges + automations panel
8
45
  (enabled state, last run) from the local store — first of the thirteen
9
46
  Desktop-only honest stops to be lifted.
10
- - `/graph show <name>` (pi shell): the automation graph renders as Unicode
47
+ - `/graph show <name>` (interactive shell): the automation graph renders as Unicode
11
48
  box art (grok-mermaid) — condition branches, loop-back edges, and Korean
12
49
  labels align correctly. No canvas emulation; editing stays declarative.
13
- - First-run onboarding now runs before the pi shell starts (sequential
14
- readline → pi-tui, no stdin contention), so `AGENTLAS_TUI=pi` works for
50
+ - First-run onboarding now runs before the interactive shell starts (sequential, no stdin contention), so `AGENTLAS_TUI=1` works for
15
51
  brand-new installs too.
16
52
 
17
53
  ## 1.0.39 — 2026-08-11
18
54
 
19
- pi-tui shell increment 2 + architecture mirror sync.
55
+ Interactive shell increment 2 + architecture mirror sync.
20
56
 
21
- - Experimental pi shell (`AGENTLAS_TUI=pi`): persisted history (cli-history.json
57
+ - Experimental interactive shell (`AGENTLAS_TUI=1`): persisted history (cli-history.json
22
58
  v2 contract), Shift-Tab permission cycling (same two-step FULL arming state
23
59
  machine as the classic REPL), `!` shell passthrough (full-permission gate,
24
60
  secret masking), and `/s` `/switch` `/kill` `/rm` `/sessions` `/tree` session
@@ -93,13 +93,13 @@ const DESKTOP_ONLY_SURFACES = {
93
93
  trex: "T-rex slide studio is Desktop-only.",
94
94
  slides: "T-rex slide studio is Desktop-only.",
95
95
  prompts: "Prompt Store is Desktop-only.",
96
- dashboard: "Dashboard is Desktop-onlyuse: agentlas doctor · usage · list",
97
- marketplace: "Marketplace browsing is Desktop-onlyuse: agentlas search \"<what you need>\"",
98
- library: "Library is Desktop-onlyuse: agentlas list · env · mcp",
99
- settings: "Settings UI is Desktop-onlyuse: agentlas setup · env · creds · multimodal · doctor",
96
+ dashboard: "Dashboard: run `AGENTLAS_TUI=1 agentlas` then /dashboard or: agentlas doctor · usage · list",
97
+ marketplace: "Hub view: run `AGENTLAS_TUI=1 agentlas` then /marketplace or: agentlas search \"<what you need>\"",
98
+ library: "Library: run `AGENTLAS_TUI=1 agentlas` then /library or: agentlas list · env · mcp",
99
+ settings: "Settings: run `AGENTLAS_TUI=1 agentlas` then /settings change with: agentlas setup · env · creds · multimodal",
100
100
  apps: "Apps surface is Desktop-only.",
101
101
  quests: "Quests are Desktop-only.",
102
- bookmarks: "Hub bookmarks are Desktop-only.",
102
+ bookmarks: "Hub bookmarks: run `AGENTLAS_TUI=1 agentlas` then /marketplace.",
103
103
  one: "Agentlas One is a separate Desktop/Mobile product surface.",
104
104
  };
105
105
 
@@ -24,7 +24,6 @@ const SLASH_COMMANDS = [
24
24
  { command: "/graph", args: "[run <이름>]", ko: "저장된 자동화 그래프", en: "Saved automation graphs" },
25
25
  { command: "/mcp", args: "", ko: "MCP 서버 목록", en: "MCP servers" },
26
26
  { command: "/doctor", args: "", ko: "런타임·데이터 점검", en: "Health check" },
27
- { command: "/dashboard", args: "", ko: "관제 대시보드 (pi 셸)", en: "Dashboard (pi shell)" },
28
27
  { command: "/runtime", args: "<kind>", ko: "새 세션 런타임 지정", en: "Set runtime for new sessions" },
29
28
  { command: "/model", args: "<id|default>", ko: "새 세션 모델 지정", en: "Set model for new sessions" },
30
29
  { command: "/effort", args: "<level|none>", ko: "새 세션 추론 강도 지정", en: "Set effort for new sessions" },
@@ -125,12 +125,12 @@ async function startRepl(ctx, opts = {}) {
125
125
  }
126
126
 
127
127
  /*
128
- * 실험 셸 opt-in (D3 Phase 2): AGENTLAS_TUI=pi + TTY.
128
+ * 대화형 셸 opt-in (D3 Phase 2): AGENTLAS_TUI=1 + TTY.
129
129
  * 온보딩 마법사(위 readline 블록)가 먼저 끝난 뒤 진입한다 — 순차 실행이라
130
- * stdin 경합이 없고, 첫 실행 사용자도 pi 셸을 쓸 수 있다(증분 2c).
130
+ * stdin 경합이 없고, 첫 실행 사용자도 셸을 쓸 수 있다(증분 2c).
131
131
  */
132
- if (process.env.AGENTLAS_TUI === "pi" && process.stdin.isTTY) {
133
- return require("./pitui-shell.cjs").startPiShell(ctx, opts);
132
+ if (/^(1|true|on)$/i.test(String(process.env.AGENTLAS_TUI || "")) && process.stdin.isTTY) {
133
+ return require("./shell.cjs").startShell(ctx, opts);
134
134
  }
135
135
  const orch = new Orchestrator({ db, lang: ctx.lang });
136
136
  const renderer = new Renderer(ui);
@@ -0,0 +1,354 @@
1
+ "use strict";
2
+ /*
3
+ * ui/screens — 데스크탑 화면의 터미널 대응물 (D3 Phase 3).
4
+ *
5
+ * 대조 원본: docs/2026-08-11-terminal-tui-overhaul/D1-데스크탑-기능-인벤토리.md
6
+ * 각 화면은 데스크탑이 IPC로 읽는 것과 같은 로컬 저장소를 직접 읽는다.
7
+ *
8
+ * 규칙:
9
+ * - 없는 데이터를 지어내지 않는다. 표면이 데스크탑 전용이면 그렇게 말한다.
10
+ * - 실패·미결은 눈에 띄게. "조용히 멈춘 실행"이 정상처럼 보이면 안 된다(D1 숨은 계약 2).
11
+ * - 모든 화면은 ctx.out 이 아니라 ui 를 직접 받아 pi 프레임 안에 그린다.
12
+ */
13
+
14
+ function table(ui, rows, opts = {}) {
15
+ // rows: [[col, col, …]] — 첫 행이 헤더. 폭은 CJK 셀 폭으로 계산한다.
16
+ const { visWidth, truncateWidth } = require("./width.cjs");
17
+ if (!rows.length) return;
18
+ const cols = rows[0].length;
19
+ const max = Number(opts.maxWidth) || 78;
20
+ const widths = Array.from({ length: cols }, (_, i) => {
21
+ const cap = opts.cap?.[i];
22
+ return Math.min(Number.isFinite(cap) ? cap : 40, Math.max(...rows.map((r) => visWidth(String(r[i] ?? "")))));
23
+ });
24
+ /*
25
+ * 넘치면 "가장 넓은 열"을 깎는다. 예전엔 0번 열을 깎으면서 Math.max(8, …) 바닥을
26
+ * 뒀는데, 1칸짜리 상태 마커 열이 8칸으로 부풀어 표가 깨졌다(실측). 바닥은 4칸이고
27
+ * 이미 그보다 좁은 열은 건드리지 않는다.
28
+ */
29
+ let over = widths.reduce((a, b) => a + b + 2, 0) - max;
30
+ while (over > 0) {
31
+ let widest = -1;
32
+ widths.forEach((w, i) => { if (w > 4 && (widest < 0 || w > widths[widest])) widest = i; });
33
+ if (widest < 0) break;
34
+ const cut = Math.min(over, widths[widest] - 4);
35
+ widths[widest] -= cut;
36
+ over -= cut;
37
+ }
38
+ rows.forEach((row, index) => {
39
+ const line = row.map((cell, i) => {
40
+ const text = truncateWidth(String(cell ?? ""), widths[i]);
41
+ return text + " ".repeat(Math.max(0, widths[i] - visWidth(text)));
42
+ }).join(" ");
43
+ ui.line(index === 0 && opts.header !== false ? ui.c.dim(line) : " " + line);
44
+ });
45
+ }
46
+
47
+ function count(db, sql, fallback = 0) {
48
+ try { return db.prepare(sql).get()?.n ?? fallback; } catch { return fallback; }
49
+ }
50
+ function rows(db, sql, args = []) {
51
+ try { return db.prepare(sql).all(...args); } catch { return []; }
52
+ }
53
+ const shortTs = (v) => (v ? String(v).replace("T", " ").slice(0, 16) : "");
54
+
55
+ /* ── /dashboard — 데스크탑 dashboard 의 관제 패널 집합 ── */
56
+ function dashboard(ui, db, en) {
57
+ const chip = (paint, s) => paint(` ${s} `);
58
+ const local = count(db, "SELECT COUNT(*) n FROM installed_agents WHERE COALESCE(builtin,0)=0 AND COALESCE(visibility,'')!='background'");
59
+ const builtin = count(db, "SELECT COUNT(*) n FROM installed_agents WHERE COALESCE(builtin,0)=1");
60
+ const firms = count(db, "SELECT COUNT(*) n FROM firms");
61
+ const marks = count(db, "SELECT COUNT(*) n FROM hub_agent_bookmarks");
62
+ const borrowed = count(db, "SELECT COUNT(*) n FROM borrowed_agent_careers");
63
+
64
+ ui.ensureNl();
65
+ ui.line(ui.c.bold(en ? "Dashboard" : "대시보드"));
66
+ ui.line(` ${chip(ui.c.inverse, `${en ? "agents" : "에이전트"} ${local}`)} ${chip(ui.c.dim, `builtin ${builtin}`)} ${chip(ui.c.inverse, `${en ? "firms" : "회사"} ${firms}`)} ${chip(ui.c.dim, `${en ? "bookmarks" : "북마크"} ${marks}`)} ${chip(ui.c.dim, `${en ? "borrowed" : "대여"} ${borrowed}`)}`);
67
+
68
+ // ── 확인 필요 (D1 숨은 계약 2: 없으면 실행이 조용히 멈춘 채 정상처럼 보인다) ──
69
+ const pending = count(db, "SELECT COUNT(*) n FROM automation_node_approvals WHERE decision NOT IN ('approved','rejected')");
70
+ const stalled = rows(db,
71
+ `SELECT r.id, a.name, r.status, r.last_activity_at
72
+ FROM automation_runs r LEFT JOIN automations a ON a.id = r.automation_id
73
+ WHERE r.status NOT IN ('ok','error','cancelled') ORDER BY COALESCE(r.last_activity_at,'') DESC LIMIT 5`);
74
+ ui.line("");
75
+ ui.line(ui.c.bold(en ? "Needs attention" : "확인 필요"));
76
+ if (!pending && !stalled.length) {
77
+ ui.line(ui.c.dim(en ? " none — nothing is waiting on you" : " 없음 — 당신을 기다리는 실행이 없습니다"));
78
+ } else {
79
+ if (pending) ui.line(` ${ui.c.amber("!")} ${en ? `${pending} approval(s) waiting` : `승인 대기 ${pending}건`}`);
80
+ for (const s of stalled) {
81
+ ui.line(` ${ui.c.amber("!")} ${s.name || s.automation_id} ${ui.c.dim(`· ${s.status} · ${shortTs(s.last_activity_at)}`)}`);
82
+ }
83
+ }
84
+
85
+ // ── 실행 활동 — 실패를 숨기지 않는다 ──
86
+ const runAgg = rows(db, "SELECT status, COUNT(*) n FROM automation_runs GROUP BY status");
87
+ const recent = rows(db,
88
+ `SELECT r.status, r.started_at, a.name FROM automation_runs r
89
+ LEFT JOIN automations a ON a.id = r.automation_id
90
+ ORDER BY COALESCE(r.started_at,'') DESC LIMIT 5`);
91
+ if (runAgg.length) {
92
+ const failed = runAgg.find((r) => r.status === "error")?.n || 0;
93
+ const total = runAgg.reduce((a, b) => a + b.n, 0);
94
+ ui.line("");
95
+ ui.line(ui.c.bold(en ? "Run activity" : "실행 활동") + " " +
96
+ (failed ? ui.c.amber(en ? `${failed}/${total} failed` : `${total}건 중 ${failed}건 실패`) : ui.c.dim(`${total}`)));
97
+ for (const r of recent) {
98
+ const mark = r.status === "ok" ? ui.c.green("✓") : r.status === "error" ? ui.c.amber("✗") : ui.c.dim("·");
99
+ ui.line(` ${mark} ${r.name || "—"} ${ui.c.dim(shortTs(r.started_at))}`);
100
+ }
101
+ }
102
+
103
+ // ── 자동화 ──
104
+ const autoTotal = db.prepare("SELECT COUNT(*) n, COALESCE(SUM(enabled),0) e FROM automations").get();
105
+ const autos = rows(db, "SELECT name, enabled, next_run_at FROM automations ORDER BY enabled DESC, COALESCE(next_run_at,'') LIMIT 5");
106
+ ui.line("");
107
+ ui.line(ui.c.bold(en ? `Automations ${autoTotal.e}/${autoTotal.n} on` : `자동화 ${autoTotal.e}/${autoTotal.n} 켜짐`));
108
+ for (const a of autos) {
109
+ ui.line(` ${a.enabled ? ui.c.green("●") : ui.c.faint("○")} ${a.name}${a.next_run_at ? ui.c.dim(` · ${en ? "next" : "다음"} ${shortTs(a.next_run_at)}`) : ""}`);
110
+ }
111
+ if (!autos.length) ui.line(ui.c.dim(en ? " (none — /automation add)" : " (없음 — /automation add)"));
112
+
113
+ // ── 사용량 상위 ──
114
+ const usage = rows(db, "SELECT agent_key, use_count, last_used_at FROM agent_usage ORDER BY use_count DESC LIMIT 5");
115
+ if (usage.length) {
116
+ ui.line("");
117
+ ui.line(ui.c.bold(en ? "Most used" : "많이 쓴 에이전트"));
118
+ table(ui, [[en ? "agent" : "에이전트", en ? "runs" : "실행", en ? "last" : "마지막"],
119
+ ...usage.map((u) => [u.agent_key, String(u.use_count), shortTs(u.last_used_at)])], { cap: [34, 6, 16] });
120
+ }
121
+
122
+ // ── 진화 제안 (승인형) ──
123
+ const props = rows(db, "SELECT status, COUNT(*) n FROM agent_evolution_proposals GROUP BY status");
124
+ if (props.length) {
125
+ ui.line("");
126
+ ui.line(ui.c.bold(en ? "Evolution proposals" : "진화 제안") + " " +
127
+ ui.c.dim(props.map((p) => `${p.status} ${p.n}`).join(" · ")));
128
+ }
129
+
130
+ ui.line("");
131
+ ui.line(ui.c.dim(en
132
+ ? "more: /library · /marketplace · /automation list · /usage · /sessions"
133
+ : "더 보기: /library · /marketplace · /automation list · /usage · /sessions"));
134
+ }
135
+
136
+ /* ── /library — 데스크탑 library/agents + env + mcps 를 한 화면으로 ── */
137
+ function library(ui, db, en, ctx) {
138
+ ui.ensureNl();
139
+ ui.line(ui.c.bold(en ? "Library" : "라이브러리"));
140
+
141
+ const agents = rows(db,
142
+ `SELECT slug, COALESCE(local_display_name, name) nm, role, entity_kind, builtin, trust_grade
143
+ FROM installed_agents WHERE COALESCE(visibility,'')!='background'
144
+ ORDER BY COALESCE(builtin,0), slug LIMIT 12`);
145
+ const total = count(db, "SELECT COUNT(*) n FROM installed_agents WHERE COALESCE(visibility,'')!='background'");
146
+ ui.line("");
147
+ ui.line(ui.c.bold(en ? `Agents (${total})` : `에이전트 (${total})`));
148
+ table(ui, [[en ? "slug" : "슬러그", en ? "name" : "이름", en ? "role" : "역할", en ? "kind" : "종류"],
149
+ ...agents.map((a) => [a.slug, a.nm || "", a.role || "", a.builtin ? "builtin" : (a.entity_kind || "agent")])],
150
+ { cap: [26, 24, 12, 10] });
151
+ if (total > agents.length) ui.line(ui.c.dim(en ? ` … ${total - agents.length} more — /agents` : ` … ${total - agents.length}개 더 — /agents`));
152
+
153
+ const mcps = rows(db, "SELECT DISTINCT agent_id FROM agent_mcp_servers LIMIT 1");
154
+ const mcpCount = count(db, "SELECT COUNT(*) n FROM agent_mcp_servers");
155
+ ui.line("");
156
+ ui.line(ui.c.bold(en ? "MCP servers" : "MCP 서버") + " " + ui.c.dim(String(mcpCount)) +
157
+ ui.c.dim(mcps.length ? " · /mcp" : en ? " · none configured — /mcp" : " · 설정 없음 — /mcp"));
158
+
159
+ ui.line("");
160
+ ui.line(ui.c.dim(en
161
+ ? "env vars: /env · credentials: /creds list · plugins: /plugin list"
162
+ : "환경변수: /env · 자격증명: /creds list · 플러그인: /plugin list"));
163
+ void ctx;
164
+ }
165
+
166
+ /* ── /marketplace · /bookmarks — Hub 북마크(로컬)와 검색 안내 ── */
167
+ function marketplace(ui, db, en) {
168
+ ui.ensureNl();
169
+ ui.line(ui.c.bold(en ? "Hub" : "Hub 마켓플레이스"));
170
+ const marks = rows(db,
171
+ "SELECT slug, entity_kind, bookmarked_at, sync_state FROM hub_agent_bookmarks ORDER BY COALESCE(bookmarked_at,'') DESC LIMIT 10");
172
+ const borrowed = rows(db,
173
+ "SELECT slug, COALESCE(name_ko, name_en) nm, use_count, last_used_at FROM borrowed_agent_careers ORDER BY COALESCE(last_used_at,'') DESC LIMIT 5");
174
+
175
+ ui.line("");
176
+ ui.line(ui.c.bold(en ? `Bookmarks (${marks.length})` : `북마크 (${marks.length})`));
177
+ if (marks.length) {
178
+ table(ui, [[en ? "slug" : "슬러그", en ? "kind" : "종류", en ? "saved" : "저장"],
179
+ ...marks.map((m) => [m.slug, m.entity_kind || "agent", shortTs(m.bookmarked_at)])], { cap: [34, 10, 16] });
180
+ } else {
181
+ ui.line(ui.c.dim(en ? " none yet" : " 아직 없음"));
182
+ }
183
+
184
+ if (borrowed.length) {
185
+ ui.line("");
186
+ ui.line(ui.c.bold(en ? "Borrowed (Hub careers)" : "빌려 쓴 에이전트"));
187
+ table(ui, [[en ? "slug" : "슬러그", en ? "name" : "이름", en ? "runs" : "실행", en ? "last" : "마지막"],
188
+ ...borrowed.map((b) => [b.slug, b.nm || "", String(b.use_count || 0), shortTs(b.last_used_at)])],
189
+ { cap: [26, 22, 6, 16] });
190
+ }
191
+
192
+ ui.line("");
193
+ ui.line(ui.c.dim(en
194
+ ? 'search: /search "<what you need>" · install: /install <slug> · credits: /billing'
195
+ : '검색: /search "<필요한 것>" · 설치: /install <slug> · 크레딧: /billing'));
196
+ }
197
+
198
+ /* ── /settings — 데스크탑 settings 의 터미널 관측 (변경은 기존 명령으로) ── */
199
+ function settings(ui, db, en, ctx) {
200
+ const { activeRuntimeRow, listAvailableCliRuntimes } = require("../runtimes/detect.cjs");
201
+ const { resolvedModelRole } = require("../runtimes/roles.cjs");
202
+ const prefs = ctx.prefs || {};
203
+ ui.ensureNl();
204
+ ui.line(ui.c.bold(en ? "Settings" : "설정"));
205
+
206
+ const active = (() => { try { return activeRuntimeRow(db); } catch { return null; } })();
207
+ const clis = (() => { try { return listAvailableCliRuntimes(); } catch { return []; } })();
208
+ const orch = (() => { try { return resolvedModelRole(db, "orchestrator"); } catch { return null; } })();
209
+ const worker = (() => { try { return resolvedModelRole(db, "worker"); } catch { return null; } })();
210
+ const describe = (sel) => (sel ? `${sel.kind === "byok" ? sel.backend || "byok" : sel.kind}${sel.model ? `/${sel.model}` : ""}` : en ? "not set" : "미설정");
211
+
212
+ table(ui, [
213
+ [en ? "setting" : "항목", en ? "value" : "값"],
214
+ [en ? "language" : "언어", prefs.language || ctx.lang || "en"],
215
+ [en ? "permission" : "권한", prefs.permission || "write"],
216
+ [en ? "active runtime" : "활성 런타임", active ? active.kind : (prefs.runtime || (en ? "auto" : "자동"))],
217
+ [en ? "installed CLIs" : "설치된 CLI", clis.map((c) => c.kind).join(", ") || (en ? "none" : "없음")],
218
+ ["orchestrator", describe(orch)],
219
+ ["worker", describe(worker)],
220
+ ], { cap: [18, 52] });
221
+
222
+ ui.line("");
223
+ ui.line(ui.c.dim(en
224
+ ? "change: /setup (wizard) · /runtime · /model · /effort · /permission · roles set · creds · env"
225
+ : "변경: /setup (마법사) · /runtime · /model · /effort · /permission · roles set · creds · env"));
226
+ ui.line(ui.c.dim(en
227
+ ? "Desktop-only here: theme, mobile pairing QR, auto-update, multimodal providers"
228
+ : "여기서 불가(데스크탑 전용): 테마, 모바일 페어링 QR, 자동 업데이트, 멀티모달 프로바이더"));
229
+ }
230
+
231
+ /* ── /projects — 데스크탑 workspace + project/detail ── */
232
+ function projects(ui, db, en) {
233
+ const list = rows(db,
234
+ `SELECT p.id, p.name, p.folder_path, p.source_type,
235
+ (SELECT COUNT(*) FROM chats c WHERE c.project_id = p.id AND c.archived_at IS NULL) chats,
236
+ (SELECT COUNT(*) FROM tasks t WHERE t.project_id = p.id AND t.archived_at IS NULL) tasks,
237
+ p.updated_at
238
+ FROM projects p ORDER BY COALESCE(p.updated_at,'') DESC`);
239
+ ui.ensureNl();
240
+ ui.line(ui.c.bold(en ? `Projects (${list.length})` : `프로젝트 (${list.length})`));
241
+ const cwd = process.cwd();
242
+ if (list.length) {
243
+ table(ui, [[en ? "project" : "프로젝트", en ? "source" : "소스", en ? "chats" : "채팅", en ? "tasks" : "작업", en ? "updated" : "수정"],
244
+ ...list.map((p) => [
245
+ (p.folder_path && cwd.startsWith(p.folder_path) ? "▸ " : " ") + (p.name || p.id),
246
+ p.source_type || "local", String(p.chats), String(p.tasks), shortTs(p.updated_at)])],
247
+ { cap: [30, 10, 6, 6, 16] });
248
+ const here = list.find((p) => p.folder_path && cwd.startsWith(p.folder_path));
249
+ ui.line("");
250
+ ui.line(here
251
+ ? ui.c.dim(en ? `▸ this folder is connected to "${here.name}"` : `▸ 이 폴더는 "${here.name}"에 연결돼 있습니다`)
252
+ : ui.c.amber(en ? "this folder is not connected — /project use <agent>" : "이 폴더는 연결 안 됨 — /project use <에이전트>"));
253
+ } else {
254
+ ui.line(ui.c.dim(en ? " none — /project use <agent>" : " 없음 — /project use <에이전트>"));
255
+ }
256
+ ui.line(ui.c.dim(en
257
+ ? "detail: /project status · team: /project team <agent>… · timeline lives in the project store"
258
+ : "상세: /project status · 팀: /project team <에이전트>… · 타임라인은 프로젝트 저장소에"));
259
+ }
260
+
261
+ /* ── /automations — 데스크탑 automation 목록+상세 ── */
262
+ function automations(ui, db, en, ctx, arg) {
263
+ const name = String(arg || "").trim();
264
+ if (name) {
265
+ const a = db.prepare("SELECT * FROM automations WHERE name = ? OR id = ?").get(name, name);
266
+ if (!a) { ui.line(ui.c.dim(en ? `no automation named "${name}"` : `"${name}" 자동화가 없습니다`)); return; }
267
+ ui.ensureNl();
268
+ ui.line(ui.c.bold(a.name) + " " + (a.enabled ? ui.c.green("● on") : ui.c.faint("○ off")));
269
+ table(ui, [
270
+ [en ? "field" : "항목", en ? "value" : "값"],
271
+ [en ? "schedule" : "일정", a.schedule || (a.trigger_type || "—")],
272
+ [en ? "next run" : "다음 실행", shortTs(a.next_run_at) || "—"],
273
+ [en ? "last run" : "마지막 실행", shortTs(a.last_run_at) || "—"],
274
+ [en ? "runs" : "실행 횟수", String(a.run_count || 0)],
275
+ [en ? "target" : "대상", `${a.target_type || "—"}${a.target_id ? `:${String(a.target_id).slice(0, 20)}` : ""}`],
276
+ [en ? "permission" : "권한", a.execution_permission || "—"],
277
+ [en ? "graph" : "그래프", a.graph_json ? (en ? `yes — /graph show ${a.name}` : `있음 — /graph show ${a.name}`) : (en ? "no" : "없음")],
278
+ ], { cap: [16, 54] });
279
+ const runs = rows(db,
280
+ "SELECT status, started_at FROM automation_runs WHERE automation_id = ? ORDER BY COALESCE(started_at,'') DESC LIMIT 6", [a.id]);
281
+ if (runs.length) {
282
+ ui.line("");
283
+ ui.line(ui.c.bold(en ? "Recent runs" : "최근 실행"));
284
+ for (const r of runs) {
285
+ const mark = r.status === "ok" ? ui.c.green("✓") : r.status === "error" ? ui.c.amber("✗") : ui.c.dim("·");
286
+ ui.line(` ${mark} ${r.status} ${ui.c.dim(shortTs(r.started_at))}`);
287
+ }
288
+ }
289
+ ui.line("");
290
+ ui.line(ui.c.dim(en
291
+ ? `run now: /automation run ${a.name} · toggle: /automation ${a.enabled ? "off" : "on"} ${a.name}`
292
+ : `지금 실행: /automation run ${a.name} · ${a.enabled ? "끄기: /automation off" : "켜기: /automation on"} ${a.name}`));
293
+ return;
294
+ }
295
+ const list = rows(db,
296
+ `SELECT a.name, a.enabled, a.schedule, a.next_run_at, a.run_count,
297
+ (SELECT COUNT(*) FROM automation_runs r WHERE r.automation_id = a.id AND r.status='error') fails
298
+ FROM automations a ORDER BY a.enabled DESC, COALESCE(a.next_run_at,'')`);
299
+ ui.ensureNl();
300
+ ui.line(ui.c.bold(en ? `Automations (${list.length})` : `자동화 (${list.length})`));
301
+ if (!list.length) { ui.line(ui.c.dim(en ? " none — /automation add" : " 없음 — /automation add")); return; }
302
+ table(ui, [[" ", en ? "name" : "이름", en ? "schedule" : "일정", en ? "next" : "다음", en ? "runs" : "실행", en ? "fails" : "실패"],
303
+ ...list.map((a) => [a.enabled ? "●" : "○", a.name, a.schedule || "—",
304
+ shortTs(a.next_run_at) || "—", String(a.run_count || 0), a.fails ? String(a.fails) : ""])],
305
+ { cap: [1, 30, 14, 16, 5, 5] });
306
+ ui.line("");
307
+ ui.line(ui.c.dim(en
308
+ ? "detail: /automations <name> · graph: /graph show <name> · add: /automation add"
309
+ : "상세: /automations <이름> · 그래프: /graph show <이름> · 추가: /automation add"));
310
+ void ctx;
311
+ }
312
+
313
+ /* ── /firms — 데스크탑 firm/detail (조직도) ── */
314
+ function firms(ui, db, en, ctx, arg) {
315
+ const name = String(arg || "").trim();
316
+ if (name) {
317
+ const f = db.prepare("SELECT * FROM firms WHERE slug = ? OR name = ?").get(name, name);
318
+ if (!f) { ui.line(ui.c.dim(en ? `no firm named "${name}"` : `"${name}" 회사가 없습니다`)); return; }
319
+ ui.ensureNl();
320
+ ui.line(ui.c.bold(f.name || f.slug) + (f.tagline ? ui.c.dim(` · ${f.tagline}`) : ""));
321
+ let org = null;
322
+ try { org = JSON.parse(f.org_chart_json || "null"); } catch { org = null; }
323
+ const members = rows(db,
324
+ "SELECT slug, COALESCE(local_display_name, name) nm, role FROM installed_agents WHERE parent_team_id = ? ORDER BY role, slug", [f.id]);
325
+ if (members.length) {
326
+ ui.line("");
327
+ ui.line(ui.c.bold(en ? `Roster (${members.length})` : `구성원 (${members.length})`));
328
+ table(ui, [[en ? "slug" : "슬러그", en ? "name" : "이름", en ? "role" : "역할"],
329
+ ...members.map((m) => [m.slug, m.nm || "", m.role || ""])], { cap: [30, 26, 14] });
330
+ } else if (org) {
331
+ ui.line(ui.c.dim(en ? " roster is declared in the org chart only" : " 로스터가 조직도 선언에만 있습니다"));
332
+ }
333
+ const chats = count(db, "SELECT COUNT(*) n FROM chats WHERE firm_id = ? AND archived_at IS NULL", );
334
+ void chats;
335
+ ui.line("");
336
+ ui.line(ui.c.dim(en ? `run: /firm ${f.slug} "<task>"` : `실행: /firm ${f.slug} "<할 일>"`));
337
+ return;
338
+ }
339
+ const list = rows(db,
340
+ `SELECT f.slug, f.name, f.tagline,
341
+ (SELECT COUNT(*) FROM installed_agents a WHERE a.parent_team_id = f.id) members
342
+ FROM firms f ORDER BY f.slug`);
343
+ ui.ensureNl();
344
+ ui.line(ui.c.bold(en ? `Firms (${list.length})` : `회사 (${list.length})`));
345
+ if (!list.length) { ui.line(ui.c.dim(en ? " none" : " 없음")); return; }
346
+ table(ui, [[en ? "slug" : "슬러그", en ? "name" : "이름", en ? "members" : "구성원"],
347
+ ...list.map((f) => [f.slug, f.name || "", String(f.members)])], { cap: [30, 30, 8] });
348
+ ui.line("");
349
+ ui.line(ui.c.dim(en ? "detail: /firms <slug>" : "상세: /firms <슬러그>"));
350
+ void ctx;
351
+ }
352
+
353
+ module.exports = { dashboard, library, marketplace, settings, projects, automations, firms, table };
354
+
@@ -1,16 +1,18 @@
1
1
  "use strict";
2
2
  /*
3
- * ui/pitui-shell — pi-tui 기반 실험 셸 (D3 Phase 2 증분 1, 2026-08-11).
3
+ * ui/shell — Agentlas 대화형 셸 (D3 Phase 2~3, 2026-08-11).
4
4
  *
5
- * 켜는 법: AGENTLAS_TUI=pi agentlas (TTY 필수 · 기본 REPL은 그대로 정본)
5
+ * 켜는 법: AGENTLAS_TUI=1 agentlas (TTY 필수 · 기본 REPL은 그대로 정본)
6
+ *
7
+ * 렌더러 의존은 구현 세부다 — 사용자 문구·환경변수·명령 어디에도 상류 이름을 쓰지 않는다.
6
8
  *
7
9
  * 설계 (D2 위험 5의 해법이 이 파일의 구조다):
8
- * - PiUi 는 기존 Ui 를 상속하되 write() 초크포인트만 pi-tui 트랜스크립트로 돌린다.
10
+ * - PiUi 는 기존 Ui 를 상속하되 write() 초크포인트만 렌더러 트랜스크립트로 돌린다.
9
11
  * line/_message/tool/rule 등 기존 메서드는 전부 write 로 수렴하므로 그대로 산다.
10
12
  * - 스트리밍 3종은 Markdown 누적으로 교체 — 표·코드블록이 실시간 재렌더된다.
11
- * - 스피너는 pi-tui Loader 로 교체 (기존 \r 기반 페인트는 dummy 스트림으로 무해화).
13
+ * - 스피너는 렌더러 Loader 로 교체 (기존 \r 기반 페인트는 dummy 스트림으로 무해화).
12
14
  * - ctx.out/err 55파일의 직출력은 shellCtx 재지정으로 전부 프레임 안에 들어온다.
13
- * - 자동완성은 ui/palette 정본(SLASH_COMMANDS)을 pi-tui SlashCommand 로 변환 — 목록 드리프트 금지.
15
+ * - 자동완성은 ui/palette 정본(SLASH_COMMANDS)을 렌더러 SlashCommand 로 변환 — 목록 드리프트 금지.
14
16
  *
15
17
  * 증분 1 범위 밖(기본 REPL로): Shift-Tab 권한 순환 · ! 셸 · 세션 전환(/s) ·
16
18
  * 히스토리 디스크 영속 · 스티어링 큐 표시. 이 항목들은 D3 Phase 2-2에서 이전한다.
@@ -25,20 +27,20 @@ const palette = require("./palette.cjs");
25
27
  const { readVersion } = require("../agentlas-banner.cjs");
26
28
  const { resolveProjectController, withProjectControllerContext } = require("../project/controller.cjs");
27
29
 
28
- function loadPiTui() {
30
+ function loadRenderer() {
29
31
  try {
30
32
  // ESM 패키지 — Node >=20.19 의 require(esm). engines 가 이 최소선을 선언한다.
31
33
  return require("@earendil-works/pi-tui");
32
34
  } catch (cause) {
33
35
  throw Object.assign(
34
- new Error("pi-tui shell needs Node >=20.19 (require(esm)). Run without AGENTLAS_TUI=pi, or upgrade Node."),
35
- { code: "pitui_unavailable", cause },
36
+ new Error("The Agentlas shell needs Node >=20.19. Run without AGENTLAS_TUI=1, or upgrade Node."),
37
+ { code: "shell_unavailable", cause },
36
38
  );
37
39
  }
38
40
  }
39
41
 
40
- /* Ui 를 상속해 write 초크포인트만 pi-tui 돌린다. */
41
- class PiUi extends Ui {
42
+ /* Ui 를 상속해 write 초크포인트만 렌더러로 돌린다. */
43
+ class ShellUi extends Ui {
42
44
  constructor(opts, pi, tui, transcript) {
43
45
  // 실터미널 대신 dummy 스트림 — 놓친 직접 out.write 가 프레임을 찢는 대신 소멸한다.
44
46
  super({ ...opts, stream: new PassThrough(), color: true });
@@ -127,23 +129,43 @@ class PiUi extends Ui {
127
129
  }
128
130
  }
129
131
 
132
+ /*
133
+ * 셸 전용 화면 (D3 Phase 3). 공용 팔레트(ui/palette)에는 넣지 않는다 —
134
+ * 그 정본은 기본 REPL 이 실제로 처리하는 명령만 광고해야 하고,
135
+ * palette-command-coverage-contract 가 그 계약을 잠근다.
136
+ */
137
+ const SHELL_SCREENS = [
138
+ { name: "dashboard", ko: "관제 대시보드 — 확인 필요·실행 활동·자동화", en: "Dashboard — attention, run activity, automations" },
139
+ { name: "library", ko: "라이브러리 — 에이전트·MCP", en: "Library — agents, MCP" },
140
+ { name: "marketplace", ko: "Hub — 북마크·대여 현황", en: "Hub — bookmarks and borrows" },
141
+ { name: "settings", ko: "설정 현황", en: "Settings overview" },
142
+ { name: "projects", ko: "프로젝트 목록 — 채팅·작업 수", en: "Projects — chats and tasks" },
143
+ { name: "automations", ko: "자동화 목록·상세 [이름]", en: "Automations list/detail [name]" },
144
+ { name: "firms", ko: "회사(팀) 목록·조직도 [슬러그]", en: "Firms and rosters [slug]" },
145
+ ];
146
+
130
147
  function toSlashCommands(lang) {
131
- // 팔레트 정본 → pi-tui SlashCommand. "/" 접두는 pi-tui 관리하므로 벗긴다.
132
- return palette.SLASH_COMMANDS.map((cmd) => ({
148
+ // 팔레트 정본 → 렌더러 SlashCommand. "/" 접두는 렌더러가 관리하므로 벗긴다.
149
+ const base = palette.SLASH_COMMANDS.map((cmd) => ({
133
150
  name: cmd.command.slice(1),
134
151
  description: lang === "ko" ? cmd.ko : cmd.en,
135
152
  argumentHint: cmd.args || undefined,
136
153
  }));
154
+ const seen = new Set(base.map((c) => c.name));
155
+ for (const s of SHELL_SCREENS) {
156
+ if (!seen.has(s.name)) base.push({ name: s.name, description: lang === "ko" ? s.ko : s.en });
157
+ }
158
+ return base;
137
159
  }
138
160
 
139
- async function startPiShell(ctx, opts = {}) {
140
- const pi = loadPiTui();
161
+ async function startShell(ctx, opts = {}) {
162
+ const pi = loadRenderer();
141
163
  const en = ctx.lang === "en";
142
164
  const db = ctx.db();
143
165
 
144
166
  const terminal = new pi.ProcessTerminal();
145
167
  const tui = new pi.TuiMainScreen(terminal);
146
- const ui = new PiUi({ lang: ctx.lang }, pi, tui, tui);
168
+ const ui = new ShellUi({ lang: ctx.lang }, pi, tui, tui);
147
169
 
148
170
  // ctx 초크포인트 재지정 — 55파일의 ctx.out 직출력이 전부 프레임 안으로 들어온다.
149
171
  const shellCtx = {
@@ -185,7 +207,7 @@ async function startPiShell(ctx, opts = {}) {
185
207
  };
186
208
 
187
209
  // ── 헤더 ──
188
- ui.line(`${ui.c.paw("▞▖")} ${ui.c.bold("AGENTLAS")} ${ui.c.dim(`${readVersion()} · pi-tui shell (experimental) · parallel ≤${maxParallel()}`)}`);
210
+ ui.line(`${ui.c.paw("▞▖")} ${ui.c.bold("AGENTLAS")} ${ui.c.dim(`${readVersion()} · parallel ≤${maxParallel()}`)}`);
189
211
  ui.line(ui.c.dim(en
190
212
  ? "plain words run a task · / commands · Esc interrupts · Ctrl+C quits"
191
213
  : "문장을 치면 실행 · / 명령 · Esc 중단 · Ctrl+C 종료"));
@@ -260,33 +282,21 @@ async function startPiShell(ctx, opts = {}) {
260
282
  } catch { /* 렌더 실패 → 아래 클래식 폴스루가 텍스트로 보여준다 */ }
261
283
  }
262
284
  }
263
- // 대시보드 (Phase 3-2) — 데스크탑 정직 정지 13종 첫 해제. 로컬 DB 직조회.
264
- if (cmd === "dashboard") {
265
- const chip = (paint, s) => paint(` ${s} `);
266
- const agents = db.prepare(
267
- "SELECT COALESCE(NULLIF(entity_kind,''),'agent') k, builtin, COUNT(*) n FROM installed_agents WHERE COALESCE(visibility,'')!='background' GROUP BY k, builtin").all();
268
- const firms = db.prepare("SELECT COUNT(*) n FROM firms").get().n;
269
- const autos = db.prepare(
270
- "SELECT name, enabled, last_run_at FROM automations ORDER BY COALESCE(last_run_at,'') DESC LIMIT 5").all();
271
- const autoTotal = db.prepare("SELECT COUNT(*) n, SUM(enabled) e FROM automations").get();
272
- const tg = db.prepare("SELECT COUNT(*) n FROM telegram_bindings").get().n;
273
- ui.ensureNl();
274
- ui.line(ui.c.bold(en ? "Dashboard" : "대시보드"));
275
- const local = agents.filter(a => !a.builtin).reduce((s, a) => s + a.n, 0);
276
- const builtin = agents.filter(a => a.builtin).reduce((s, a) => s + a.n, 0);
277
- ui.line(` ${chip(ui.c.inverse, en ? `agents ${local}` : `에이전트 ${local}`)} ${chip(ui.c.dim, `builtin ${builtin}`)} ${chip(ui.c.inverse, en ? `firms ${firms}` : `회사 ${firms}`)} ${chip(ui.c.dim, `telegram ${tg}`)}`);
278
- ui.line("");
279
- ui.line(ui.c.bold(en ? `Automations ${autoTotal.e || 0}/${autoTotal.n} on` : `자동화 ${autoTotal.e || 0}/${autoTotal.n} 켜짐`));
280
- for (const a of autos) {
281
- const st = a.enabled ? ui.c.green("●") : ui.c.faint("○");
282
- ui.line(` ${st} ${a.name}${a.last_run_at ? ui.c.dim(` · ${String(a.last_run_at).slice(0, 16)}`) : ""}`);
283
- }
284
- if (!autos.length) ui.line(ui.c.dim(en ? " (none — /automation add)" : " (없음 — /automation add)"));
285
- ui.line("");
286
- ui.line(ui.c.dim(en
287
- ? "details: /agents · /automation list · /usage · /sessions"
288
- : "자세히: /agents · /automation list · /usage · /sessions"));
289
- return;
285
+ // 데스크탑 대응 화면 (Phase 3) — 정직 정지였던 표면들을 실물로 대체
286
+ {
287
+ const screens = require("./screens.cjs");
288
+ const SCREEN = {
289
+ dashboard: screens.dashboard,
290
+ library: screens.library,
291
+ marketplace: screens.marketplace,
292
+ bookmarks: screens.marketplace,
293
+ settings: screens.settings,
294
+ projects: screens.projects,
295
+ automations: screens.automations,
296
+ firms: screens.firms,
297
+ };
298
+ // 인자 있는 화면(automations/firms 상세) restStr 그대로 넘긴다.
299
+ if (SCREEN[cmd]) { SCREEN[cmd](ui, db, en, shellCtx, cmdline.slice(raw.length).trim()); return; }
290
300
  }
291
301
  // 세션 관찰/전환 (증분 2b) — 기본 REPL과 같은 orch/renderer 배선
292
302
  if (cmd === "sessions" || cmd === "tree") {
@@ -317,7 +327,7 @@ async function startPiShell(ctx, opts = {}) {
317
327
  ui.line(ui.c.dim(commands.DESKTOP_ONLY_SURFACES[cmd]));
318
328
  return;
319
329
  }
320
- ui.line(ui.c.dim(en ? `not in the pi shell yet: /${cmd} — use the classic REPL` : `pi 셸에는 아직 없음: /${cmd} — 기본 REPL을 쓰세요`));
330
+ ui.line(ui.c.dim(en ? `not available here yet: /${cmd}` : `여기서는 아직 됩니다: /${cmd}`));
321
331
  };
322
332
 
323
333
  let busy = false;
@@ -367,7 +377,7 @@ async function startPiShell(ctx, opts = {}) {
367
377
  };
368
378
 
369
379
  tui.addInputListener((data) => {
370
- // Shift-Tab 권한 순환 — pi-tui raw mode 를 단독 소유하므로 readline 의
380
+ // Shift-Tab 권한 순환 — 렌더러가 raw mode 를 단독 소유하므로 readline 의
371
381
  // swallowCompletion 우회 없이 여기서 직접 소비한다 (D2 위험 2의 해소 형태).
372
382
  if (pi.matchesKey(data, "shift+tab")) {
373
383
  permShortcut.handleKey("", { name: "tab", shift: true });
@@ -397,8 +407,8 @@ async function startPiShell(ctx, opts = {}) {
397
407
  });
398
408
 
399
409
  tui.start();
400
- // pi-tui 프로세스를 잡고 있는 동안 살아 있는 프라미스
410
+ // 렌더러가 프로세스를 잡고 있는 동안 살아 있는 프라미스
401
411
  return new Promise(() => {});
402
412
  }
403
413
 
404
- module.exports = { startPiShell };
414
+ module.exports = { startShell };
@@ -3,7 +3,7 @@
3
3
  * ui/width: 터미널 셀 폭·그래핌 유틸 정본.
4
4
  * agentlas-composer.cjs(데드코드가 된 바텀 입력 박스)에서 2026-08-11 추출 — 함수는
5
5
  * 바이트 동일. 소비자: agentlas-ui / agentlas-banner / agentlas-onboard /
6
- * ui/repl / hephaestus/runtime. pi-tui 이행(D3 Phase 1-3) 정지작업.
6
+ * ui/repl / hephaestus/runtime. 이행(D3 Phase 1-3) 정지작업.
7
7
  */
8
8
  const GRAPHEME_SEGMENTER = new Intl.Segmenter(undefined, { granularity: "grapheme" });
9
9
  const MARK_RE = /\p{Mark}/u;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentlas",
3
- "version": "1.0.40",
3
+ "version": "1.0.42",
4
4
  "description": "Agentlas project terminal — run project controllers and task-scoped agent teams from the terminal. Standalone: no desktop app process required.",
5
5
  "bin": {
6
6
  "agentlas": "bin/agentlas.cjs"