nimi-code 1.0.0 → 1.1.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.
- package/dist/main.js +465 -40
- package/package.json +1 -1
- package/readme.md +160 -50
package/dist/main.js
CHANGED
|
@@ -797,6 +797,69 @@ var init_permissions = __esm({
|
|
|
797
797
|
}
|
|
798
798
|
});
|
|
799
799
|
|
|
800
|
+
// src/config/auth.ts
|
|
801
|
+
import { mkdirSync, readFileSync } from "fs";
|
|
802
|
+
import { mkdir as mkdir2, writeFile as writeFile3 } from "fs/promises";
|
|
803
|
+
import { homedir as homedir2 } from "os";
|
|
804
|
+
import { dirname as dirname2, join as join5 } from "path";
|
|
805
|
+
function getAuthPath() {
|
|
806
|
+
return join5(homedir2(), ".nimi", "auth.json");
|
|
807
|
+
}
|
|
808
|
+
function parseAuthJson(raw) {
|
|
809
|
+
try {
|
|
810
|
+
const parsed = JSON.parse(raw);
|
|
811
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return {};
|
|
812
|
+
const out = {};
|
|
813
|
+
for (const [id, cred] of Object.entries(parsed)) {
|
|
814
|
+
if (typeof cred === "object" && cred !== null && !Array.isArray(cred)) {
|
|
815
|
+
const key = cred.key;
|
|
816
|
+
if (key === void 0 || typeof key === "string") out[id] = { type: "api_key", key };
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
return out;
|
|
820
|
+
} catch {
|
|
821
|
+
return {};
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
function readAuthFileSync(authPath = getAuthPath()) {
|
|
825
|
+
try {
|
|
826
|
+
return parseAuthJson(readFileSync(authPath, "utf-8"));
|
|
827
|
+
} catch {
|
|
828
|
+
return {};
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
function getStoredKeySync(providerId, authPath) {
|
|
832
|
+
const key = readAuthFileSync(authPath)[providerId]?.key;
|
|
833
|
+
return key ? key : void 0;
|
|
834
|
+
}
|
|
835
|
+
function resolveKeySync(providerId, envVar) {
|
|
836
|
+
return getStoredKeySync(providerId) || process.env[envVar] || void 0;
|
|
837
|
+
}
|
|
838
|
+
function formatNoApiKeyFoundMessage(providerId, envVar) {
|
|
839
|
+
return `No API key found for ${providerId}. Run /login ${providerId} (or set ${envVar}). Credentials are saved to ${getAuthPath()}.`;
|
|
840
|
+
}
|
|
841
|
+
async function saveApiKey(providerId, key, authPath = getAuthPath()) {
|
|
842
|
+
const data = readAuthFileSync(authPath);
|
|
843
|
+
data[providerId] = { type: "api_key", key };
|
|
844
|
+
mkdirSync(dirname2(authPath), { recursive: true, mode: 448 });
|
|
845
|
+
await writeFile3(authPath, `${JSON.stringify(data, null, 2)}
|
|
846
|
+
`, { encoding: "utf-8", mode: 384 });
|
|
847
|
+
}
|
|
848
|
+
async function deleteApiKey(providerId, authPath = getAuthPath()) {
|
|
849
|
+
const data = readAuthFileSync(authPath);
|
|
850
|
+
if (!(providerId in data)) return false;
|
|
851
|
+
delete data[providerId];
|
|
852
|
+
await mkdir2(dirname2(authPath), { recursive: true, mode: 448 });
|
|
853
|
+
await writeFile3(authPath, `${JSON.stringify(data, null, 2)}
|
|
854
|
+
`, { encoding: "utf-8", mode: 384 });
|
|
855
|
+
return true;
|
|
856
|
+
}
|
|
857
|
+
var init_auth = __esm({
|
|
858
|
+
"src/config/auth.ts"() {
|
|
859
|
+
"use strict";
|
|
860
|
+
}
|
|
861
|
+
});
|
|
862
|
+
|
|
800
863
|
// src/providers/anthropic.ts
|
|
801
864
|
import Anthropic from "@anthropic-ai/sdk";
|
|
802
865
|
function toAnthropic(message) {
|
|
@@ -1018,50 +1081,72 @@ var init_openai_compat = __esm({
|
|
|
1018
1081
|
});
|
|
1019
1082
|
|
|
1020
1083
|
// src/providers/index.ts
|
|
1021
|
-
function
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1084
|
+
function listProviders() {
|
|
1085
|
+
return Object.values(PROVIDER_DEFS);
|
|
1086
|
+
}
|
|
1087
|
+
function getProviderMeta(name) {
|
|
1088
|
+
const meta = PROVIDER_DEFS[name];
|
|
1089
|
+
if (!meta) {
|
|
1090
|
+
throw new Error(`Unknown provider: ${name}. Available: ${Object.keys(PROVIDER_DEFS).join(", ")}`);
|
|
1091
|
+
}
|
|
1092
|
+
return meta;
|
|
1093
|
+
}
|
|
1094
|
+
function getProviderDefaultModel(name) {
|
|
1095
|
+
return getProviderMeta(name).defaultModel;
|
|
1025
1096
|
}
|
|
1026
1097
|
function getProvider(name) {
|
|
1027
|
-
const
|
|
1028
|
-
|
|
1029
|
-
|
|
1098
|
+
const meta = getProviderMeta(name);
|
|
1099
|
+
const key = resolveKeySync(meta.id, meta.envVar);
|
|
1100
|
+
if (!key) throw new MissingApiKeyError(meta.id, meta.envVar);
|
|
1101
|
+
if (!process.env[meta.envVar]) process.env[meta.envVar] = key;
|
|
1102
|
+
if (meta.kind === "anthropic") {
|
|
1103
|
+
return createAnthropic();
|
|
1030
1104
|
}
|
|
1031
|
-
return
|
|
1105
|
+
return createOpenAICompat(meta.id, meta.baseURL, key, meta.defaultModel);
|
|
1032
1106
|
}
|
|
1033
|
-
var
|
|
1107
|
+
var PROVIDER_DEFS, MissingApiKeyError;
|
|
1034
1108
|
var init_providers = __esm({
|
|
1035
1109
|
"src/providers/index.ts"() {
|
|
1036
1110
|
"use strict";
|
|
1111
|
+
init_auth();
|
|
1037
1112
|
init_anthropic();
|
|
1038
1113
|
init_openai_compat();
|
|
1039
|
-
|
|
1040
|
-
anthropic:
|
|
1041
|
-
"anthropic-openai":
|
|
1042
|
-
"groq":
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
"openai":
|
|
1114
|
+
PROVIDER_DEFS = {
|
|
1115
|
+
anthropic: { id: "anthropic", label: "Anthropic", envVar: "ANTHROPIC_API_KEY", defaultModel: "claude-sonnet-5", kind: "anthropic" },
|
|
1116
|
+
"anthropic-openai": { id: "anthropic-openai", label: "Anthropic (OpenAI-compatible)", envVar: "ANTHROPIC_API_KEY", defaultModel: "claude-sonnet-5", baseURL: "https://api.anthropic.com/v1/", kind: "openai-compat" },
|
|
1117
|
+
groq: { id: "groq", label: "Groq", envVar: "GROQ_API_KEY", defaultModel: "qwen/qwen3.8-27b", baseURL: "https://api.groq.com/openai/v1", kind: "openai-compat" },
|
|
1118
|
+
meta: { id: "meta", label: "Meta", envVar: "META_API_KEY", defaultModel: "muse-spark-1.3-contributor", baseURL: "https://api.meta.ai/v1", kind: "openai-compat" },
|
|
1119
|
+
deepseek: { id: "deepseek", label: "DeepSeek", envVar: "DEEPSEEK_API_KEY", defaultModel: "deepseek-flash", baseURL: "https://api.deepseek.com", kind: "openai-compat" },
|
|
1120
|
+
openai: { id: "openai", label: "OpenAI", envVar: "OPENAI_API_KEY", defaultModel: "gpt-4o-mini", baseURL: "https://api.openai.com/v1", kind: "openai-compat" }
|
|
1121
|
+
};
|
|
1122
|
+
MissingApiKeyError = class extends Error {
|
|
1123
|
+
provider;
|
|
1124
|
+
envVar;
|
|
1125
|
+
constructor(provider2, envVar) {
|
|
1126
|
+
super(formatNoApiKeyFoundMessage(provider2, envVar));
|
|
1127
|
+
this.name = "MissingApiKeyError";
|
|
1128
|
+
this.provider = provider2;
|
|
1129
|
+
this.envVar = envVar;
|
|
1130
|
+
}
|
|
1046
1131
|
};
|
|
1047
1132
|
}
|
|
1048
1133
|
});
|
|
1049
1134
|
|
|
1050
1135
|
// src/session/store.ts
|
|
1051
1136
|
import { existsSync as existsSync3 } from "fs";
|
|
1052
|
-
import { appendFile, mkdir as
|
|
1053
|
-
import { join as
|
|
1137
|
+
import { appendFile, mkdir as mkdir3, readdir as readdir4, readFile as readFile6 } from "fs/promises";
|
|
1138
|
+
import { join as join6 } from "path";
|
|
1054
1139
|
function getSessionDir() {
|
|
1055
|
-
return
|
|
1140
|
+
return join6(process.cwd(), ".nimi", "sessions");
|
|
1056
1141
|
}
|
|
1057
1142
|
function getSessionPath(id) {
|
|
1058
|
-
return
|
|
1143
|
+
return join6(getSessionDir(), id + ".jsonl");
|
|
1059
1144
|
}
|
|
1060
1145
|
function newSessionId() {
|
|
1061
1146
|
return Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
|
|
1062
1147
|
}
|
|
1063
1148
|
async function appendEntry(id, entry) {
|
|
1064
|
-
await
|
|
1149
|
+
await mkdir3(getSessionDir(), { recursive: true });
|
|
1065
1150
|
await appendFile(getSessionPath(id), JSON.stringify(entry) + "\n", "utf-8");
|
|
1066
1151
|
}
|
|
1067
1152
|
async function appendMessages(id, messages) {
|
|
@@ -1089,7 +1174,7 @@ async function loadSession(id) {
|
|
|
1089
1174
|
return { info, messages };
|
|
1090
1175
|
}
|
|
1091
1176
|
async function listSessions() {
|
|
1092
|
-
await
|
|
1177
|
+
await mkdir3(getSessionDir(), { recursive: true });
|
|
1093
1178
|
const files = await readdir4(getSessionDir());
|
|
1094
1179
|
const ids = [];
|
|
1095
1180
|
for (const f of files) {
|
|
@@ -1105,13 +1190,13 @@ var init_store = __esm({
|
|
|
1105
1190
|
});
|
|
1106
1191
|
|
|
1107
1192
|
// src/trace/recorder.ts
|
|
1108
|
-
import { appendFileSync, mkdirSync, writeFileSync } from "fs";
|
|
1109
|
-
import { dirname as
|
|
1193
|
+
import { appendFileSync, mkdirSync as mkdirSync2, writeFileSync } from "fs";
|
|
1194
|
+
import { dirname as dirname3 } from "path";
|
|
1110
1195
|
function isSnapshotEvent(type) {
|
|
1111
1196
|
return type === "turn_end" || type === "agent_end" || type === "compacted";
|
|
1112
1197
|
}
|
|
1113
1198
|
function createTraceRecorder(path, info) {
|
|
1114
|
-
|
|
1199
|
+
mkdirSync2(dirname3(path), { recursive: true });
|
|
1115
1200
|
writeFileSync(
|
|
1116
1201
|
path,
|
|
1117
1202
|
JSON.stringify({
|
|
@@ -1235,6 +1320,8 @@ function nameToAction(name, arg = "") {
|
|
|
1235
1320
|
if (name === "compact") return { type: "compact" };
|
|
1236
1321
|
if (name === "context") return { type: "context" };
|
|
1237
1322
|
if (name === "model") return { type: "model", arg: arg.trim() };
|
|
1323
|
+
if (name === "login") return { type: "login", arg: arg.trim() };
|
|
1324
|
+
if (name === "logout") return { type: "logout", arg: arg.trim() };
|
|
1238
1325
|
if (name === "tools") return { type: "tools" };
|
|
1239
1326
|
if (name === "sessions") return { type: "sessions" };
|
|
1240
1327
|
if (name === "session") return { type: "session" };
|
|
@@ -1245,7 +1332,7 @@ var HELP_TEXT;
|
|
|
1245
1332
|
var init_slash = __esm({
|
|
1246
1333
|
"src/tui/slash.ts"() {
|
|
1247
1334
|
"use strict";
|
|
1248
|
-
HELP_TEXT = "Commands:\n /help Show this help\n /clear Clear the transcript\n /compact Summarize old history to free context\n /context Show token usage vs the window\n /model [id] List models, or switch (keeps history)\n /tools List the tools the model can call\n /sessions List saved sessions\n /session Show this session's resume command\n /quit Exit nimi";
|
|
1335
|
+
HELP_TEXT = "Commands:\n /help Show this help\n /clear Clear the transcript\n /compact Summarize old history to free context\n /context Show token usage vs the window\n /model [id] List models, or switch (keeps history)\n /login [provider] Sign in with an API key (saved to ~/.nimi/auth.json)\n /logout [provider] Remove a saved API key\n /tools List the tools the model can call\n /sessions List saved sessions\n /session Show this session's resume command\n /quit Exit nimi";
|
|
1249
1336
|
}
|
|
1250
1337
|
});
|
|
1251
1338
|
|
|
@@ -1314,7 +1401,7 @@ function ToolRow({ row }) {
|
|
|
1314
1401
|
] })
|
|
1315
1402
|
] });
|
|
1316
1403
|
}
|
|
1317
|
-
function App({ provider: provider2, model: model2, system: system2, tools, initialPrompt, yolo, sessionId, initialMessages, contextLimit }) {
|
|
1404
|
+
function App({ provider: provider2, model: model2, system: system2, tools, initialPrompt, yolo, sessionId, initialMessages, contextLimit, initialSetup }) {
|
|
1318
1405
|
const { exit } = useApp();
|
|
1319
1406
|
const [rows, setRows] = useState([]);
|
|
1320
1407
|
const [draft, setDraft] = useState("");
|
|
@@ -1322,6 +1409,9 @@ function App({ provider: provider2, model: model2, system: system2, tools, initi
|
|
|
1322
1409
|
const [error, setError] = useState(null);
|
|
1323
1410
|
const [session, setSession] = useState(0);
|
|
1324
1411
|
const [pending, setPending] = useState(null);
|
|
1412
|
+
const [authFlow, setAuthFlow] = useState(null);
|
|
1413
|
+
const [keyDraft, setKeyDraft] = useState("");
|
|
1414
|
+
const [authError, setAuthError] = useState(null);
|
|
1325
1415
|
const idRef = useRef(1);
|
|
1326
1416
|
const messagesRef = useRef([...initialMessages]);
|
|
1327
1417
|
const savedCount = useRef(initialMessages.length);
|
|
@@ -1359,6 +1449,83 @@ function App({ provider: provider2, model: model2, system: system2, tools, initi
|
|
|
1359
1449
|
},
|
|
1360
1450
|
[yolo]
|
|
1361
1451
|
);
|
|
1452
|
+
const openKeyPrompt = useCallback(
|
|
1453
|
+
(providerId) => {
|
|
1454
|
+
try {
|
|
1455
|
+
const meta = getProviderMeta(providerId);
|
|
1456
|
+
setKeyDraft("");
|
|
1457
|
+
setAuthError(null);
|
|
1458
|
+
setAuthFlow({ step: "key", providerId: meta.id, envVar: meta.envVar });
|
|
1459
|
+
} catch (e) {
|
|
1460
|
+
const id = nextId();
|
|
1461
|
+
setRows((prev) => [...prev, { kind: "text", id, text: e instanceof Error ? e.message : String(e) }]);
|
|
1462
|
+
}
|
|
1463
|
+
},
|
|
1464
|
+
[nextId]
|
|
1465
|
+
);
|
|
1466
|
+
const openProviderPicker = useCallback((mode) => {
|
|
1467
|
+
setAuthError(null);
|
|
1468
|
+
setAuthFlow({ step: "provider", mode, selected: 0 });
|
|
1469
|
+
}, []);
|
|
1470
|
+
const submitKey = useCallback(
|
|
1471
|
+
async (providerId, key) => {
|
|
1472
|
+
const trimmed = key.trim();
|
|
1473
|
+
if (!trimmed) {
|
|
1474
|
+
setAuthError("Empty key \u2014 paste an API key, or press esc to go back.");
|
|
1475
|
+
return;
|
|
1476
|
+
}
|
|
1477
|
+
try {
|
|
1478
|
+
await saveApiKey(providerId, trimmed);
|
|
1479
|
+
const created = getProvider(providerId);
|
|
1480
|
+
setActiveProvider(created);
|
|
1481
|
+
const current = getModelInfo(activeModel);
|
|
1482
|
+
setActiveModel(current?.provider === providerId ? activeModel : created.defaultModel);
|
|
1483
|
+
setAuthFlow(null);
|
|
1484
|
+
setKeyDraft("");
|
|
1485
|
+
setAuthError(null);
|
|
1486
|
+
setError(null);
|
|
1487
|
+
const id = nextId();
|
|
1488
|
+
setRows((prev) => [...prev, { kind: "text", id, text: `Saved API key for ${providerId} to ${getAuthPath()}.` }]);
|
|
1489
|
+
} catch (e) {
|
|
1490
|
+
setAuthError(e instanceof Error ? e.message : String(e));
|
|
1491
|
+
}
|
|
1492
|
+
},
|
|
1493
|
+
[activeModel, nextId]
|
|
1494
|
+
);
|
|
1495
|
+
const runLogout = useCallback(
|
|
1496
|
+
async (providerId, echoCommand) => {
|
|
1497
|
+
const id = nextId();
|
|
1498
|
+
try {
|
|
1499
|
+
getProviderMeta(providerId);
|
|
1500
|
+
} catch (e) {
|
|
1501
|
+
setRows((prev) => [...prev, { kind: "user", id, text: echoCommand }, { kind: "text", id: id + 1, text: e instanceof Error ? e.message : String(e) }]);
|
|
1502
|
+
idRef.current++;
|
|
1503
|
+
return;
|
|
1504
|
+
}
|
|
1505
|
+
const removed = await deleteApiKey(providerId);
|
|
1506
|
+
if (removed) delete process.env[getProviderMeta(providerId).envVar];
|
|
1507
|
+
setAuthFlow(null);
|
|
1508
|
+
const stillConfigured = (() => {
|
|
1509
|
+
try {
|
|
1510
|
+
getProvider(providerId);
|
|
1511
|
+
return true;
|
|
1512
|
+
} catch {
|
|
1513
|
+
return false;
|
|
1514
|
+
}
|
|
1515
|
+
})();
|
|
1516
|
+
if (activeProvider?.name === providerId && !stillConfigured) {
|
|
1517
|
+
setActiveProvider(null);
|
|
1518
|
+
setAuthFlow({ step: "provider", mode: "login", selected: 0 });
|
|
1519
|
+
}
|
|
1520
|
+
setRows((prev) => [
|
|
1521
|
+
...prev,
|
|
1522
|
+
{ kind: "user", id, text: echoCommand },
|
|
1523
|
+
{ kind: "text", id: id + 1, text: removed ? `Removed stored API key for ${providerId}. Environment variables are unchanged.` : `No stored API key for ${providerId}.` }
|
|
1524
|
+
]);
|
|
1525
|
+
idRef.current++;
|
|
1526
|
+
},
|
|
1527
|
+
[activeProvider, nextId]
|
|
1528
|
+
);
|
|
1362
1529
|
const submitPrompt = useCallback(
|
|
1363
1530
|
async (prompt) => {
|
|
1364
1531
|
const text = prompt.trim();
|
|
@@ -1377,6 +1544,24 @@ function App({ provider: provider2, model: model2, system: system2, tools, initi
|
|
|
1377
1544
|
setSession((s) => s + 1);
|
|
1378
1545
|
return;
|
|
1379
1546
|
}
|
|
1547
|
+
if (slash.type === "login") {
|
|
1548
|
+
const id2 = nextId();
|
|
1549
|
+
if (!slash.arg) {
|
|
1550
|
+
setRows((prev) => [...prev, { kind: "user", id: id2, text }]);
|
|
1551
|
+
idRef.current++;
|
|
1552
|
+
openProviderPicker("login");
|
|
1553
|
+
} else {
|
|
1554
|
+
setRows((prev) => [...prev, { kind: "user", id: id2, text }]);
|
|
1555
|
+
idRef.current++;
|
|
1556
|
+
openKeyPrompt(slash.arg);
|
|
1557
|
+
}
|
|
1558
|
+
return;
|
|
1559
|
+
}
|
|
1560
|
+
if (slash.type === "logout") {
|
|
1561
|
+
if (!slash.arg) openProviderPicker("logout");
|
|
1562
|
+
else await runLogout(slash.arg, text);
|
|
1563
|
+
return;
|
|
1564
|
+
}
|
|
1380
1565
|
if (slash.type === "model") {
|
|
1381
1566
|
const id2 = nextId();
|
|
1382
1567
|
if (!slash.arg) {
|
|
@@ -1393,6 +1578,7 @@ function App({ provider: provider2, model: model2, system: system2, tools, initi
|
|
|
1393
1578
|
setRows((prev) => [...prev, { kind: "user", id: id2, text }, { kind: "text", id: id2 + 1, text: `Switched to ${info.id} \u2014 history kept.` }]);
|
|
1394
1579
|
} catch (e) {
|
|
1395
1580
|
setRows((prev) => [...prev, { kind: "user", id: id2, text }, { kind: "text", id: id2 + 1, text: e instanceof Error ? e.message : String(e) }]);
|
|
1581
|
+
if (e instanceof MissingApiKeyError) openKeyPrompt(e.provider);
|
|
1396
1582
|
}
|
|
1397
1583
|
}
|
|
1398
1584
|
}
|
|
@@ -1436,6 +1622,12 @@ function App({ provider: provider2, model: model2, system: system2, tools, initi
|
|
|
1436
1622
|
if (slash.type === "compact") {
|
|
1437
1623
|
if (workingRef.current) return;
|
|
1438
1624
|
const id2 = nextId();
|
|
1625
|
+
if (!activeProvider) {
|
|
1626
|
+
setRows((prev) => [...prev, { kind: "user", id: id2, text }, { kind: "text", id: id2 + 1, text: "Not logged in. Run /login to add an API key first." }]);
|
|
1627
|
+
idRef.current++;
|
|
1628
|
+
openProviderPicker("login");
|
|
1629
|
+
return;
|
|
1630
|
+
}
|
|
1439
1631
|
if (messagesRef.current.length === 0) {
|
|
1440
1632
|
setRows((prev) => [...prev, { kind: "user", id: id2, text }, { kind: "text", id: id2 + 1, text: "Nothing to compact." }]);
|
|
1441
1633
|
idRef.current++;
|
|
@@ -1476,6 +1668,12 @@ function App({ provider: provider2, model: model2, system: system2, tools, initi
|
|
|
1476
1668
|
return;
|
|
1477
1669
|
}
|
|
1478
1670
|
const userId = nextId();
|
|
1671
|
+
if (!activeProvider) {
|
|
1672
|
+
setRows((prev) => [...prev, { kind: "user", id: userId, text }, { kind: "text", id: userId + 1, text: "Not logged in. Pick a provider to add its API key." }]);
|
|
1673
|
+
idRef.current++;
|
|
1674
|
+
if (!authFlow) openProviderPicker("login");
|
|
1675
|
+
return;
|
|
1676
|
+
}
|
|
1479
1677
|
setRows((prev) => [...prev, { kind: "user", id: userId, text }]);
|
|
1480
1678
|
messagesRef.current.push({ role: "user", content: text });
|
|
1481
1679
|
workingRef.current = true;
|
|
@@ -1543,9 +1741,44 @@ function App({ provider: provider2, model: model2, system: system2, tools, initi
|
|
|
1543
1741
|
}
|
|
1544
1742
|
}
|
|
1545
1743
|
},
|
|
1546
|
-
[appendText, canUseTool, contextLimit, exit, activeModel, activeProvider, nextId, sessionId, system2, tools]
|
|
1744
|
+
[appendText, canUseTool, contextLimit, exit, activeModel, activeProvider, authFlow, nextId, openKeyPrompt, openProviderPicker, runLogout, sessionId, system2, tools]
|
|
1547
1745
|
);
|
|
1548
1746
|
useInput((input, key) => {
|
|
1747
|
+
if (authFlow?.step === "provider") {
|
|
1748
|
+
const options = listProviders();
|
|
1749
|
+
if (key.upArrow) {
|
|
1750
|
+
setAuthFlow((prev) => prev?.step === "provider" ? { ...prev, selected: (prev.selected + options.length - 1) % options.length } : prev);
|
|
1751
|
+
return;
|
|
1752
|
+
}
|
|
1753
|
+
if (key.downArrow) {
|
|
1754
|
+
setAuthFlow((prev) => prev?.step === "provider" ? { ...prev, selected: (prev.selected + 1) % options.length } : prev);
|
|
1755
|
+
return;
|
|
1756
|
+
}
|
|
1757
|
+
if (key.return) {
|
|
1758
|
+
const current = authFlow;
|
|
1759
|
+
const picked = options[current.selected];
|
|
1760
|
+
if (!picked) return;
|
|
1761
|
+
if (current.mode === "login") {
|
|
1762
|
+
setKeyDraft("");
|
|
1763
|
+
setAuthError(null);
|
|
1764
|
+
setAuthFlow({ step: "key", providerId: picked.id, envVar: picked.envVar });
|
|
1765
|
+
} else {
|
|
1766
|
+
void runLogout(picked.id, `/logout ${picked.id}`);
|
|
1767
|
+
}
|
|
1768
|
+
return;
|
|
1769
|
+
}
|
|
1770
|
+
if (key.escape && activeProvider) {
|
|
1771
|
+
setAuthFlow(null);
|
|
1772
|
+
setAuthError(null);
|
|
1773
|
+
}
|
|
1774
|
+
return;
|
|
1775
|
+
}
|
|
1776
|
+
if (authFlow?.step === "key" && key.escape) {
|
|
1777
|
+
setAuthFlow({ step: "provider", mode: "login", selected: 0 });
|
|
1778
|
+
setAuthError(null);
|
|
1779
|
+
setKeyDraft("");
|
|
1780
|
+
return;
|
|
1781
|
+
}
|
|
1549
1782
|
if (pending) {
|
|
1550
1783
|
const ch = input.toLowerCase();
|
|
1551
1784
|
if (ch === "y") pending.resolve("yes");
|
|
@@ -1558,11 +1791,16 @@ function App({ provider: provider2, model: model2, system: system2, tools, initi
|
|
|
1558
1791
|
}
|
|
1559
1792
|
});
|
|
1560
1793
|
useEffect(() => {
|
|
1561
|
-
if (!
|
|
1794
|
+
if (!activeProvider && initialSetup && !authFlow) {
|
|
1795
|
+
setAuthFlow({ step: "key", providerId: initialSetup.providerName, envVar: initialSetup.envVar });
|
|
1796
|
+
}
|
|
1797
|
+
}, [activeProvider, initialSetup, authFlow]);
|
|
1798
|
+
useEffect(() => {
|
|
1799
|
+
if (!startedRef.current && initialPrompt && activeProvider) {
|
|
1562
1800
|
startedRef.current = true;
|
|
1563
1801
|
void submitPrompt(initialPrompt);
|
|
1564
1802
|
}
|
|
1565
|
-
}, [initialPrompt, submitPrompt]);
|
|
1803
|
+
}, [initialPrompt, submitPrompt, activeProvider]);
|
|
1566
1804
|
const settled = rows.length > 0 ? rows.slice(0, -1) : [];
|
|
1567
1805
|
const live = rows.length > 0 ? rows[rows.length - 1] : null;
|
|
1568
1806
|
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
@@ -1570,6 +1808,60 @@ function App({ provider: provider2, model: model2, system: system2, tools, initi
|
|
|
1570
1808
|
rows.length === 0 && /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: initialMessages.length > 0 ? `Resumed session \u2014 ${initialMessages.length} messages restored. Type, or /help.` : "Type a message, or /help." }) }),
|
|
1571
1809
|
/* @__PURE__ */ jsx(Static, { items: settled, children: (row) => renderRow(row) }, session),
|
|
1572
1810
|
live && renderRow(live),
|
|
1811
|
+
authFlow?.step === "provider" && /* @__PURE__ */ jsxs(Box, { marginTop: 1, flexDirection: "column", children: [
|
|
1812
|
+
/* @__PURE__ */ jsx(Text, { bold: true, children: authFlow.mode === "login" ? "Select a provider to log in:" : "Select a provider to log out:" }),
|
|
1813
|
+
listProviders().map((p, i) => {
|
|
1814
|
+
const selected = i === authFlow.selected;
|
|
1815
|
+
const saved = getStoredKeySync(p.id) ? " \u2713 saved" : "";
|
|
1816
|
+
return /* @__PURE__ */ jsxs(Text, { color: selected ? "cyan" : void 0, dimColor: !selected, children: [
|
|
1817
|
+
selected ? "\u2192 " : " ",
|
|
1818
|
+
p.label,
|
|
1819
|
+
" ",
|
|
1820
|
+
/* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
1821
|
+
"(",
|
|
1822
|
+
p.id,
|
|
1823
|
+
")",
|
|
1824
|
+
saved
|
|
1825
|
+
] })
|
|
1826
|
+
] }, p.id);
|
|
1827
|
+
}),
|
|
1828
|
+
/* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
1829
|
+
"\u2191\u2193 navigate \xB7 enter select",
|
|
1830
|
+
activeProvider ? " \xB7 esc cancel" : ""
|
|
1831
|
+
] })
|
|
1832
|
+
] }),
|
|
1833
|
+
authFlow?.step === "key" && /* @__PURE__ */ jsxs(Box, { marginTop: 1, flexDirection: "column", children: [
|
|
1834
|
+
/* @__PURE__ */ jsxs(Text, { bold: true, children: [
|
|
1835
|
+
"Login to ",
|
|
1836
|
+
authFlow.providerId
|
|
1837
|
+
] }),
|
|
1838
|
+
/* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
1839
|
+
"Paste ",
|
|
1840
|
+
authFlow.envVar,
|
|
1841
|
+
" \u2014 saved to ",
|
|
1842
|
+
getAuthPath()
|
|
1843
|
+
] }),
|
|
1844
|
+
/* @__PURE__ */ jsxs(Box, { marginTop: 1, children: [
|
|
1845
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u203A " }),
|
|
1846
|
+
/* @__PURE__ */ jsx(
|
|
1847
|
+
TextInput,
|
|
1848
|
+
{
|
|
1849
|
+
value: keyDraft,
|
|
1850
|
+
onChange: setKeyDraft,
|
|
1851
|
+
onSubmit: (value) => {
|
|
1852
|
+
void submitKey(authFlow.providerId, value);
|
|
1853
|
+
},
|
|
1854
|
+
placeholder: "sk-\u2026",
|
|
1855
|
+
mask: "*"
|
|
1856
|
+
}
|
|
1857
|
+
)
|
|
1858
|
+
] }),
|
|
1859
|
+
authError && /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsxs(Text, { color: "red", children: [
|
|
1860
|
+
"\u2715 ",
|
|
1861
|
+
authError
|
|
1862
|
+
] }) }),
|
|
1863
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "enter save \xB7 esc back" })
|
|
1864
|
+
] }),
|
|
1573
1865
|
pending && /* @__PURE__ */ jsxs(Box, { marginTop: 1, flexDirection: "column", children: [
|
|
1574
1866
|
/* @__PURE__ */ jsxs(Text, { color: "yellow", children: [
|
|
1575
1867
|
"Permission needed: ",
|
|
@@ -1581,7 +1873,7 @@ function App({ provider: provider2, model: model2, system: system2, tools, initi
|
|
|
1581
1873
|
"\u2715 ",
|
|
1582
1874
|
error
|
|
1583
1875
|
] }) }),
|
|
1584
|
-
/* @__PURE__ */ jsxs(Box, { marginTop: 1, children: [
|
|
1876
|
+
!authFlow && /* @__PURE__ */ jsxs(Box, { marginTop: 1, children: [
|
|
1585
1877
|
/* @__PURE__ */ jsx(Text, { dimColor: working, children: "\u203A " }),
|
|
1586
1878
|
/* @__PURE__ */ jsx(
|
|
1587
1879
|
TextInput,
|
|
@@ -1593,11 +1885,11 @@ function App({ provider: provider2, model: model2, system: system2, tools, initi
|
|
|
1593
1885
|
setDraft("");
|
|
1594
1886
|
void submitPrompt(value);
|
|
1595
1887
|
},
|
|
1596
|
-
placeholder: pending ? "answer y/a/n\u2026" : working ? "working\u2026" : "Ask nimi\u2026"
|
|
1888
|
+
placeholder: pending ? "answer y/a/n\u2026" : working ? "working\u2026" : activeProvider ? "Ask nimi\u2026" : "Run /login\u2026"
|
|
1597
1889
|
}
|
|
1598
1890
|
)
|
|
1599
1891
|
] }),
|
|
1600
|
-
/* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: working ? "working\u2026 esc to interrupt" : "enter send \xB7 /
|
|
1892
|
+
/* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: authFlow ? "login flow \xB7 esc back" : working ? "working\u2026 esc to interrupt" : "enter send \xB7 /login /model /help \xB7 esc interrupt" }) })
|
|
1601
1893
|
] });
|
|
1602
1894
|
}
|
|
1603
1895
|
var DIVIDER_MAX;
|
|
@@ -1608,6 +1900,7 @@ var init_App = __esm({
|
|
|
1608
1900
|
init_compaction();
|
|
1609
1901
|
init_models();
|
|
1610
1902
|
init_providers();
|
|
1903
|
+
init_auth();
|
|
1611
1904
|
init_store();
|
|
1612
1905
|
init_format();
|
|
1613
1906
|
init_slash();
|
|
@@ -1630,6 +1923,21 @@ async function runClassic(opts) {
|
|
|
1630
1923
|
const canUseTool = opts.yolo ? allowAll() : createPromptPermission(rl);
|
|
1631
1924
|
let trace = null;
|
|
1632
1925
|
const ask = (q) => new Promise((resolve7) => rl.question(q, resolve7));
|
|
1926
|
+
const ensureKey = async (providerId) => {
|
|
1927
|
+
try {
|
|
1928
|
+
const meta = getProviderMeta(providerId);
|
|
1929
|
+
const answer = (await ask(`Enter ${meta.envVar} for ${providerId} (saved to ${getAuthPath()}): `)).trim();
|
|
1930
|
+
if (!answer) {
|
|
1931
|
+
console.log("Empty key \u2014 cancelled.");
|
|
1932
|
+
return false;
|
|
1933
|
+
}
|
|
1934
|
+
await saveApiKey(providerId, answer);
|
|
1935
|
+
return true;
|
|
1936
|
+
} catch (e) {
|
|
1937
|
+
console.log(e instanceof Error ? e.message : String(e));
|
|
1938
|
+
return false;
|
|
1939
|
+
}
|
|
1940
|
+
};
|
|
1633
1941
|
console.log(`nimi ${model2} \xB7 session ${opts.sessionId.slice(-6)} \xB7 /help for commands`);
|
|
1634
1942
|
if (messages.length > 0) console.log(`Resumed \u2014 ${messages.length} messages restored.`);
|
|
1635
1943
|
if (opts.yolo) console.log("Permission prompts off (--yolo).");
|
|
@@ -1654,6 +1962,10 @@ async function runClassic(opts) {
|
|
|
1654
1962
|
continue;
|
|
1655
1963
|
}
|
|
1656
1964
|
if (slash.type === "compact") {
|
|
1965
|
+
if (!provider2) {
|
|
1966
|
+
console.log("Not logged in. Use /login <provider> first.");
|
|
1967
|
+
continue;
|
|
1968
|
+
}
|
|
1657
1969
|
const result = await compactMessages(messages, provider2, model2);
|
|
1658
1970
|
if (!result.compacted) console.log("History is already short.");
|
|
1659
1971
|
else {
|
|
@@ -1676,11 +1988,66 @@ async function runClassic(opts) {
|
|
|
1676
1988
|
console.log(`Switched to ${model2} \u2014 history kept.`);
|
|
1677
1989
|
} catch (e) {
|
|
1678
1990
|
console.log(e instanceof Error ? e.message : String(e));
|
|
1991
|
+
if (e instanceof MissingApiKeyError && await ensureKey(e.provider)) {
|
|
1992
|
+
try {
|
|
1993
|
+
provider2 = getProvider(info.provider);
|
|
1994
|
+
model2 = info.id;
|
|
1995
|
+
console.log(`Switched to ${model2} \u2014 history kept.`);
|
|
1996
|
+
} catch (retry) {
|
|
1997
|
+
console.log(retry instanceof Error ? retry.message : String(retry));
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
1679
2000
|
}
|
|
1680
2001
|
}
|
|
1681
2002
|
}
|
|
1682
2003
|
continue;
|
|
1683
2004
|
}
|
|
2005
|
+
if (slash.type === "login") {
|
|
2006
|
+
if (!slash.arg) {
|
|
2007
|
+
console.log("Usage: /login <provider>");
|
|
2008
|
+
continue;
|
|
2009
|
+
}
|
|
2010
|
+
try {
|
|
2011
|
+
getProviderMeta(slash.arg);
|
|
2012
|
+
} catch (e) {
|
|
2013
|
+
console.log(e instanceof Error ? e.message : String(e));
|
|
2014
|
+
continue;
|
|
2015
|
+
}
|
|
2016
|
+
if (await ensureKey(slash.arg)) {
|
|
2017
|
+
try {
|
|
2018
|
+
provider2 = getProvider(slash.arg);
|
|
2019
|
+
const current = getModelInfo(model2);
|
|
2020
|
+
model2 = current?.provider === slash.arg ? model2 : provider2.defaultModel;
|
|
2021
|
+
console.log(`Logged in to ${slash.arg}. Credentials saved to ${getAuthPath()}.`);
|
|
2022
|
+
} catch (e) {
|
|
2023
|
+
console.log(e instanceof Error ? e.message : String(e));
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
2026
|
+
continue;
|
|
2027
|
+
}
|
|
2028
|
+
if (slash.type === "logout") {
|
|
2029
|
+
if (!slash.arg) {
|
|
2030
|
+
console.log("Usage: /logout <provider>");
|
|
2031
|
+
continue;
|
|
2032
|
+
}
|
|
2033
|
+
try {
|
|
2034
|
+
getProviderMeta(slash.arg);
|
|
2035
|
+
} catch (e) {
|
|
2036
|
+
console.log(e instanceof Error ? e.message : String(e));
|
|
2037
|
+
continue;
|
|
2038
|
+
}
|
|
2039
|
+
const removed = await deleteApiKey(slash.arg);
|
|
2040
|
+
console.log(removed ? `Removed stored API key for ${slash.arg}. Environment variables are unchanged.` : `No stored API key for ${slash.arg}.`);
|
|
2041
|
+
if (provider2?.name === slash.arg) {
|
|
2042
|
+
try {
|
|
2043
|
+
provider2 = getProvider(slash.arg);
|
|
2044
|
+
} catch {
|
|
2045
|
+
provider2 = null;
|
|
2046
|
+
console.log(`Logged out of ${slash.arg} \u2014 add a key with /login or switch with /model.`);
|
|
2047
|
+
}
|
|
2048
|
+
}
|
|
2049
|
+
continue;
|
|
2050
|
+
}
|
|
1684
2051
|
if (slash.type === "tools") {
|
|
1685
2052
|
for (const t of ALL_TOOLS) console.log(` ${t.name}${t.readOnly ? " [read-only]" : " [asks permission]"}`);
|
|
1686
2053
|
continue;
|
|
@@ -1701,6 +2068,10 @@ async function runClassic(opts) {
|
|
|
1701
2068
|
console.log(`Unknown command: ${slash.name}. Try /help.`);
|
|
1702
2069
|
continue;
|
|
1703
2070
|
}
|
|
2071
|
+
if (!provider2) {
|
|
2072
|
+
console.log("Not logged in. Use /login <provider> first.");
|
|
2073
|
+
continue;
|
|
2074
|
+
}
|
|
1704
2075
|
messages.push({ role: "user", content: line });
|
|
1705
2076
|
if (opts.tracePath && !trace) {
|
|
1706
2077
|
trace = createTraceRecorder(opts.tracePath, {
|
|
@@ -1743,6 +2114,7 @@ var init_classic = __esm({
|
|
|
1743
2114
|
init_agent();
|
|
1744
2115
|
init_compaction();
|
|
1745
2116
|
init_providers();
|
|
2117
|
+
init_auth();
|
|
1746
2118
|
init_models();
|
|
1747
2119
|
init_permissions();
|
|
1748
2120
|
init_store();
|
|
@@ -1865,7 +2237,11 @@ OPTIONS
|
|
|
1865
2237
|
-h, --help Show this help
|
|
1866
2238
|
|
|
1867
2239
|
PROVIDERS
|
|
1868
|
-
anthropic, anthropic-openai, groq, meta, deepseek
|
|
2240
|
+
anthropic, anthropic-openai, groq, meta, deepseek, openai
|
|
2241
|
+
|
|
2242
|
+
AUTH
|
|
2243
|
+
First run opens /login in the TUI (keys saved to ~/.nimi/auth.json).
|
|
2244
|
+
Headless/CI: export the provider's key instead (e.g. META_API_KEY).
|
|
1869
2245
|
|
|
1870
2246
|
EXAMPLES
|
|
1871
2247
|
nimi -p "explain this codebase"
|
|
@@ -1874,7 +2250,7 @@ EXAMPLES
|
|
|
1874
2250
|
`);
|
|
1875
2251
|
}
|
|
1876
2252
|
function printVersion() {
|
|
1877
|
-
console.log(`nimi ${true ? "1.
|
|
2253
|
+
console.log(`nimi ${true ? "1.1.0" : "dev"}`);
|
|
1878
2254
|
}
|
|
1879
2255
|
|
|
1880
2256
|
// src/config/system-prompt.ts
|
|
@@ -1945,6 +2321,7 @@ async function readJsonSafe(path) {
|
|
|
1945
2321
|
// src/main.tsx
|
|
1946
2322
|
init_permissions();
|
|
1947
2323
|
init_providers();
|
|
2324
|
+
init_auth();
|
|
1948
2325
|
init_store();
|
|
1949
2326
|
init_recorder();
|
|
1950
2327
|
init_tools();
|
|
@@ -1967,9 +2344,6 @@ if (args.version) {
|
|
|
1967
2344
|
process.exit(0);
|
|
1968
2345
|
}
|
|
1969
2346
|
var settings = await loadSettings();
|
|
1970
|
-
var provider = getProvider(args.provider ?? settings.provider ?? "meta");
|
|
1971
|
-
var model = args.model ?? settings.model ?? provider.defaultModel;
|
|
1972
|
-
var system = await buildSystemPrompt(settings.systemPromptExtra);
|
|
1973
2347
|
if (args.listSessions) {
|
|
1974
2348
|
const ids = await listSessions();
|
|
1975
2349
|
if (ids.length === 0) console.log("No saved sessions yet.");
|
|
@@ -1988,7 +2362,37 @@ if (args.listSessions) {
|
|
|
1988
2362
|
}
|
|
1989
2363
|
process.exit(0);
|
|
1990
2364
|
}
|
|
2365
|
+
var providerName = args.provider ?? settings.provider ?? "meta";
|
|
2366
|
+
var provider = null;
|
|
2367
|
+
var missingAuth = null;
|
|
2368
|
+
var model;
|
|
2369
|
+
try {
|
|
2370
|
+
provider = getProvider(providerName);
|
|
2371
|
+
model = args.model ?? settings.model ?? provider.defaultModel;
|
|
2372
|
+
} catch (e) {
|
|
2373
|
+
if (e instanceof MissingApiKeyError) {
|
|
2374
|
+
missingAuth = { providerName: e.provider, envVar: e.envVar };
|
|
2375
|
+
try {
|
|
2376
|
+
model = args.model ?? settings.model ?? getProviderDefaultModel(providerName);
|
|
2377
|
+
} catch (unknownProvider) {
|
|
2378
|
+
console.error(unknownProvider instanceof Error ? unknownProvider.message : String(unknownProvider));
|
|
2379
|
+
process.exit(1);
|
|
2380
|
+
}
|
|
2381
|
+
} else {
|
|
2382
|
+
console.error(e instanceof Error ? e.message : String(e));
|
|
2383
|
+
process.exit(1);
|
|
2384
|
+
}
|
|
2385
|
+
}
|
|
2386
|
+
var system = await buildSystemPrompt(settings.systemPromptExtra);
|
|
1991
2387
|
if (!args.classic && (args.print || !process.stdin.isTTY || !process.stdout.isTTY)) {
|
|
2388
|
+
if (missingAuth) {
|
|
2389
|
+
console.error(`No API key found for ${missingAuth.providerName}. Set ${missingAuth.envVar}, or run "nimi" interactively and use /login ${missingAuth.providerName}.`);
|
|
2390
|
+
process.exit(1);
|
|
2391
|
+
}
|
|
2392
|
+
if (!provider) {
|
|
2393
|
+
console.error("No provider configured.");
|
|
2394
|
+
process.exit(1);
|
|
2395
|
+
}
|
|
1992
2396
|
if (!args.prompt) {
|
|
1993
2397
|
console.error('No prompt given. Run "nimi --help" for usage.');
|
|
1994
2398
|
process.exit(1);
|
|
@@ -2069,13 +2473,34 @@ if (args.session) {
|
|
|
2069
2473
|
} else {
|
|
2070
2474
|
tuiSessionId = newSessionId();
|
|
2071
2475
|
tuiMessages = [];
|
|
2072
|
-
await appendEntry(tuiSessionId, { type: "session_info", id: tuiSessionId, created_at: (/* @__PURE__ */ new Date()).toISOString(), model, provider: provider
|
|
2476
|
+
await appendEntry(tuiSessionId, { type: "session_info", id: tuiSessionId, created_at: (/* @__PURE__ */ new Date()).toISOString(), model, provider: provider?.name ?? providerName });
|
|
2073
2477
|
}
|
|
2074
2478
|
if (args.classic) {
|
|
2479
|
+
if (!provider && missingAuth) {
|
|
2480
|
+
const askRl = createInterface2({ input: process.stdin, output: process.stdout });
|
|
2481
|
+
const answer = await new Promise((resolve7) => askRl.question(`No API key for ${missingAuth.providerName}. Enter ${missingAuth.envVar} (saved to ~/.nimi/auth.json): `, resolve7));
|
|
2482
|
+
askRl.close();
|
|
2483
|
+
if (!answer.trim()) {
|
|
2484
|
+
console.error(`No API key given. Set ${missingAuth.envVar} or run "nimi" and use /login.`);
|
|
2485
|
+
process.exit(1);
|
|
2486
|
+
}
|
|
2487
|
+
await saveApiKey(missingAuth.providerName, answer.trim());
|
|
2488
|
+
try {
|
|
2489
|
+
provider = getProvider(missingAuth.providerName);
|
|
2490
|
+
model = args.model ?? settings.model ?? provider.defaultModel;
|
|
2491
|
+
} catch (e) {
|
|
2492
|
+
console.error(e instanceof Error ? e.message : String(e));
|
|
2493
|
+
process.exit(1);
|
|
2494
|
+
}
|
|
2495
|
+
}
|
|
2496
|
+
if (!provider) {
|
|
2497
|
+
console.error("No provider configured.");
|
|
2498
|
+
process.exit(1);
|
|
2499
|
+
}
|
|
2075
2500
|
const { runClassic: runClassic2 } = await Promise.resolve().then(() => (init_classic(), classic_exports));
|
|
2076
2501
|
await runClassic2({ provider, model, system, tools: ALL_TOOLS, sessionId: tuiSessionId, initialMessages: tuiMessages, yolo: args.yolo, contextLimit: args.contextLimit, tracePath: args.trace });
|
|
2077
2502
|
process.exit(0);
|
|
2078
2503
|
}
|
|
2079
2504
|
render(
|
|
2080
|
-
/* @__PURE__ */ jsx2(App2, { provider, model, system, tools: ALL_TOOLS, initialPrompt: args.prompt, yolo: args.yolo, sessionId: tuiSessionId, initialMessages: tuiMessages, contextLimit: args.contextLimit })
|
|
2505
|
+
/* @__PURE__ */ jsx2(App2, { provider, model, system, tools: ALL_TOOLS, initialPrompt: args.prompt, yolo: args.yolo, sessionId: tuiSessionId, initialMessages: tuiMessages, contextLimit: args.contextLimit, initialSetup: missingAuth })
|
|
2081
2506
|
);
|
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -1,77 +1,187 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<picture>
|
|
3
|
+
<source media="(prefers-color-scheme: dark)" srcset="docs/assets/nimi-logo-dark.png">
|
|
4
|
+
<img alt="Nimi" src="docs/assets/nimi-logo-light.png" width="480">
|
|
5
|
+
</picture>
|
|
6
|
+
</p>
|
|
2
7
|
|
|
3
|
-
|
|
4
|
-
|
|
8
|
+
<p align="center">
|
|
9
|
+
<strong>A lightweight, open-source terminal coding-agent harness. Every step, inspectable.</strong>
|
|
10
|
+
</p>
|
|
5
11
|
|
|
6
|
-
|
|
12
|
+
<p align="center">
|
|
13
|
+
<a href="https://www.npmjs.com/package/nimi-code"><img alt="npm version" src="https://img.shields.io/npm/v/nimi-code?style=flat-square"></a>
|
|
14
|
+
<a href="LICENSE"><img alt="license" src="https://img.shields.io/npm/l/nimi-code?style=flat-square"></a>
|
|
15
|
+
</p>
|
|
16
|
+
|
|
17
|
+
# nimi
|
|
18
|
+
|
|
19
|
+
Nimi points a language model at your codebase and gives it controlled access to your files, shell, and git — read, search, write, edit, run commands — until the job is done. Read-only tools run freely; anything that mutates pauses at an explicit permission gate. Every run is recorded as a JSONL trace you can replay, and a browser playground visualizes a sample run step by step.
|
|
20
|
+
|
|
21
|
+
What Nimi is **not**: not a model, not a subscription, not a hosted platform. You bring your own provider key, run everything on your own machine, and can fork the whole thing (ISC licensed).
|
|
22
|
+
|
|
23
|
+
## Quickstart
|
|
24
|
+
|
|
25
|
+
Requires **Node 22+** and one provider key.
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm install -g nimi-code
|
|
29
|
+
export META_API_KEY=... # or ANTHROPIC_API_KEY, GROQ_API_KEY, DEEPSEEK_API_KEY, OPENAI_API_KEY
|
|
30
|
+
cd /path/to/your/project
|
|
7
31
|
nimi "list the typescript files under src"
|
|
8
|
-
echo "explain the agent loop" | nimi -p
|
|
9
|
-
nimi --classic "refactor src/tools/grep.ts"
|
|
10
32
|
```
|
|
11
33
|
|
|
12
|
-
|
|
34
|
+
That's it — Nimi opens an interactive session in your terminal. Give it a task in plain language.
|
|
35
|
+
|
|
36
|
+
## Usage
|
|
13
37
|
|
|
14
|
-
|
|
38
|
+
| Mode | Command | Behavior |
|
|
39
|
+
|---|---|---|
|
|
40
|
+
| Interactive (default) | `nimi "task"` | Full terminal UI with streaming output |
|
|
41
|
+
| Classic REPL | `nimi --classic "task"` | Plain readline loop, compact result previews |
|
|
42
|
+
| One-shot | `nimi --print "task"` | Runs once, streams to stdout, exits (scriptable) |
|
|
43
|
+
| Pipe | `echo ... \| nimi -p` | Prompt from stdin, no TTY needed |
|
|
44
|
+
|
|
45
|
+
### Flags
|
|
15
46
|
|
|
16
47
|
```
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
nimi
|
|
48
|
+
nimi [message] interactive (default)
|
|
49
|
+
nimi --classic [message] plain REPL
|
|
50
|
+
nimi --print [message] one-shot, streams to stdout
|
|
51
|
+
echo ... | nimi -p prompt from pipe
|
|
52
|
+
|
|
53
|
+
-p, --prompt <text> prompt to send
|
|
54
|
+
-z, --provider <id> anthropic, anthropic-openai, groq, meta (default), deepseek, openai
|
|
55
|
+
--model <id> override the provider's default model
|
|
56
|
+
--session <id> resume a session (--list-sessions to list)
|
|
57
|
+
--context-limit <n> override context window
|
|
58
|
+
--trace <path> write a JSONL showcase trace (print mode)
|
|
59
|
+
--classic --print --yolo
|
|
60
|
+
-v, --version -h, --help
|
|
20
61
|
```
|
|
21
62
|
|
|
22
|
-
|
|
63
|
+
### Slash commands (interactive mode)
|
|
64
|
+
|
|
65
|
+
`/model` `/tools` `/sessions` `/session` `/compact` `/context` `/clear` `/help` `/quit` — bare `clear`, `quit`, `exit`, and `q` work too.
|
|
66
|
+
|
|
67
|
+
Examples:
|
|
23
68
|
|
|
69
|
+
```bash
|
|
70
|
+
nimi -p "explain this codebase" --provider deepseek
|
|
71
|
+
nimi "find all TODOs and write them to NOTES.md"
|
|
72
|
+
nimi --session abc123 "continue where we left off"
|
|
73
|
+
nimi --list-sessions
|
|
24
74
|
```
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
75
|
+
|
|
76
|
+
## Tools & permissions
|
|
77
|
+
|
|
78
|
+
Read-only tools run without asking. Mutating tools pause at a gate: `[y]es` / `[a]lways` / `[n]o` — `always` is remembered per tool for the session. Piped/non-TTY runs auto-allow (pass `--yolo` explicitly to skip prompts in a terminal).
|
|
79
|
+
|
|
80
|
+
| Runs freely (read-only) | Asks first (mutating) |
|
|
81
|
+
|---|---|
|
|
82
|
+
| `read`, `ls`, `find`, `grep` | `write`, `edit`, `bash` |
|
|
83
|
+
| `git_status`, `git_log`, `git_diff` | `git_add`, `git_commit`, `git_branch`, `git_checkout`, `git_reset` |
|
|
84
|
+
|
|
85
|
+
Denying a tool doesn't crash the run — the model gets an error result (`User denied <name>…`) and decides what to do next.
|
|
86
|
+
|
|
87
|
+
## Providers
|
|
88
|
+
|
|
89
|
+
Nimi is provider-agnostic over OpenAI-compatible APIs. The default provider is `meta`.
|
|
90
|
+
|
|
91
|
+
| Provider (`-z`) | Key | Default model |
|
|
92
|
+
|---|---|---|
|
|
93
|
+
| `anthropic` | `ANTHROPIC_API_KEY` | `claude-sonnet-5` |
|
|
94
|
+
| `anthropic-openai` | `ANTHROPIC_API_KEY` | `claude-sonnet-5` (OpenAI-compatible endpoint) |
|
|
95
|
+
| `groq` | `GROQ_API_KEY` | `qwen/qwen3.8-27b` |
|
|
96
|
+
| `meta` (default) | `META_API_KEY` | `muse-spark-1.3-contributor` |
|
|
97
|
+
| `deepseek` | `DEEPSEEK_API_KEY` | `deepseek-flash` |
|
|
98
|
+
| `openai` | `OPENAI_API_KEY` | `gpt-4o-mini` |
|
|
99
|
+
|
|
100
|
+
Override the model per run with `--model <id>`, per environment with `NIMI_MODEL`, or switch mid-session with `/model`.
|
|
101
|
+
|
|
102
|
+
## Config, sessions, context
|
|
103
|
+
|
|
104
|
+
Settings cascade (each level wins over the previous):
|
|
105
|
+
|
|
106
|
+
`~/.nimi/settings.json` → `.nimi/settings.json` (project) → `NIMI_PROVIDER` / `NIMI_MODEL` → CLI flags
|
|
107
|
+
|
|
108
|
+
Sessions persist per project in `.nimi/sessions/` — resume with `--session <id>`, list with `--list-sessions`. On startup Nimi also loads project context from `AGENTS.md`, `CLAUDE.md`, `.context`, and `.nimi/context.md` when present.
|
|
109
|
+
|
|
110
|
+
## Traces
|
|
111
|
+
|
|
112
|
+
Every run can be recorded as JSONL — one line per event and message snapshot — and resumed or inspected later:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
nimi --print -p "find all TODOs" --trace demo/samples/showcase.jsonl
|
|
29
116
|
```
|
|
30
117
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
118
|
+
The trace is the same artifact the web playground replays below: events fold into turns, tool calls, permission decisions, and terminal output.
|
|
119
|
+
|
|
120
|
+
## Web playground (local)
|
|
121
|
+
|
|
122
|
+
`demo/` is a static Vite + React app that replays the bundled sample trace — a trace viewer and explainer, not a live agent. No API key needed.
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
cd demo
|
|
126
|
+
npm install
|
|
127
|
+
npm run dev # → http://localhost:5173
|
|
37
128
|
```
|
|
38
129
|
|
|
39
|
-
|
|
130
|
+
- **Home** explains the harness; **Playground** (`#/playground`) is the workbench: Dataflow/Streaming graph, step inspector (Overview, Events, Context, Source, plus raw payload/stdout), timeline scrubber, keyboard shortcuts (`space` play, `←/→` step), and URL-addressable playback state.
|
|
131
|
+
- The **sample selector** replays four lenses over the same trace (full run, denied-permission branch, Classic view, TUI view). **Allow/Deny only switches the replay branch** — it controls no live machine.
|
|
132
|
+
- Production build: `npm run build` → `demo/dist/` (gitignored), deployable to any static host.
|
|
133
|
+
|
|
134
|
+
## Architecture
|
|
40
135
|
|
|
41
136
|
```
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
echo ... | nimi -p Read the prompt from a pipe
|
|
46
|
-
|
|
47
|
-
Options
|
|
48
|
-
-p, --prompt <text> Prompt to send
|
|
49
|
-
-z, --provider <id> anthropic, groq, meta, deepseek, openai, anthropic-openai
|
|
50
|
-
--model <id> Model id (registry in src/providers/models.ts)
|
|
51
|
-
--session <id> Resume a saved session (--list-sessions to list)
|
|
52
|
-
--context-limit <n> Override the model's context window
|
|
53
|
-
--classic --print --yolo REPL / one-shot / skip permission prompts
|
|
54
|
-
-v, --version -h, --help
|
|
137
|
+
prompt → parseArgs → settings → system prompt + tool catalog → runAgent()
|
|
138
|
+
→ provider.stream() → assistant reply → permission gate → tool.execute()
|
|
139
|
+
→ tool result → … → session file (.nimi/sessions/<id>.jsonl) + optional --trace JSONL
|
|
55
140
|
```
|
|
56
141
|
|
|
57
|
-
|
|
142
|
+
<picture>
|
|
143
|
+
<source media="(prefers-color-scheme: dark)" srcset="docs/assets/architecture-dark.svg">
|
|
144
|
+
<img alt="Nimi architecture map: shell and entry, config, session, agent loop, provider, tools, permissions, renderer" src="docs/assets/architecture-light.svg">
|
|
145
|
+
</picture>
|
|
146
|
+
|
|
147
|
+
Generated from the same graph data as the playground (`demo/src/data/architecture.js`). Regenerate with `node scripts/export-architecture-svg.mjs`.
|
|
148
|
+
|
|
149
|
+
### Repo layout
|
|
150
|
+
|
|
151
|
+
| Path | Contents |
|
|
152
|
+
|---|---|
|
|
153
|
+
| `bin/` | `nimi` executable entry |
|
|
154
|
+
| `src/main.tsx` | CLI entry, mode router, trace wiring |
|
|
155
|
+
| `src/agent.ts` | Agent loop (`runAgent`) |
|
|
156
|
+
| `src/cli/` | Flag parser |
|
|
157
|
+
| `src/config/` | Settings cascade, system prompt |
|
|
158
|
+
| `src/providers/` | Anthropic + OpenAI-compatible providers, default models |
|
|
159
|
+
| `src/tools/` | Read/write/shell/git tool implementations |
|
|
160
|
+
| `src/permissions.ts` | `y/a/n` permission gate |
|
|
161
|
+
| `src/session/` | JSONL session store |
|
|
162
|
+
| `src/trace/` | JSONL trace recorder |
|
|
163
|
+
| `src/modes/` `src/tui/` | Classic REPL and Ink terminal UI |
|
|
164
|
+
| `demo/` | Static web playground (sample trace replay) |
|
|
165
|
+
| `test/` | Offline tool tests |
|
|
166
|
+
| `docs/assets/` | README logo and architecture diagram |
|
|
58
167
|
|
|
59
|
-
##
|
|
168
|
+
## Development
|
|
60
169
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
170
|
+
```bash
|
|
171
|
+
npm install
|
|
172
|
+
cp .env.example .env # add one provider key
|
|
173
|
+
npm run build # build with tsup
|
|
174
|
+
node bin/nimi.js "hello"
|
|
64
175
|
|
|
65
|
-
|
|
176
|
+
npm run check # biome lint + tsc typecheck
|
|
177
|
+
npm test # offline tool tests (vitest)
|
|
178
|
+
npm run test:tools
|
|
179
|
+
```
|
|
66
180
|
|
|
67
|
-
|
|
68
|
-
< CLI flags. Sessions live in `.nimi/sessions/<id>.jsonl` (gitignored).
|
|
181
|
+
## Contributing
|
|
69
182
|
|
|
70
|
-
|
|
183
|
+
Issues and pull requests are welcome. Run `npm run check` and `npm test` before submitting. See `AGENTS.md` for repo conventions (Astryx workflow for `demo/`, tokens, required checks).
|
|
71
184
|
|
|
72
|
-
|
|
73
|
-
npm run check # biome lint + tsc --noEmit (0 errors)
|
|
74
|
-
npm run test:tools # offline tool suite, no keys (7 tests)
|
|
75
|
-
```
|
|
185
|
+
## License
|
|
76
186
|
|
|
77
|
-
|
|
187
|
+
[ISC](LICENSE) — do what you want, keep the copyright notice.
|