miii-agent 0.1.6 → 0.1.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/README.md +23 -3
- package/dist/cli.js +164 -33
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# miii
|
|
2
2
|
|
|
3
|
+
> small · simple · smart · strategic · semantic
|
|
4
|
+
>
|
|
3
5
|
> Your code never leaves your machine. No API keys. No cloud. No bullshit.
|
|
4
6
|
|
|
5
7
|
**miii** is a local-first AI coding agent that lives in your terminal. Powered by [Ollama](https://ollama.com), it reads your code, writes features, runs tests, and fixes bugs — entirely on your hardware, at native speed.
|
|
@@ -10,6 +12,18 @@
|
|
|
10
12
|
|
|
11
13
|
---
|
|
12
14
|
|
|
15
|
+
## The name
|
|
16
|
+
|
|
17
|
+
**miii** stands for five principles it's built around:
|
|
18
|
+
|
|
19
|
+
- **small** — tight codebase, no bloat. You can read the whole thing.
|
|
20
|
+
- **simple** — no API keys, no accounts, no config ceremony. Just run it.
|
|
21
|
+
- **smart** — decomposes problems and verifies its own work like an engineer.
|
|
22
|
+
- **strategic** — plans before it acts; tools are gated, paths are confined.
|
|
23
|
+
- **semantic** — works from the meaning of your code, not blind text matching.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
13
27
|
## Demo
|
|
14
28
|
|
|
15
29
|

|
|
@@ -115,7 +129,7 @@ miii ships with a built-in tool suite the agent can invoke autonomously:
|
|
|
115
129
|
| `grep` | Regex search across files |
|
|
116
130
|
| `run_bash` | Execute shell commands |
|
|
117
131
|
|
|
118
|
-
Every sensitive operation is gated by a permission system — you approve what the agent can touch.
|
|
132
|
+
Every sensitive operation is gated by a permission system — you approve what the agent can touch, and "always" approvals persist to `~/.miii/permissions.json` so you're never asked twice. File tools are confined to your working directory; `../` traversal and absolute paths outside it are rejected.
|
|
119
133
|
|
|
120
134
|
---
|
|
121
135
|
|
|
@@ -137,8 +151,9 @@ graph TD
|
|
|
137
151
|
|
|
138
152
|
subgraph Agent ["Agent Layer"]
|
|
139
153
|
AgentLoop -->|"chat request"| Adapter["Ollama Adapter\n(agent/adapter.ts)"]
|
|
140
|
-
AgentLoop -->|"
|
|
141
|
-
AgentLoop -->|"permission check"| Policy["Permission Policy\n(permissions/policy.ts)"]
|
|
154
|
+
AgentLoop -->|"1. validate input"| Validate["Input Validator\n(tools/validate.ts)"]
|
|
155
|
+
AgentLoop -->|"2. permission check"| Policy["Permission Policy\n(permissions/policy.ts)"]
|
|
156
|
+
AgentLoop -->|"3. tool call"| ToolRegistry["Tool Registry\n(tools/registry.ts)"]
|
|
142
157
|
AgentLoop -->|"events"| EventBus["Event Bus\n(hooks/bus.ts)"]
|
|
143
158
|
end
|
|
144
159
|
|
|
@@ -149,6 +164,9 @@ graph TD
|
|
|
149
164
|
ToolRegistry --> Glob["glob"]
|
|
150
165
|
ToolRegistry --> Grep["grep"]
|
|
151
166
|
ToolRegistry --> RunBash["run_bash"]
|
|
167
|
+
ReadFile -.-> Confine["Path Confinement\n(tools/paths.ts)"]
|
|
168
|
+
WriteFile -.-> Confine
|
|
169
|
+
EditFile -.-> Confine
|
|
152
170
|
end
|
|
153
171
|
|
|
154
172
|
Adapter -->|"HTTP streaming"| Ollama["Ollama\n(local LLM server)"]
|
|
@@ -159,9 +177,11 @@ graph TD
|
|
|
159
177
|
|
|
160
178
|
subgraph Storage ["Local Storage"]
|
|
161
179
|
Config["~/.miii/config.json\n(model, host, effort)"]
|
|
180
|
+
Rules["~/.miii/permissions.json\n(saved allow rules)"]
|
|
162
181
|
end
|
|
163
182
|
|
|
164
183
|
App -.->|"reads"| Config
|
|
184
|
+
Policy -.->|"reads / persists 'always'"| Rules
|
|
165
185
|
```
|
|
166
186
|
|
|
167
187
|
---
|
package/dist/cli.js
CHANGED
|
@@ -7,8 +7,8 @@ import { createElement } from "react";
|
|
|
7
7
|
// src/ui/App.tsx
|
|
8
8
|
import { useState as useState4, useEffect as useEffect3 } from "react";
|
|
9
9
|
import { Box as Box10, Text as Text10, useApp } from "ink";
|
|
10
|
-
import { homedir as
|
|
11
|
-
import { sep } from "path";
|
|
10
|
+
import { homedir as homedir4 } from "os";
|
|
11
|
+
import { sep as sep2 } from "path";
|
|
12
12
|
|
|
13
13
|
// src/ollama/client.ts
|
|
14
14
|
import { Ollama } from "ollama";
|
|
@@ -354,8 +354,12 @@ function filteredCommands(filter) {
|
|
|
354
354
|
// src/session/store.ts
|
|
355
355
|
import { writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync as existsSync2, readdirSync, readFileSync as readFileSync2, rmSync } from "fs";
|
|
356
356
|
import { join as join2 } from "path";
|
|
357
|
+
import { homedir as homedir2 } from "os";
|
|
357
358
|
import { randomUUID } from "crypto";
|
|
358
|
-
|
|
359
|
+
function encodeProjectDir(cwd) {
|
|
360
|
+
return cwd.replace(/[/\\]/g, "-").replace(/^-+/, "");
|
|
361
|
+
}
|
|
362
|
+
var SESSION_DIR = join2(homedir2(), ".miii", "projects", encodeProjectDir(process.cwd()), "session");
|
|
359
363
|
function newSessionId() {
|
|
360
364
|
return randomUUID();
|
|
361
365
|
}
|
|
@@ -545,8 +549,8 @@ function searchFiles(cwd, query) {
|
|
|
545
549
|
scored.sort((a, b) => a[0] - b[0] || a[1].length - b[1].length);
|
|
546
550
|
return scored.slice(0, MAX_RESULTS).map(([, f]) => f);
|
|
547
551
|
}
|
|
548
|
-
function FilePicker({ matches, cursor }) {
|
|
549
|
-
if (
|
|
552
|
+
function FilePicker({ matches: matches2, cursor }) {
|
|
553
|
+
if (matches2.length === 0) return null;
|
|
550
554
|
return /* @__PURE__ */ jsxs7(
|
|
551
555
|
Box7,
|
|
552
556
|
{
|
|
@@ -557,7 +561,7 @@ function FilePicker({ matches, cursor }) {
|
|
|
557
561
|
marginBottom: 0,
|
|
558
562
|
paddingX: 1,
|
|
559
563
|
children: [
|
|
560
|
-
|
|
564
|
+
matches2.map((f, i) => {
|
|
561
565
|
const active = i === cursor;
|
|
562
566
|
return /* @__PURE__ */ jsx7(Box7, { children: /* @__PURE__ */ jsxs7(Text7, { bold: active, color: active ? "blue" : void 0, dimColor: !active, children: [
|
|
563
567
|
active ? "\u276F " : " ",
|
|
@@ -842,6 +846,7 @@ function PermissionPrompt({ req, cursor }) {
|
|
|
842
846
|
const label = TOOL_LABEL[req.toolName] ?? req.toolName;
|
|
843
847
|
const options = [
|
|
844
848
|
{ label: "Yes", key: "yes" },
|
|
849
|
+
{ label: "Yes, don't ask again for this", key: "always" },
|
|
845
850
|
{ label: "No", key: "no" }
|
|
846
851
|
];
|
|
847
852
|
const summary = summarizeInput(req.input);
|
|
@@ -910,6 +915,23 @@ import { useState as useState3, useRef } from "react";
|
|
|
910
915
|
|
|
911
916
|
// src/tools/edit_file.ts
|
|
912
917
|
import { readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
918
|
+
|
|
919
|
+
// src/tools/paths.ts
|
|
920
|
+
import { resolve, relative as relative2, isAbsolute, sep } from "path";
|
|
921
|
+
function confinePath(p) {
|
|
922
|
+
if (typeof p !== "string" || p.length === 0) {
|
|
923
|
+
throw new Error("Path is required.");
|
|
924
|
+
}
|
|
925
|
+
const root = process.cwd();
|
|
926
|
+
const abs = resolve(root, p);
|
|
927
|
+
const rel = relative2(root, abs);
|
|
928
|
+
if (rel === ".." || rel.startsWith(".." + sep) || isAbsolute(rel)) {
|
|
929
|
+
throw new Error(`Path "${p}" is outside the working directory (${root}). Access denied.`);
|
|
930
|
+
}
|
|
931
|
+
return abs;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
// src/tools/edit_file.ts
|
|
913
935
|
var edit_file = {
|
|
914
936
|
name: "edit_file",
|
|
915
937
|
description: "Replace an exact string in a file. old_str must be unique.",
|
|
@@ -923,16 +945,21 @@ var edit_file = {
|
|
|
923
945
|
required: ["path", "old_str", "new_str"]
|
|
924
946
|
},
|
|
925
947
|
handler: ({ path, old_str, new_str }) => {
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
948
|
+
try {
|
|
949
|
+
const abs = confinePath(path);
|
|
950
|
+
const src = readFileSync3(abs, "utf-8");
|
|
951
|
+
const first = src.indexOf(old_str);
|
|
952
|
+
if (first === -1) {
|
|
953
|
+
return { content: `old_str not found in ${path}`, is_error: true };
|
|
954
|
+
}
|
|
955
|
+
if (src.indexOf(old_str, first + 1) !== -1) {
|
|
956
|
+
return { content: `old_str not unique in ${path}`, is_error: true };
|
|
957
|
+
}
|
|
958
|
+
writeFileSync3(abs, src.slice(0, first) + new_str + src.slice(first + old_str.length), "utf-8");
|
|
959
|
+
return { content: `Edited ${path}` };
|
|
960
|
+
} catch (err) {
|
|
961
|
+
return { content: err instanceof Error ? err.message : String(err), is_error: true };
|
|
933
962
|
}
|
|
934
|
-
writeFileSync3(path, src.slice(0, first) + new_str + src.slice(first + old_str.length), "utf-8");
|
|
935
|
-
return { content: `Edited ${path}` };
|
|
936
963
|
}
|
|
937
964
|
};
|
|
938
965
|
|
|
@@ -951,7 +978,7 @@ var read_file = {
|
|
|
951
978
|
handler: ({ path }) => {
|
|
952
979
|
try {
|
|
953
980
|
const MAX = 2e5;
|
|
954
|
-
const raw = readFileSync4(path, "utf-8");
|
|
981
|
+
const raw = readFileSync4(confinePath(path), "utf-8");
|
|
955
982
|
const truncated = raw.length > MAX;
|
|
956
983
|
const body = truncated ? raw.slice(0, MAX) + `
|
|
957
984
|
[truncated: ${raw.length - MAX} more chars]` : raw;
|
|
@@ -978,8 +1005,9 @@ var write_file = {
|
|
|
978
1005
|
},
|
|
979
1006
|
handler: ({ path, content }) => {
|
|
980
1007
|
try {
|
|
981
|
-
|
|
982
|
-
|
|
1008
|
+
const abs = confinePath(path);
|
|
1009
|
+
mkdirSync3(dirname(abs), { recursive: true });
|
|
1010
|
+
writeFileSync4(abs, content, "utf-8");
|
|
983
1011
|
return { content: `Wrote ${path} (${content.length} bytes)` };
|
|
984
1012
|
} catch (err) {
|
|
985
1013
|
return { content: err instanceof Error ? err.message : String(err), is_error: true };
|
|
@@ -1157,6 +1185,42 @@ function toOllamaTools(tools = TOOLS) {
|
|
|
1157
1185
|
}));
|
|
1158
1186
|
}
|
|
1159
1187
|
|
|
1188
|
+
// src/tools/validate.ts
|
|
1189
|
+
import { z } from "zod";
|
|
1190
|
+
function propSchema(spec) {
|
|
1191
|
+
if (spec.enum && spec.enum.length) return z.enum(spec.enum);
|
|
1192
|
+
switch (spec.type) {
|
|
1193
|
+
case "string":
|
|
1194
|
+
return z.string();
|
|
1195
|
+
case "number":
|
|
1196
|
+
return z.number();
|
|
1197
|
+
case "integer":
|
|
1198
|
+
return z.number().int();
|
|
1199
|
+
case "boolean":
|
|
1200
|
+
return z.boolean();
|
|
1201
|
+
case "array":
|
|
1202
|
+
return z.array(z.unknown());
|
|
1203
|
+
case "object":
|
|
1204
|
+
return z.record(z.unknown());
|
|
1205
|
+
default:
|
|
1206
|
+
return z.unknown();
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
function toZod(schema) {
|
|
1210
|
+
const required = new Set(schema.required ?? []);
|
|
1211
|
+
const shape = {};
|
|
1212
|
+
for (const [key, spec] of Object.entries(schema.properties)) {
|
|
1213
|
+
shape[key] = required.has(key) ? propSchema(spec) : z.unknown().optional();
|
|
1214
|
+
}
|
|
1215
|
+
return z.object(shape).passthrough();
|
|
1216
|
+
}
|
|
1217
|
+
function validateInput(schema, input) {
|
|
1218
|
+
const result = toZod(schema).safeParse(input ?? {});
|
|
1219
|
+
if (result.success) return null;
|
|
1220
|
+
const issues = result.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ");
|
|
1221
|
+
return `Invalid arguments: ${issues}`;
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1160
1224
|
// src/prompt/system.ts
|
|
1161
1225
|
function buildSystemPrompt(tools, cwd) {
|
|
1162
1226
|
const toolLines = tools.map((t) => `- ${t.name}: ${t.description}`).join("\n");
|
|
@@ -1231,16 +1295,65 @@ ${toolLines}
|
|
|
1231
1295
|
- Treat a green test run or a successful command as the completion signal. If it fails, fix and re-run.
|
|
1232
1296
|
|
|
1233
1297
|
# Permissions
|
|
1234
|
-
-
|
|
1298
|
+
- File tools are confined to the working directory; paths outside it are denied.
|
|
1299
|
+
- Each tool call may prompt the user for approval. If they choose "don't ask again", the exact command or path is persisted to ~/.miii/permissions.json and the same call is auto-allowed thereafter.
|
|
1235
1300
|
`;
|
|
1236
1301
|
}
|
|
1237
1302
|
|
|
1238
1303
|
// src/permissions/policy.ts
|
|
1239
|
-
|
|
1304
|
+
import { readFileSync as readFileSync5, writeFileSync as writeFileSync5, mkdirSync as mkdirSync4, existsSync as existsSync3, renameSync } from "fs";
|
|
1305
|
+
import { join as join4 } from "path";
|
|
1306
|
+
import { homedir as homedir3 } from "os";
|
|
1307
|
+
var RULES_DIR = join4(homedir3(), ".miii");
|
|
1308
|
+
var RULES_PATH = join4(RULES_DIR, "permissions.json");
|
|
1309
|
+
function loadRules() {
|
|
1310
|
+
if (!existsSync3(RULES_PATH)) return [];
|
|
1311
|
+
try {
|
|
1312
|
+
const data = JSON.parse(readFileSync5(RULES_PATH, "utf-8"));
|
|
1313
|
+
return Array.isArray(data.rules) ? data.rules : [];
|
|
1314
|
+
} catch {
|
|
1315
|
+
return [];
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
function saveRules(rules) {
|
|
1319
|
+
mkdirSync4(RULES_DIR, { recursive: true });
|
|
1320
|
+
const tmp = RULES_PATH + ".tmp";
|
|
1321
|
+
writeFileSync5(tmp, JSON.stringify({ rules }, null, 2), "utf-8");
|
|
1322
|
+
renameSync(tmp, RULES_PATH);
|
|
1323
|
+
}
|
|
1324
|
+
function addRule(tool, pattern) {
|
|
1325
|
+
const rules = loadRules();
|
|
1326
|
+
if (rules.some((r) => r.tool === tool && r.pattern === pattern)) return;
|
|
1327
|
+
rules.push({ tool, pattern });
|
|
1328
|
+
saveRules(rules);
|
|
1329
|
+
}
|
|
1330
|
+
function subjectFor(toolName, input) {
|
|
1331
|
+
const obj = input ?? {};
|
|
1332
|
+
if (toolName === "run_bash") return typeof obj.command === "string" ? obj.command : "";
|
|
1333
|
+
if (typeof obj.path === "string") return obj.path;
|
|
1334
|
+
return "";
|
|
1335
|
+
}
|
|
1336
|
+
function globToRegExp(glob2) {
|
|
1337
|
+
const escaped = glob2.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
1338
|
+
const pattern = escaped.replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
1339
|
+
return new RegExp(`^${pattern}$`);
|
|
1340
|
+
}
|
|
1341
|
+
function matches(rule, toolName, subject) {
|
|
1342
|
+
if (rule.tool !== toolName) return false;
|
|
1343
|
+
try {
|
|
1344
|
+
return globToRegExp(rule.pattern).test(subject);
|
|
1345
|
+
} catch {
|
|
1346
|
+
return false;
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1240
1349
|
async function check(toolName, input, ctx) {
|
|
1241
|
-
|
|
1350
|
+
const subject = subjectFor(toolName, input);
|
|
1351
|
+
const rules = loadRules();
|
|
1352
|
+
if (rules.some((r) => matches(r, toolName, subject))) return "allow";
|
|
1242
1353
|
const answer = await ctx.ask(toolName, input);
|
|
1243
|
-
|
|
1354
|
+
if (answer === "no") return "deny";
|
|
1355
|
+
if (answer === "always") addRule(toolName, subject);
|
|
1356
|
+
return "allow";
|
|
1244
1357
|
}
|
|
1245
1358
|
|
|
1246
1359
|
// src/agent/adapter.ts
|
|
@@ -1490,6 +1603,18 @@ async function* runAgent(opts) {
|
|
|
1490
1603
|
yield { type: "tool-result", block: r2 };
|
|
1491
1604
|
continue;
|
|
1492
1605
|
}
|
|
1606
|
+
const invalid = validateInput(tool.input_schema, use.input);
|
|
1607
|
+
if (invalid) {
|
|
1608
|
+
const r2 = {
|
|
1609
|
+
type: "tool_result",
|
|
1610
|
+
tool_use_id: use.id,
|
|
1611
|
+
content: `${invalid} for ${use.name}.`,
|
|
1612
|
+
is_error: true
|
|
1613
|
+
};
|
|
1614
|
+
results.push(r2);
|
|
1615
|
+
yield { type: "tool-result", block: r2 };
|
|
1616
|
+
continue;
|
|
1617
|
+
}
|
|
1493
1618
|
const decision = await check(use.name, use.input, permissions);
|
|
1494
1619
|
if (decision === "deny") {
|
|
1495
1620
|
const r2 = {
|
|
@@ -1503,7 +1628,10 @@ async function* runAgent(opts) {
|
|
|
1503
1628
|
yield { type: "tool-result", block: r2 };
|
|
1504
1629
|
continue;
|
|
1505
1630
|
}
|
|
1506
|
-
|
|
1631
|
+
try {
|
|
1632
|
+
await hooks?.firePre(use);
|
|
1633
|
+
} catch {
|
|
1634
|
+
}
|
|
1507
1635
|
let r;
|
|
1508
1636
|
try {
|
|
1509
1637
|
const out = await tool.handler(use.input);
|
|
@@ -1521,7 +1649,10 @@ async function* runAgent(opts) {
|
|
|
1521
1649
|
is_error: true
|
|
1522
1650
|
};
|
|
1523
1651
|
}
|
|
1524
|
-
|
|
1652
|
+
try {
|
|
1653
|
+
await hooks?.firePost(use, r);
|
|
1654
|
+
} catch {
|
|
1655
|
+
}
|
|
1525
1656
|
results.push(r);
|
|
1526
1657
|
yield { type: "tool-result", block: r };
|
|
1527
1658
|
}
|
|
@@ -1552,8 +1683,8 @@ function useAgentRunner(model, activeCtx) {
|
|
|
1552
1683
|
const abortRef = useRef(null);
|
|
1553
1684
|
const pendingPermissionRef = useRef(null);
|
|
1554
1685
|
function askPermission(toolName, input) {
|
|
1555
|
-
return new Promise((
|
|
1556
|
-
const req = { toolName, input, resolve };
|
|
1686
|
+
return new Promise((resolve2) => {
|
|
1687
|
+
const req = { toolName, input, resolve: resolve2 };
|
|
1557
1688
|
pendingPermissionRef.current = req;
|
|
1558
1689
|
setPermissionCursor(0);
|
|
1559
1690
|
setPendingPermission(req);
|
|
@@ -1562,7 +1693,7 @@ function useAgentRunner(model, activeCtx) {
|
|
|
1562
1693
|
function resolvePermission(cursor) {
|
|
1563
1694
|
const req = pendingPermissionRef.current;
|
|
1564
1695
|
if (!req) return;
|
|
1565
|
-
const answers = ["yes", "no"];
|
|
1696
|
+
const answers = ["yes", "always", "no"];
|
|
1566
1697
|
pendingPermissionRef.current = null;
|
|
1567
1698
|
setPendingPermission(null);
|
|
1568
1699
|
req.resolve(answers[cursor]);
|
|
@@ -1898,7 +2029,7 @@ function useKeyboard(opts) {
|
|
|
1898
2029
|
return;
|
|
1899
2030
|
}
|
|
1900
2031
|
if (key.downArrow) {
|
|
1901
|
-
setPermissionCursor((i) => Math.min(
|
|
2032
|
+
setPermissionCursor((i) => Math.min(2, i + 1));
|
|
1902
2033
|
return;
|
|
1903
2034
|
}
|
|
1904
2035
|
if (key.return) {
|
|
@@ -1910,7 +2041,7 @@ function useKeyboard(opts) {
|
|
|
1910
2041
|
if (state === "ready") {
|
|
1911
2042
|
if (busyRef.current) return;
|
|
1912
2043
|
const paletteOpen = input.startsWith("/");
|
|
1913
|
-
const
|
|
2044
|
+
const matches2 = paletteOpen ? filteredCommands(input) : [];
|
|
1914
2045
|
const mention = !paletteOpen ? parseMention(input) : null;
|
|
1915
2046
|
const fileMatches = mention ? searchFiles(process.cwd(), mention.query) : [];
|
|
1916
2047
|
const fileOpen = mention !== null && fileMatches.length > 0;
|
|
@@ -1919,11 +2050,11 @@ function useKeyboard(opts) {
|
|
|
1919
2050
|
return;
|
|
1920
2051
|
}
|
|
1921
2052
|
if (paletteOpen && key.downArrow) {
|
|
1922
|
-
setPaletteCursor((i) => Math.min(
|
|
2053
|
+
setPaletteCursor((i) => Math.min(matches2.length - 1, i + 1));
|
|
1923
2054
|
return;
|
|
1924
2055
|
}
|
|
1925
|
-
if (paletteOpen && (key.tab || key.return) &&
|
|
1926
|
-
setInput(() =>
|
|
2056
|
+
if (paletteOpen && (key.tab || key.return) && matches2[paletteCursor] && input !== matches2[paletteCursor].name) {
|
|
2057
|
+
setInput(() => matches2[paletteCursor].name);
|
|
1927
2058
|
setPaletteCursor(() => 0);
|
|
1928
2059
|
return;
|
|
1929
2060
|
}
|
|
@@ -2042,7 +2173,7 @@ async function checkForUpdate() {
|
|
|
2042
2173
|
import { Fragment as Fragment2, jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
2043
2174
|
function App() {
|
|
2044
2175
|
const { exit } = useApp();
|
|
2045
|
-
const cwd = process.cwd().replace(
|
|
2176
|
+
const cwd = process.cwd().replace(homedir4(), "~").split(sep2).join("/");
|
|
2046
2177
|
const [cfg, setCfg] = useState4(loadConfig());
|
|
2047
2178
|
const [models, setModels] = useState4([]);
|
|
2048
2179
|
const [contexts, setContexts] = useState4({});
|