agentlas 0.4.0

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.
@@ -0,0 +1,199 @@
1
+ "use strict";
2
+
3
+ const fs = require("node:fs");
4
+ const path = require("node:path");
5
+ const os = require("node:os");
6
+ const crypto = require("node:crypto");
7
+
8
+ const TEXT_EXTS = new Set([".md", ".txt", ".json", ".jsonl", ".yaml", ".yml", ".toml", ".js", ".ts", ".tsx", ".cjs", ".mjs", ".sh"]);
9
+ const SECRET_RE = /(sk-(?:ant-)?[A-Za-z0-9_-]{20,}|gh[opsu]_[A-Za-z0-9_]{20,}|AKIA[0-9A-Z]{16}|AIza[0-9A-Za-z_-]{35}|-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----)/;
10
+ const SECRET_ASSIGN_RE = /\b(?:api[_-]?key|secret|password|token)\s*[:=]\s*["']?([A-Za-z0-9+/=_-]{20,})["']?/i;
11
+ const PROMPT_INJECTION_RE = /\b(ignore (?:all |previous |prior )?instructions|reveal (?:your )?system prompt|print hidden instructions)\b/i;
12
+ const DESTRUCTIVE_RE = /\b(rm\s+-rf\s+(?:\/|~)|curl\b[^\n]{0,240}\|\s*(?:sudo\s+)?(?:sh|bash|zsh)|mkfs\.|dd\s+if=\/dev\/)\b/i;
13
+
14
+ function collectFiles(root) {
15
+ const base = path.resolve(root);
16
+ const files = [];
17
+ function walk(dir) {
18
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
19
+ if (entry.name.startsWith("._")) continue;
20
+ const abs = path.join(dir, entry.name);
21
+ const rel = path.relative(base, abs).split(path.sep).join("/");
22
+ if (entry.isDirectory()) {
23
+ if ([".git", ".next", "node_modules", "dist", "out", "release"].includes(entry.name)) continue;
24
+ walk(abs);
25
+ continue;
26
+ }
27
+ if (!entry.isFile()) continue;
28
+ const ext = path.extname(entry.name).toLowerCase();
29
+ if (ext && !TEXT_EXTS.has(ext) && !["AGENTS.md", "CLAUDE.md", "GEMINI.md", "agent.md", "README.md"].includes(entry.name)) continue;
30
+ try {
31
+ files.push({ path: rel, content: fs.readFileSync(abs, "utf8") });
32
+ } catch {
33
+ // Skip non-text files.
34
+ }
35
+ }
36
+ }
37
+ walk(base);
38
+ return files.sort((a, b) => a.path.localeCompare(b.path));
39
+ }
40
+
41
+ function hashPackage(files) {
42
+ const h = crypto.createHash("sha256");
43
+ for (const file of files) {
44
+ if (file.path === "agentlas.json") continue;
45
+ h.update(file.path);
46
+ h.update("\0");
47
+ h.update(file.content);
48
+ h.update("\0");
49
+ }
50
+ return `sha256:${h.digest("hex")}`;
51
+ }
52
+
53
+ function inferEntry(files) {
54
+ const paths = new Set(files.map((file) => file.path));
55
+ return ["AGENTS.md", "agent.md", "CLAUDE.md", "README.md"].find((candidate) => paths.has(candidate)) || files[0]?.path || "AGENTS.md";
56
+ }
57
+
58
+ function inferSkills(files) {
59
+ const skills = files.map((file) => file.path.match(/(?:^|\/)skills\/([^/]+)\/SKILL\.md$/)?.[1]).filter(Boolean);
60
+ return [...new Set(skills)].sort().length ? [...new Set(skills)].sort() : ["agentlas-package"];
61
+ }
62
+
63
+ function buildManifest(root, options = {}) {
64
+ const files = collectFiles(root);
65
+ const name = options.name || path.basename(path.resolve(root));
66
+ return {
67
+ schemaVersion: "1.0",
68
+ name,
69
+ packageHash: hashPackage(files),
70
+ runtimeBundleVersion: "1.0",
71
+ entry: inferEntry(files),
72
+ skills: inferSkills(files),
73
+ toolPermissions: { network: "ask", shell: "deny", fileRead: "manifest-allowlist" },
74
+ memoryPolicy: { writeBack: "ask", publicCopy: "reset" },
75
+ memory: files.filter((file) => [".agentlas/memory-map.json", ".agentlas/agent-card.json"].includes(file.path)).map((file) => file.path),
76
+ allowRead: ["README.md", "AGENTS.md", "agent.md", "skills/**", ".agentlas/*.json"],
77
+ denyRead: [".env", ".env.*", "**/secrets/**", "**/credentials/**", "**/cookies/**", "**/*token*", "**/*secret*"],
78
+ publicExportPolicy: "clean-copy",
79
+ requiredRuntime: ["mcp-client"],
80
+ license: "call-only-default",
81
+ createdBy: "agentlas-desktop-setup-wizard",
82
+ };
83
+ }
84
+
85
+ function redact(text) {
86
+ return text.replace(SECRET_RE, "[REDACTED_SECRET]").replace(SECRET_ASSIGN_RE, (match, secret) => match.replace(secret, "[REDACTED_SECRET]"));
87
+ }
88
+
89
+ function scanFiles(files) {
90
+ const findings = [];
91
+ function add(verdict, type, file, line, message) {
92
+ findings.push({ verdict, type, path: file.path, ...(line ? { line } : {}), message, redacted: true });
93
+ }
94
+ for (const file of files) {
95
+ if ([".env", ".env.local"].includes(file.path) || /(?:^|\/)(secrets|credentials|cookies)\//i.test(file.path) || /token|secret/i.test(file.path)) {
96
+ add("BLOCK", "credential-path", file, null, "Credential-like file path is excluded from Cloud package and public publish.");
97
+ }
98
+ file.content.split(/\r?\n/).forEach((line, index) => {
99
+ if (SECRET_RE.test(line) || SECRET_ASSIGN_RE.test(line)) add("BLOCK", "secret-like-value", file, index + 1, "Secret-like value detected and redacted.");
100
+ if (PROMPT_INJECTION_RE.test(line)) add("WARN", "prompt-injection", file, index + 1, "Prompt-injection style instruction needs review.");
101
+ if (DESTRUCTIVE_RE.test(line)) add("WARN", "destructive-command", file, index + 1, "Destructive or remote shell command needs review before execution.");
102
+ });
103
+ }
104
+ const verdict = findings.some((finding) => finding.verdict === "BLOCK") ? "BLOCK" : findings.some((finding) => finding.verdict === "WARN") ? "WARN" : "PASS";
105
+ return { verdict, scannedAt: new Date().toISOString(), findings };
106
+ }
107
+
108
+ function scanFolder(root) {
109
+ return scanFiles(collectFiles(root));
110
+ }
111
+
112
+ function runWizard(root, options = {}) {
113
+ const base = path.resolve(root);
114
+ const files = collectFiles(base);
115
+ const manifest = buildManifest(base, options);
116
+ const scanReport = scanFiles(files);
117
+ fs.writeFileSync(path.join(base, "agentlas.json"), JSON.stringify(manifest, null, 2) + "\n", "utf8");
118
+ const agentlasDir = path.join(base, ".agentlas");
119
+ fs.mkdirSync(agentlasDir, { recursive: true });
120
+ fs.writeFileSync(path.join(agentlasDir, "security-scan.json"), JSON.stringify(scanReport, null, 2) + "\n", "utf8");
121
+ const status = scanReport.verdict === "BLOCK" ? "Blocked" : "Ready for MCP call";
122
+ return {
123
+ status,
124
+ manifest,
125
+ scanReport,
126
+ stateTransitionLog: ["Started setup wizard", "Generated agentlas.json", `Security scan: ${scanReport.verdict}`, status],
127
+ blockers: status === "Blocked" ? ["Security scan blocked package upload."] : [],
128
+ };
129
+ }
130
+
131
+ function loadManifest(root) {
132
+ return JSON.parse(fs.readFileSync(path.join(path.resolve(root), "agentlas.json"), "utf8"));
133
+ }
134
+
135
+ function matches(filePath, pattern) {
136
+ const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*");
137
+ return new RegExp(`^${escaped}$`, "i").test(filePath);
138
+ }
139
+
140
+ function compileBundle(root) {
141
+ const manifest = loadManifest(root);
142
+ const files = collectFiles(root);
143
+ const byPath = new Map(files.map((file) => [file.path, file]));
144
+ const entry = byPath.get(manifest.entry) || byPath.get("AGENTS.md");
145
+ if (!entry) throw new Error(`Entry file not found: ${manifest.entry}`);
146
+ const scanReport = scanFiles(files);
147
+ return {
148
+ schemaVersion: "1.0",
149
+ agent: manifest.name,
150
+ packageHash: manifest.packageHash,
151
+ entry: { path: entry.path, content: redact(entry.content).slice(0, 8000) },
152
+ skills: manifest.skills,
153
+ toolPermissions: manifest.toolPermissions,
154
+ memoryPolicy: manifest.memoryPolicy,
155
+ memorySummary: (manifest.memory || []).map((memoryPath) => byPath.get(memoryPath)).filter(Boolean).map((file) => `${file.path}: ${redact(file.content).replace(/\s+/g, " ").slice(0, 480)}`),
156
+ securityWarnings: scanReport.findings.map((finding) => `${finding.verdict}:${finding.type}:${finding.path}`),
157
+ lazyRead: { tool: "agentlas.read_agent_file", allowedPatterns: manifest.allowRead, deniedPatterns: manifest.denyRead },
158
+ };
159
+ }
160
+
161
+ function readAgentFile(root, requestedPath) {
162
+ const manifest = loadManifest(root);
163
+ if ((manifest.denyRead || []).some((pattern) => matches(requestedPath, pattern))) {
164
+ return { status: "denied", path: requestedPath, reason: "Denied by agentlas.json denyRead.", redacted: true };
165
+ }
166
+ if (!(manifest.allowRead || []).some((pattern) => matches(requestedPath, pattern))) {
167
+ return { status: "denied", path: requestedPath, reason: "Path is not in agentlas.json allowRead.", redacted: false };
168
+ }
169
+ const abs = path.join(path.resolve(root), requestedPath);
170
+ if (!fs.existsSync(abs)) return { status: "missing", path: requestedPath, reason: "File not found." };
171
+ const raw = fs.readFileSync(abs, "utf8");
172
+ const content = redact(raw);
173
+ return { status: "allowed", path: requestedPath, content, redacted: content !== raw };
174
+ }
175
+
176
+ function runFieldTest() {
177
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "agentlas-desktop-cloud-field-"));
178
+ const agent = path.join(root, "mac_a", "instagram-operator");
179
+ fs.mkdirSync(path.join(agent, "skills", "social-media-strategist"), { recursive: true });
180
+ fs.mkdirSync(path.join(agent, ".agentlas"), { recursive: true });
181
+ fs.writeFileSync(path.join(agent, "AGENTS.md"), "# Instagram Operator\n\nBuild weekly Instagram posts.\n", "utf8");
182
+ fs.writeFileSync(path.join(agent, "skills", "social-media-strategist", "SKILL.md"), "---\nname: social-media-strategist\ndescription: Use for social content.\n---\n\nCreate social plans.\n", "utf8");
183
+ fs.writeFileSync(path.join(agent, ".agentlas", "memory-map.json"), "{\"project\":\"instagram-operator\"}\n", "utf8");
184
+ const wizard = runWizard(agent, { name: "instagram-operator" });
185
+ const bundle = compileBundle(agent);
186
+ const allowed = readAgentFile(agent, "AGENTS.md");
187
+ const denied = readAgentFile(agent, ".env");
188
+ const ledger = [{ agentId: "agent_public_instagram", callerId: "other_user", creatorId: "creator", version: "1.0.0", status: "PASS", mode: "public-call-only" }];
189
+ const scenarios = [
190
+ { id: "E1", status: wizard.status === "Ready for MCP call" ? "PASS" : "FAIL", evidence: ["agentlas.json", ".agentlas/security-scan.json"], blockers: wizard.blockers },
191
+ { id: "E2", status: bundle.entry.path === "AGENTS.md" && allowed.status === "allowed" && denied.status === "denied" ? "PASS" : "FAIL", evidence: ["runtime-bundle", "lazy-read"], blockers: [] },
192
+ { id: "E3", status: ledger[0].status === "PASS" ? "PASS" : "FAIL", evidence: ["mock-call-only-ledger"], blockers: [] },
193
+ ];
194
+ const report = { suite: "agentlas-desktop-cloud-field-test", status: scenarios.every((item) => item.status === "PASS") ? "PASS" : "FAIL", scenarios, ledger };
195
+ fs.rmSync(root, { recursive: true, force: true });
196
+ return report;
197
+ }
198
+
199
+ module.exports = { buildManifest, scanFolder, runWizard, compileBundle, readAgentFile, runFieldTest };
@@ -0,0 +1,256 @@
1
+ "use strict";
2
+ /*
3
+ * agentlas-composer: a raw-mode bottom input box (Claude Code / Hermes style).
4
+ *
5
+ * โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ
6
+ * โ”‚ โ€บ your message โ”‚
7
+ * โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ
8
+ * claude-code ยท full ยท 12.3k tok ยท / for commands
9
+ * (slash suggestions render here while typing /โ€ฆ)
10
+ *
11
+ * Single-line field with horizontal scroll (fixed 3-line box โ†’ flicker-free clear/redraw).
12
+ * Full line editing, persisted history, Tab/path/slash completion, slash palette.
13
+ * Zero external deps. Caller falls back to readline when stdin/stdout is not a TTY.
14
+ */
15
+ const readline = require("node:readline");
16
+
17
+ // East-Asian width: CJK / Hangul / Kana / fullwidth glyphs occupy 2 terminal cells.
18
+ function isWide(cp) {
19
+ return (
20
+ (cp >= 0x1100 && cp <= 0x115f) || // Hangul Jamo
21
+ (cp >= 0x2e80 && cp <= 0x303e) || // CJK radicals โ€ฆ symbols
22
+ (cp >= 0x3041 && cp <= 0x33ff) || // Hiragana โ€ฆ CJK compat
23
+ (cp >= 0x3400 && cp <= 0x4dbf) || // CJK ext A
24
+ (cp >= 0x4e00 && cp <= 0x9fff) || // CJK unified
25
+ (cp >= 0xa000 && cp <= 0xa4cf) || // Yi
26
+ (cp >= 0xac00 && cp <= 0xd7a3) || // Hangul syllables
27
+ (cp >= 0xf900 && cp <= 0xfaff) || // CJK compat ideographs
28
+ (cp >= 0xfe30 && cp <= 0xfe4f) || // CJK compat forms
29
+ (cp >= 0xff00 && cp <= 0xff60) || // Fullwidth forms
30
+ (cp >= 0xffe0 && cp <= 0xffe6) ||
31
+ (cp >= 0x1f300 && cp <= 0x1faff) // emoji / pictographs
32
+ );
33
+ }
34
+ function charWidth(ch) {
35
+ const cp = ch.codePointAt(0);
36
+ if (cp < 0x20) return 0;
37
+ return isWide(cp) ? 2 : 1;
38
+ }
39
+ function visWidth(s) {
40
+ const clean = String(s).replace(/\x1b\[[0-9;]*m/g, "");
41
+ let n = 0;
42
+ for (const ch of clean) n += charWidth(ch);
43
+ return n;
44
+ }
45
+
46
+ function createComposer(opts) {
47
+ const out = opts.stream || process.stdout;
48
+ const inp = opts.input || process.stdin;
49
+ const ui = opts.ui;
50
+ const c = ui.c;
51
+ const loadHistory = opts.loadHistory || (() => []);
52
+ const saveHistory = opts.saveHistory || (() => {});
53
+ let history = (loadHistory() || []).filter((x) => typeof x === "string"); // index 0 = most recent
54
+
55
+ function cols() {
56
+ return Math.max(30, out.columns || process.stdout.columns || 80);
57
+ }
58
+ function boxWidth() {
59
+ return Math.min(cols() - 1, 120);
60
+ }
61
+
62
+ // Build the rendered block (array of lines) + the cursor target column on the input line.
63
+ function frame(state, ctx) {
64
+ const w = boxWidth();
65
+ const inner = w - 2; // chars between โ”‚ โ€ฆ โ”‚
66
+ const glyph = " " + (ctx.glyph || "โ€บ") + " "; // " โ€บ "
67
+ const glyphW = visWidth(glyph);
68
+ const fieldW = Math.max(8, inner - glyphW);
69
+
70
+ // horizontal scroll by visual width โ€” keep the cursor visible (CJK-safe)
71
+ let start = Math.min(state.scroll, state.cur);
72
+ while (start < state.cur && visWidth(state.buf.slice(start, state.cur)) > fieldW - 1) start++;
73
+ state.scroll = start;
74
+
75
+ let shown = "";
76
+ let ww = 0;
77
+ for (let i = start; i < state.buf.length; ) {
78
+ const ch = state.buf.codePointAt(i) > 0xffff ? state.buf.slice(i, i + 2) : state.buf[i];
79
+ const cw = charWidth(ch);
80
+ if (ww + cw > fieldW) break;
81
+ shown += ch;
82
+ ww += cw;
83
+ i += ch.length;
84
+ }
85
+ const pad = " ".repeat(Math.max(0, fieldW - ww));
86
+ const top = c.faint("โ•ญ" + "โ”€".repeat(inner) + "โ•ฎ");
87
+ const mid = c.faint("โ”‚") + c.emerald(glyph) + c.text(shown) + pad + c.faint("โ”‚");
88
+ const bot = c.faint("โ•ฐ" + "โ”€".repeat(inner) + "โ•ฏ");
89
+ const lines = [top, mid, bot];
90
+ if (ctx.status) lines.push(" " + c.faint(ctx.status));
91
+
92
+ const rows = state.suggest || [];
93
+ rows.slice(0, 8).forEach((r, i) => {
94
+ const cmd = String(r.command || "").padEnd(16);
95
+ const desc = String(r.description || "");
96
+ const label = (" " + cmd + " " + desc).slice(0, w);
97
+ lines.push(i === state.suggestSel ? c.inverse(label) : " " + c.blue(cmd) + c.dim(desc.slice(0, w - 20)));
98
+ });
99
+ if (rows.length) lines.push(c.faint(" โ†‘โ†“ move ยท Tab complete ยท Enter run ยท Esc close"));
100
+
101
+ const curCol = 1 + glyphW + visWidth(state.buf.slice(start, state.cur)); // 0-based visual column
102
+ return { lines, curCol };
103
+ }
104
+
105
+ function render(state, ctx) {
106
+ const f = frame(state, ctx);
107
+ let seq = "";
108
+ if (state.drawn > 0) seq += "\r\x1b[1A\x1b[0J"; // from input line: col0, up to top border, clear down
109
+ seq += f.lines.join("\r\n");
110
+ const up = f.lines.length - 1 - 1; // from last line up to the input line (index 1)
111
+ if (up > 0) seq += "\x1b[" + up + "A";
112
+ seq += "\r";
113
+ if (f.curCol > 0) seq += "\x1b[" + f.curCol + "C";
114
+ out.write(seq);
115
+ state.drawn = f.lines.length;
116
+ }
117
+
118
+ function clearBox(state) {
119
+ if (state.drawn > 0) {
120
+ out.write("\r\x1b[1A\x1b[0J");
121
+ state.drawn = 0;
122
+ }
123
+ }
124
+
125
+ function read(ctx) {
126
+ ctx = ctx || {};
127
+ return new Promise((resolve) => {
128
+ const state = { buf: "", cur: 0, scroll: 0, drawn: 0, suggest: [], suggestSel: 0, hist: -1, stash: "", dismissed: null };
129
+
130
+ function refreshSuggest() {
131
+ if (ctx.suggest && state.buf !== state.dismissed) {
132
+ state.suggest = ctx.suggest(state.buf) || [];
133
+ } else {
134
+ state.suggest = [];
135
+ }
136
+ if (state.suggestSel >= state.suggest.length) state.suggestSel = 0;
137
+ }
138
+ function draw() {
139
+ refreshSuggest();
140
+ render(state, ctx);
141
+ }
142
+
143
+ const wasRaw = !!inp.isRaw;
144
+ try { if (inp.setRawMode) inp.setRawMode(true); } catch { /* ignore */ }
145
+ readline.emitKeypressEvents(inp);
146
+ inp.resume();
147
+
148
+ let ctrlc = 0;
149
+ function done(result) {
150
+ inp.removeListener("keypress", onKey);
151
+ try { if (inp.setRawMode) inp.setRawMode(wasRaw); } catch { /* ignore */ }
152
+ resolve(result);
153
+ }
154
+ function setBuf(s, cur) {
155
+ state.buf = s;
156
+ state.cur = cur == null ? s.length : Math.max(0, Math.min(cur, s.length));
157
+ state.dismissed = null;
158
+ draw();
159
+ }
160
+ function submit() {
161
+ const value = state.buf;
162
+ clearBox(state);
163
+ out.write(c.paw("โ–Œ") + c.emerald(" โ€บ ") + c.text(value) + "\r\n");
164
+ if (value.trim()) {
165
+ history = history.filter((h) => h !== value);
166
+ history.unshift(value);
167
+ saveHistory(history);
168
+ }
169
+ done({ value });
170
+ }
171
+
172
+ function onKey(str, key) {
173
+ key = key || {};
174
+ const name = key.name;
175
+
176
+ if (key.ctrl && name === "c") {
177
+ if (state.buf.length) { ctrlc = 0; return setBuf("", 0); }
178
+ const now = Date.now();
179
+ if (now < ctrlc) { clearBox(state); return done({ exit: true }); }
180
+ ctrlc = now + 1500;
181
+ return;
182
+ }
183
+ if (key.ctrl && name === "d") {
184
+ if (!state.buf.length) { clearBox(state); return done({ eof: true }); }
185
+ return;
186
+ }
187
+ if (name === "return" || name === "enter") return submit();
188
+ if (name === "escape") {
189
+ if (state.suggest.length) { state.dismissed = state.buf; state.suggest = []; return render(state, ctx); }
190
+ return setBuf("", 0);
191
+ }
192
+ if (name === "backspace" || (key.ctrl && name === "h")) {
193
+ if (state.cur > 0) setBuf(state.buf.slice(0, state.cur - 1) + state.buf.slice(state.cur), state.cur - 1);
194
+ return;
195
+ }
196
+ if (name === "delete") return setBuf(state.buf.slice(0, state.cur) + state.buf.slice(state.cur + 1), state.cur);
197
+ if (name === "left") { if (state.cur > 0) { state.cur--; draw(); } return; }
198
+ if (name === "right") { if (state.cur < state.buf.length) { state.cur++; draw(); } return; }
199
+ if (name === "home" || (key.ctrl && name === "a")) { state.cur = 0; draw(); return; }
200
+ if (name === "end" || (key.ctrl && name === "e")) { state.cur = state.buf.length; draw(); return; }
201
+ if (key.ctrl && name === "u") return setBuf(state.buf.slice(state.cur), 0);
202
+ if (key.ctrl && name === "k") return setBuf(state.buf.slice(0, state.cur), state.cur);
203
+ if (key.ctrl && name === "w") {
204
+ const left = state.buf.slice(0, state.cur).replace(/\s*\S+\s*$/, "");
205
+ return setBuf(left + state.buf.slice(state.cur), left.length);
206
+ }
207
+ if (name === "up") {
208
+ if (state.suggest.length) { state.suggestSel = (state.suggestSel - 1 + state.suggest.length) % state.suggest.length; state.buf = state.suggest[state.suggestSel].command; state.cur = state.buf.length; return render(state, ctx); }
209
+ return histNav(1);
210
+ }
211
+ if (name === "down") {
212
+ if (state.suggest.length) { state.suggestSel = (state.suggestSel + 1) % state.suggest.length; state.buf = state.suggest[state.suggestSel].command; state.cur = state.buf.length; return render(state, ctx); }
213
+ return histNav(-1);
214
+ }
215
+ if (name === "tab") {
216
+ if (state.suggest.length) { const cmd = state.suggest[state.suggestSel].command; state.dismissed = cmd; return setBuf(cmd, cmd.length); }
217
+ if (ctx.complete) {
218
+ const res = ctx.complete(state.buf) || [];
219
+ const hits = res[0] || [];
220
+ const token = res[1] || "";
221
+ if (hits.length === 1) {
222
+ const head = token ? state.buf.slice(0, state.buf.length - token.length) : state.buf;
223
+ return setBuf(head + hits[0]);
224
+ }
225
+ }
226
+ return;
227
+ }
228
+ // printable insert (single char or paste). Strip control/newlines โ†’ single-line field.
229
+ if (str && !key.ctrl && !key.meta) {
230
+ const text = String(str).replace(/[\r\n\t]+/g, " ").replace(/[\x00-\x1f]/g, "");
231
+ if (text) return setBuf(state.buf.slice(0, state.cur) + text + state.buf.slice(state.cur), state.cur + text.length);
232
+ }
233
+ }
234
+
235
+ function histNav(dir) {
236
+ if (!history.length) return;
237
+ if (state.hist === -1 && dir === 1) state.stash = state.buf;
238
+ let i = state.hist + dir;
239
+ if (i < -1) i = -1;
240
+ if (i >= history.length) i = history.length - 1;
241
+ state.hist = i;
242
+ const v = i === -1 ? state.stash : history[i];
243
+ state.buf = v;
244
+ state.cur = v.length;
245
+ draw();
246
+ }
247
+
248
+ inp.on("keypress", onKey);
249
+ draw();
250
+ });
251
+ }
252
+
253
+ return { read, setHistory: (h) => { history = (h || []).filter((x) => typeof x === "string"); } };
254
+ }
255
+
256
+ module.exports = { createComposer, visWidth };
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ /*
3
+ * CLI preferences (separate from the app's SQLite/keychain) โ€” first-run onboarding result.
4
+ * Stored at <userData>/cli-prefs.json: { onboarded, lang, runtime, permission }.
5
+ */
6
+ const fs = require("node:fs");
7
+ const path = require("node:path");
8
+
9
+ function prefsPath(userDataDir) {
10
+ return path.join(userDataDir, "cli-prefs.json");
11
+ }
12
+ function loadPrefs(userDataDir) {
13
+ try {
14
+ return JSON.parse(fs.readFileSync(prefsPath(userDataDir), "utf8")) || {};
15
+ } catch {
16
+ return {};
17
+ }
18
+ }
19
+ function savePrefs(userDataDir, prefs) {
20
+ try {
21
+ fs.mkdirSync(userDataDir, { recursive: true });
22
+ fs.writeFileSync(prefsPath(userDataDir), JSON.stringify(prefs, null, 2), "utf8");
23
+ return true;
24
+ } catch {
25
+ return false;
26
+ }
27
+ }
28
+
29
+ module.exports = { prefsPath, loadPrefs, savePrefs };
@@ -0,0 +1,173 @@
1
+ "use strict";
2
+ /*
3
+ * agentlas-doctor: ๋Ÿฐํƒ€์ž„ CLI ์‹คํŒจ์˜ "์‹œ์Šคํ…œ ์›์ธ"์„ ๊ฒฐ์ •๋ก ์ ์œผ๋กœ ์ง„๋‹จยท์ˆ˜๋ฆฌํ•œ๋‹ค.
4
+ *
5
+ * ๋ฐ์Šคํฌํƒ‘ ์•ฑ electron/system-agents/runtime-doctor.ts ์™€ ๋กœ์ง ํŒจ๋ฆฌํ‹ฐ๋ฅผ ์œ ์ง€ํ•ด์•ผ ํ•œ๋‹ค
6
+ * (3์ œํ’ˆ ์‹ฑํฌ: ๋ฐ์Šคํฌํƒ‘ TS โ†” ํ„ฐ๋ฏธ๋„ CJS โ†” system-optimizer ํŒจํ‚ค์ง€ ํ”Œ๋ ˆ์ด๋ถ).
7
+ * ํŒจ๋ฆฌํ‹ฐ๋Š” Agentlas_F/scripts/sync-runtime-doctor.sh ๊ฐ€ ๊ณต์œ  ํ”ฝ์Šค์ฒ˜๋กœ ๊ฒ€์ฆํ•œ๋‹ค โ€”
8
+ * ์ด ํŒŒ์ผ์˜ ๋ถ„๋ฅ˜/์ˆ˜๋ฆฌ ๊ทœ์น™์„ ๋ฐ”๊พธ๋ฉด ๋ฐ˜๋“œ์‹œ ๊ทธ ์Šคํฌ๋ฆฝํŠธ๋ฅผ PASS ์‹œ์ผœ๋ผ.
9
+ *
10
+ * ์‚ฌ๋ก€(2026-07-08): codex CLI ์—…๋ฐ์ดํŠธ๊ฐ€ openai-curated ํ”Œ๋Ÿฌ๊ทธ์ธ(notion/figma)์„ ์ž๋™
11
+ * ํ™œ์„ฑํ™” โ†’ ๋ฏธ์ธ์ฆ OAuth ์›๊ฒฉ MCP๊ฐ€ ๋งค ์‹คํ–‰ AuthRequired fatal โ†’ codex exit 1.
12
+ * ์ˆ˜๋ฆฌ: ์—๋Ÿฌ stderr์˜ ํ˜ธ์ŠคํŠธ์™€ ํ”Œ๋Ÿฌ๊ทธ์ธ ์บ์‹œ .mcp.json url ํ˜ธ์ŠคํŠธ๋ฅผ ๋Œ€์กฐํ•ด "์ •ํ™•ํžˆ ๊ทธ
13
+ * ํ”Œ๋Ÿฌ๊ทธ์ธ๋งŒ" config.toml์—์„œ enabled=false (๋ฐฑ์—… ํ•„์ˆ˜, ์ธ์ฆ๋ผ ์ž˜ ๋„๋Š” ํ”Œ๋Ÿฌ๊ทธ์ธ ์˜คํญ ๊ธˆ์ง€).
14
+ */
15
+ const fs = require("node:fs");
16
+ const os = require("node:os");
17
+ const path = require("node:path");
18
+
19
+ function codexHome() {
20
+ return process.env.CODEX_HOME || path.join(os.homedir(), ".codex");
21
+ }
22
+
23
+ /** ์—๋Ÿฌ ํ…์ŠคํŠธ์—์„œ ์‹คํŒจ ์›์ธ์œผ๋กœ ์ง€๋ชฉ๋œ ์›๊ฒฉ ํ˜ธ์ŠคํŠธ๋“ค์„ ์ถ”์ถœํ•œ๋‹ค. */
24
+ function extractHosts(error) {
25
+ const hosts = new Set();
26
+ const re = /https?:\/\/([a-z0-9][a-z0-9.-]*[a-z0-9])/gi;
27
+ let m;
28
+ while ((m = re.exec(error)) !== null) hosts.add(m[1].toLowerCase());
29
+ return [...hosts];
30
+ }
31
+
32
+ /** kind: mcp-oauth-unauthenticated | timeout | cli-exit | unknown (๋ฐ์Šคํฌํƒ‘ TS์™€ ๋™์ผ ๊ทœ์น™) */
33
+ function classifyFailure(error) {
34
+ const text = error || "";
35
+ if (/authrequired|invalid_token|oauth-protected-resource|www_authenticate/i.test(text)) {
36
+ return { kind: "mcp-oauth-unauthenticated", hosts: extractHosts(text) };
37
+ }
38
+ if (/no response for \d+s|auto-aborted/i.test(text)) return { kind: "timeout", hosts: [] };
39
+ if (/CLI exit \d+|exited with code [1-9]/i.test(text)) return { kind: "cli-exit", hosts: extractHosts(text) };
40
+ return { kind: "unknown", hosts: [] };
41
+ }
42
+
43
+ /** ์‹คํŒจ ํ˜ธ์ŠคํŠธ์™€ ์ผ์น˜ํ•˜๋Š” OAuth MCP๋ฅผ ์‹ค์€ ํ”Œ๋Ÿฌ๊ทธ์ธ ์ฐพ๊ธฐ(ํ˜ธ์ŠคํŠธ๊ฐ€ ์—๋Ÿฌ์— ๋“ฑ์žฅํ•œ ๊ฒƒ๋งŒ). */
44
+ function findOauthPluginsByHost(hosts) {
45
+ if (!hosts.length) return [];
46
+ const cacheRoot = path.join(codexHome(), "plugins", "cache");
47
+ const hits = [];
48
+ let marketplaces = [];
49
+ try {
50
+ marketplaces = fs.readdirSync(cacheRoot, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
51
+ } catch {
52
+ return [];
53
+ }
54
+ for (const marketplace of marketplaces) {
55
+ let plugins = [];
56
+ try {
57
+ plugins = fs.readdirSync(path.join(cacheRoot, marketplace), { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
58
+ } catch {
59
+ continue;
60
+ }
61
+ for (const plugin of plugins) {
62
+ let versions = [];
63
+ try {
64
+ versions = fs.readdirSync(path.join(cacheRoot, marketplace, plugin), { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
65
+ } catch {
66
+ continue;
67
+ }
68
+ for (const ver of versions) {
69
+ const mcpJson = path.join(cacheRoot, marketplace, plugin, ver, ".mcp.json");
70
+ if (!fs.existsSync(mcpJson)) continue;
71
+ try {
72
+ const parsed = JSON.parse(fs.readFileSync(mcpJson, "utf8"));
73
+ for (const server of Object.values(parsed.mcpServers || {})) {
74
+ if (!server || !server.url) continue;
75
+ let host = "";
76
+ try {
77
+ host = new URL(server.url).hostname.toLowerCase();
78
+ } catch {
79
+ continue;
80
+ }
81
+ if (hosts.some((h) => h === host || h.endsWith("." + host) || host.endsWith("." + h))) {
82
+ // cache ๋””๋ ‰ํ† ๋ฆฌ "openai-curated-remote"๋Š” config ํ‚ค์—์„  "openai-curated".
83
+ hits.push({ pluginKey: `${plugin}@${marketplace.replace(/-remote$/, "")}`, host });
84
+ }
85
+ }
86
+ } catch {
87
+ /* ์†์ƒ๋œ .mcp.json์€ ๊ฑด๋„ˆ๋œ€ */
88
+ }
89
+ }
90
+ }
91
+ }
92
+ const seen = new Set();
93
+ return hits.filter((h) => (seen.has(h.pluginKey) ? false : (seen.add(h.pluginKey), true)));
94
+ }
95
+
96
+ /** config.toml์—์„œ ํ•ด๋‹น ํ”Œ๋Ÿฌ๊ทธ์ธ์„ enabled=false๋กœ ๋‚ด๋ฆฐ๋‹ค(๋ฐฑ์—… ํ•„์ˆ˜). ๋ฐ˜ํ™˜: ์‹ค์ œ ๋ณ€๊ฒฝ ์—ฌ๋ถ€. */
97
+ function disableCodexPlugin(pluginKey) {
98
+ const configPath = path.join(codexHome(), "config.toml");
99
+ if (!fs.existsSync(configPath)) return false;
100
+ const original = fs.readFileSync(configPath, "utf8");
101
+ const header = `[plugins."${pluginKey}"]`;
102
+ let next;
103
+ if (original.includes(header)) {
104
+ const idx = original.indexOf(header);
105
+ const after = original.slice(idx);
106
+ const replacedAfter = after.replace(/(\[plugins\."[^"]+"\]\s*\n)enabled\s*=\s*true/, "$1enabled = false");
107
+ if (replacedAfter === after) return false; // ์ด๋ฏธ false๊ฑฐ๋‚˜ ํ˜•ํƒœ๊ฐ€ ๋‹ค๋ฆ„
108
+ next = original.slice(0, idx) + replacedAfter;
109
+ } else {
110
+ next = `${original.trimEnd()}\n\n${header}\nenabled = false\n`;
111
+ }
112
+ const backup = `${configPath}.bak-doctor-${new Date().toISOString().replace(/[:.]/g, "-")}`;
113
+ fs.copyFileSync(configPath, backup);
114
+ fs.writeFileSync(configPath, next);
115
+ return true;
116
+ }
117
+
118
+ /**
119
+ * ์ง„๋‹จ + (์•„๋Š” ๊ณ„์—ด์ด๋ฉด) ์ฆ‰์‹œ ์ˆ˜๋ฆฌ. ๋ฐ˜ํ™˜: { kind, summary, repaired, actions[] }
120
+ * ๋ฐ์Šคํฌํƒ‘ runtime-doctor.ts ์˜ runRuntimeDoctor ์™€ ๋™์ผ ๊ณ„์•ฝ.
121
+ */
122
+ function runRuntimeDoctor(errorMessage) {
123
+ const { kind, hosts } = classifyFailure(errorMessage);
124
+ const actions = [];
125
+
126
+ if (kind === "mcp-oauth-unauthenticated") {
127
+ const hitList = findOauthPluginsByHost(hosts);
128
+ let repairedAny = false;
129
+ for (const hit of hitList) {
130
+ try {
131
+ if (disableCodexPlugin(hit.pluginKey)) {
132
+ repairedAny = true;
133
+ actions.push({
134
+ title: `codex plugin disabled: ${hit.pluginKey}`,
135
+ detail: `๋ฏธ์ธ์ฆ OAuth MCP(${hit.host})๊ฐ€ ๋Ÿฐํƒ€์ž„์„ ์ฃฝ์—ฌ์„œ ~/.codex/config.toml์—์„œ ๋น„ํ™œ์„ฑํ™”ํ–ˆ์Šต๋‹ˆ๋‹ค(๋ฐฑ์—… ์ƒ์„ฑ). ์ด ์„œ๋น„์Šค๋ฅผ ์“ฐ๋ ค๋ฉด ์ธ์ฆ ํ›„ ๋‹ค์‹œ ์ผœ์„ธ์š”.`,
136
+ });
137
+ }
138
+ } catch (err) {
139
+ actions.push({ title: `repair failed: ${hit.pluginKey}`, detail: err && err.message ? err.message : String(err) });
140
+ }
141
+ }
142
+ return {
143
+ kind,
144
+ summary: hitList.length
145
+ ? `๋Ÿฐํƒ€์ž„์— ๋ฏธ์ธ์ฆ OAuth MCP ํ”Œ๋Ÿฌ๊ทธ์ธ(${hitList.map((h) => h.pluginKey).join(", ")})์ด ๋ถ™์–ด ์žˆ์–ด CLI๊ฐ€ ์ฃฝ์—ˆ์Šต๋‹ˆ๋‹ค.`
146
+ : `๋ฏธ์ธ์ฆ OAuth MCP(${hosts.join(", ") || "unknown host"})๊ฐ€ ๋Ÿฐํƒ€์ž„์„ ์ฃฝ์˜€์ง€๋งŒ ์–ด๋–ค ํ”Œ๋Ÿฌ๊ทธ์ธ์ธ์ง€ ํŠน์ •ํ•˜์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค.`,
147
+ repaired: repairedAny,
148
+ actions,
149
+ };
150
+ }
151
+
152
+ if (kind === "timeout") {
153
+ return {
154
+ kind,
155
+ summary: "์‹คํ–‰์ด ์žฅ์‹œ๊ฐ„ ๋ฌด์‘๋‹ต์ด๋ผ ์ž๋™ ์ค‘๋‹จ๋์Šต๋‹ˆ๋‹ค. ๋Œ€ํ™”ํ˜• ์ธ์ฆ ๋Œ€๊ธฐยทstdin ๋ธ”๋กยท์›๊ฒฉ MCP ํ–‰์ด ํ”ํ•œ ์›์ธ์ž…๋‹ˆ๋‹ค.",
156
+ repaired: false,
157
+ actions,
158
+ };
159
+ }
160
+
161
+ if (kind === "cli-exit") {
162
+ return {
163
+ kind,
164
+ summary: "๋Ÿฐํƒ€์ž„ CLI๊ฐ€ ๋น„์ •์ƒ ์ข…๋ฃŒํ–ˆ์ง€๋งŒ ์•„๋Š” ์ˆ˜๋ฆฌ ๊ณ„์—ด์ด ์•„๋‹™๋‹ˆ๋‹ค.",
165
+ repaired: false,
166
+ actions,
167
+ };
168
+ }
169
+
170
+ return { kind: "unknown", summary: "", repaired: false, actions };
171
+ }
172
+
173
+ module.exports = { classifyFailure, extractHosts, findOauthPluginsByHost, disableCodexPlugin, runRuntimeDoctor };