ivorleaf 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/LICENSE.md +9 -0
- package/README.md +134 -0
- package/dist/api.js +268 -0
- package/dist/commands/action.js +19 -0
- package/dist/commands/clear.js +7 -0
- package/dist/commands/doctor.js +41 -0
- package/dist/commands/explore.js +40 -0
- package/dist/commands/history.js +20 -0
- package/dist/commands/init.js +16 -0
- package/dist/commands/limits.js +21 -0
- package/dist/commands/open.js +25 -0
- package/dist/commands/orchestrate.js +19 -0
- package/dist/commands/sources.js +16 -0
- package/dist/commands/status.js +20 -0
- package/dist/commands/workflowMenu.js +87 -0
- package/dist/config.js +33 -0
- package/dist/index.js +88 -0
- package/dist/render.js +600 -0
- package/dist/utils/activeSession.js +58 -0
- package/dist/utils/prompt.js +4 -0
- package/dist/utils/recentSources.js +27 -0
- package/package.json +38 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Proprietary License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ivorleaf. All rights reserved.
|
|
4
|
+
|
|
5
|
+
This software and associated documentation files (the "Software") are proprietary and confidential. No license, right, title, or interest in the Software is granted except by prior written permission from Ivorleaf.
|
|
6
|
+
|
|
7
|
+
You may not copy, modify, distribute, sublicense, sell, lease, publish, disclose, reverse engineer, decompile, or otherwise use the Software except as expressly authorized in writing by Ivorleaf.
|
|
8
|
+
|
|
9
|
+
All rights not expressly granted are reserved.
|
package/README.md
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# Ivorleaf
|
|
2
|
+
|
|
3
|
+
A cross-platform terminal client for the Ivorleaf API. Ivorleaf renders lessons and workflows as terminal-friendly Markdown, keeps an active learning session, and supports raw JSON output for debugging.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npm install -g ivorleaf
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Requires Node.js 20 or newer.
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
ivorleaf init
|
|
17
|
+
ivorleaf explore "using sql for lead management"
|
|
18
|
+
ivorleaf quiz
|
|
19
|
+
ivorleaf sources
|
|
20
|
+
ivorleaf citations
|
|
21
|
+
ivorleaf status
|
|
22
|
+
ivorleaf exit
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`explore` creates a lesson and stores the returned session as active. Follow-up commands such as `quiz`, `flashcards`, `citations`, `compare`, and `continue` use that active session automatically.
|
|
26
|
+
|
|
27
|
+
## Authentication
|
|
28
|
+
|
|
29
|
+
### `ivorleaf init`
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
ivorleaf init
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
`init` prompts for an Ivorleaf API key, validates it, and stores it at `~/.ivorleaf/config.json` with user-only permissions. The CLI never persists the API URL.
|
|
36
|
+
|
|
37
|
+
Environment variables can override saved configuration for development or CI:
|
|
38
|
+
|
|
39
|
+
```sh
|
|
40
|
+
export IVORLEAF_API_URL="https://ivorleaf-api-745737209421.us-east1.run.app"
|
|
41
|
+
export IVORLEAF_API_KEY="your-key"
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The environment API key takes precedence over the saved key.
|
|
45
|
+
|
|
46
|
+
## Commands
|
|
47
|
+
|
|
48
|
+
```text
|
|
49
|
+
ivorleaf init
|
|
50
|
+
ivorleaf doctor
|
|
51
|
+
ivorleaf limits
|
|
52
|
+
ivorleaf explore <prompt...>
|
|
53
|
+
ivorleaf quiz
|
|
54
|
+
ivorleaf flashcards
|
|
55
|
+
ivorleaf sources
|
|
56
|
+
ivorleaf citations
|
|
57
|
+
ivorleaf compare
|
|
58
|
+
ivorleaf continue
|
|
59
|
+
ivorleaf status
|
|
60
|
+
ivorleaf exit
|
|
61
|
+
ivorleaf clear-session
|
|
62
|
+
ivorleaf history
|
|
63
|
+
ivorleaf open <session_id>
|
|
64
|
+
ivorleaf action <action_name> [session_id]
|
|
65
|
+
ivorleaf recommend [session_id]
|
|
66
|
+
ivorleaf help
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Useful output flags:
|
|
70
|
+
|
|
71
|
+
```text
|
|
72
|
+
--json Print the raw JSON API response
|
|
73
|
+
--plain Print unstyled Markdown
|
|
74
|
+
--sources Include full source details when available
|
|
75
|
+
--debug-response Print non-secret response-shape diagnostics
|
|
76
|
+
--pager Force paged output
|
|
77
|
+
--no-pager Disable paged output
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Long terminal output opens in a paged reading view with session context and bottom hints. Use `--no-pager` for continuous output.
|
|
81
|
+
|
|
82
|
+
## Examples
|
|
83
|
+
|
|
84
|
+
```sh
|
|
85
|
+
ivorleaf explore "Explain Newton's second law"
|
|
86
|
+
ivorleaf explore using sql for lead management --show-sources
|
|
87
|
+
ivorleaf quiz --sources
|
|
88
|
+
ivorleaf flashcards
|
|
89
|
+
ivorleaf citations
|
|
90
|
+
ivorleaf compare
|
|
91
|
+
ivorleaf continue --pager
|
|
92
|
+
ivorleaf status
|
|
93
|
+
ivorleaf clear-session
|
|
94
|
+
ivorleaf history --json
|
|
95
|
+
ivorleaf open SESSION_ID --plain
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Paged output keeps the command, active session, topic/title, source and warning counts, and available workflows visible while the lesson or workflow body scrolls. Hints shown at the bottom include `q quit`, `↑/↓ scroll`, `Enter continue`, `s sources`, `c citations`, and `w workflows`.
|
|
99
|
+
|
|
100
|
+
Every request sends:
|
|
101
|
+
|
|
102
|
+
```text
|
|
103
|
+
Authorization: Bearer <api_key>
|
|
104
|
+
X-Ivorleaf-Client: cli
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Development
|
|
108
|
+
|
|
109
|
+
```sh
|
|
110
|
+
npm install
|
|
111
|
+
npm run typecheck
|
|
112
|
+
npm test
|
|
113
|
+
npm run build
|
|
114
|
+
node dist/index.js help
|
|
115
|
+
node dist/index.js doctor
|
|
116
|
+
node dist/index.js explore "Explain CRISPR simply"
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Before publishing, run:
|
|
120
|
+
|
|
121
|
+
```sh
|
|
122
|
+
npm run prepublishOnly
|
|
123
|
+
npm pack
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Do not publish from local verification unless you intend to release:
|
|
127
|
+
|
|
128
|
+
```sh
|
|
129
|
+
npm publish
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## License
|
|
133
|
+
|
|
134
|
+
Proprietary / All Rights Reserved. See [LICENSE.md](LICENSE.md).
|
package/dist/api.js
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import { getApiKey, getApiUrl, redactSecrets } from "./config.js";
|
|
2
|
+
export const ACTION_ROUTES = {
|
|
3
|
+
quiz: "/v1/history/{session_id}/quiz/generate",
|
|
4
|
+
flashcards: "/v1/history/{session_id}/flashcards/generate",
|
|
5
|
+
table: "/v1/history/{session_id}/workflows/table",
|
|
6
|
+
citation_builder: "/v1/history/{session_id}/workflows/citations",
|
|
7
|
+
educator_mode: "/v1/history/{session_id}/actions/educator_mode",
|
|
8
|
+
sop_mode: "/v1/history/{session_id}/actions/sop_mode",
|
|
9
|
+
review_sheet: "/v1/history/{session_id}/actions/review_sheet",
|
|
10
|
+
literature_expansion: "/v1/history/{session_id}/actions/literature_expansion",
|
|
11
|
+
source_comparison: "/v1/history/{session_id}/actions/source_comparison",
|
|
12
|
+
};
|
|
13
|
+
export function resolveActionRoute(sessionId, actionName) {
|
|
14
|
+
const route = ACTION_ROUTES[actionName];
|
|
15
|
+
if (!route) {
|
|
16
|
+
throw new ApiError(`Unknown workflow action "${actionName}". Supported actions: ${Object.keys(ACTION_ROUTES).join(", ")}`);
|
|
17
|
+
}
|
|
18
|
+
return route.replace("{session_id}", encodeURIComponent(sessionId));
|
|
19
|
+
}
|
|
20
|
+
export class ApiError extends Error {
|
|
21
|
+
status;
|
|
22
|
+
response;
|
|
23
|
+
constructor(message, status, response) {
|
|
24
|
+
super(message);
|
|
25
|
+
this.status = status;
|
|
26
|
+
this.response = response;
|
|
27
|
+
this.name = "ApiError";
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export class IvorleafApi {
|
|
31
|
+
apiKey;
|
|
32
|
+
baseUrl;
|
|
33
|
+
constructor(apiKey, baseUrl = getApiUrl()) {
|
|
34
|
+
this.apiKey = apiKey;
|
|
35
|
+
this.baseUrl = baseUrl;
|
|
36
|
+
}
|
|
37
|
+
static async fromConfig() {
|
|
38
|
+
const key = await getApiKey();
|
|
39
|
+
if (!key)
|
|
40
|
+
throw new ApiError("No API key is configured.\n\nRun:\nivorleaf init");
|
|
41
|
+
return new IvorleafApi(key);
|
|
42
|
+
}
|
|
43
|
+
async request(path, init = {}) {
|
|
44
|
+
let response;
|
|
45
|
+
try {
|
|
46
|
+
response = await fetch(`${this.baseUrl}${path}`, {
|
|
47
|
+
...init,
|
|
48
|
+
headers: {
|
|
49
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
50
|
+
"X-Ivorleaf-Client": "cli",
|
|
51
|
+
Accept: "application/json",
|
|
52
|
+
...(init.body ? { "Content-Type": "application/json" } : {}),
|
|
53
|
+
...init.headers,
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
throw new ApiError(`API unreachable. Verify internet connectivity and API availability.\n${redactSecrets(String(error))}`);
|
|
59
|
+
}
|
|
60
|
+
const text = await response.text();
|
|
61
|
+
let data = null;
|
|
62
|
+
if (text) {
|
|
63
|
+
try {
|
|
64
|
+
data = JSON.parse(text);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
data = text;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (!response.ok) {
|
|
71
|
+
const guidance = response.status === 401 || response.status === 403
|
|
72
|
+
? "Verify the API key is active and has not been revoked. Run: ivorleaf init"
|
|
73
|
+
: `The API returned HTTP ${response.status}.`;
|
|
74
|
+
throw new ApiError(guidance, response.status, data);
|
|
75
|
+
}
|
|
76
|
+
if (text && typeof data === "string") {
|
|
77
|
+
throw new ApiError("The API returned a non-JSON response. Verify IVORLEAF_API_URL points to the Ivorleaf API.", response.status);
|
|
78
|
+
}
|
|
79
|
+
return data;
|
|
80
|
+
}
|
|
81
|
+
usageStatus() {
|
|
82
|
+
return this.request("/v1/api/usage/status");
|
|
83
|
+
}
|
|
84
|
+
createLesson(prompt) {
|
|
85
|
+
// TODO: Align this deliberately isolated payload if the FastAPI schema differs.
|
|
86
|
+
return this.request("/v1/lesson", { method: "POST", body: JSON.stringify({ prompt }) });
|
|
87
|
+
}
|
|
88
|
+
history() {
|
|
89
|
+
return this.request("/v1/history");
|
|
90
|
+
}
|
|
91
|
+
getSession(sessionId) {
|
|
92
|
+
return this.request(`/v1/history/${encodeURIComponent(sessionId)}`);
|
|
93
|
+
}
|
|
94
|
+
action(sessionId, actionName) {
|
|
95
|
+
return this.request(resolveActionRoute(sessionId, actionName), {
|
|
96
|
+
method: "POST",
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
orchestrate(sessionId, intent) {
|
|
100
|
+
// TODO: Align the intent field if the FastAPI request model uses a different name.
|
|
101
|
+
return this.request(`/v1/history/${encodeURIComponent(sessionId)}/orchestrate`, {
|
|
102
|
+
method: "POST",
|
|
103
|
+
body: JSON.stringify({ intent }),
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
export function asObject(value) {
|
|
108
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
109
|
+
}
|
|
110
|
+
export function findString(value, keys) {
|
|
111
|
+
const object = asObject(value);
|
|
112
|
+
for (const key of keys)
|
|
113
|
+
if (typeof object[key] === "string")
|
|
114
|
+
return object[key];
|
|
115
|
+
for (const nested of ["data", "result", "lesson", "session", "output"]) {
|
|
116
|
+
if (object[nested]) {
|
|
117
|
+
const found = findString(object[nested], keys);
|
|
118
|
+
if (found)
|
|
119
|
+
return found;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
export function findPlan(value) {
|
|
125
|
+
return findNamedMetadataString(value, ["plan", "plan_name", "tier", "subscription_plan", "account_plan"]);
|
|
126
|
+
}
|
|
127
|
+
export function findRevisionId(value) {
|
|
128
|
+
return findNamedMetadataString(value, ["revision_id", "revisionId"]);
|
|
129
|
+
}
|
|
130
|
+
export function findSessionId(value, seen = new Set()) {
|
|
131
|
+
if (!value || typeof value !== "object" || seen.has(value))
|
|
132
|
+
return null;
|
|
133
|
+
seen.add(value);
|
|
134
|
+
if (Array.isArray(value)) {
|
|
135
|
+
for (const item of value) {
|
|
136
|
+
const found = findSessionId(item, seen);
|
|
137
|
+
if (found)
|
|
138
|
+
return found;
|
|
139
|
+
}
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
const object = value;
|
|
143
|
+
for (const key of ["session_id", "sessionId"]) {
|
|
144
|
+
const candidate = object[key];
|
|
145
|
+
if (typeof candidate === "string" && candidate.trim())
|
|
146
|
+
return candidate.trim();
|
|
147
|
+
if (typeof candidate === "number" && Number.isFinite(candidate))
|
|
148
|
+
return String(candidate);
|
|
149
|
+
}
|
|
150
|
+
// An `id` is a session identifier only inside an explicitly named session object.
|
|
151
|
+
for (const key of ["session", "history_session"]) {
|
|
152
|
+
const session = asObject(object[key]);
|
|
153
|
+
const candidate = session.id;
|
|
154
|
+
if (typeof candidate === "string" && candidate.trim())
|
|
155
|
+
return candidate.trim();
|
|
156
|
+
if (typeof candidate === "number" && Number.isFinite(candidate))
|
|
157
|
+
return String(candidate);
|
|
158
|
+
}
|
|
159
|
+
for (const child of Object.values(object)) {
|
|
160
|
+
const found = findSessionId(child, seen);
|
|
161
|
+
if (found)
|
|
162
|
+
return found;
|
|
163
|
+
}
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
function findNamedMetadataString(value, keys, seen = new Set()) {
|
|
167
|
+
if (!value || typeof value !== "object" || seen.has(value))
|
|
168
|
+
return null;
|
|
169
|
+
seen.add(value);
|
|
170
|
+
if (Array.isArray(value)) {
|
|
171
|
+
for (const item of value) {
|
|
172
|
+
const found = findNamedMetadataString(item, keys, seen);
|
|
173
|
+
if (found)
|
|
174
|
+
return found;
|
|
175
|
+
}
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
const object = value;
|
|
179
|
+
for (const key of keys) {
|
|
180
|
+
const candidate = object[key];
|
|
181
|
+
if (typeof candidate === "string" && candidate.trim())
|
|
182
|
+
return candidate.trim();
|
|
183
|
+
if (typeof candidate === "number" && Number.isFinite(candidate))
|
|
184
|
+
return String(candidate);
|
|
185
|
+
}
|
|
186
|
+
for (const [key, child] of Object.entries(object)) {
|
|
187
|
+
if (["sources", "source_details", "references", "citations", "citation_sources"].includes(key))
|
|
188
|
+
continue;
|
|
189
|
+
const found = findNamedMetadataString(child, keys, seen);
|
|
190
|
+
if (found)
|
|
191
|
+
return found;
|
|
192
|
+
}
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
export function findLatestHistorySessionId(value) {
|
|
196
|
+
const list = findHistoryList(value);
|
|
197
|
+
if (!list.length)
|
|
198
|
+
return null;
|
|
199
|
+
return findHistoryItemSessionId(list[0]);
|
|
200
|
+
}
|
|
201
|
+
export function findFreshHistorySessionId(lessonResponse, historyResponse) {
|
|
202
|
+
const requestId = findRequestId(lessonResponse);
|
|
203
|
+
if (!requestId)
|
|
204
|
+
return null;
|
|
205
|
+
const list = findHistoryList(historyResponse);
|
|
206
|
+
for (const item of list) {
|
|
207
|
+
if (findRequestId(item) === requestId)
|
|
208
|
+
return findHistoryItemSessionId(item);
|
|
209
|
+
}
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
export function findRequestId(value, seen = new Set()) {
|
|
213
|
+
if (!value || typeof value !== "object" || seen.has(value))
|
|
214
|
+
return null;
|
|
215
|
+
seen.add(value);
|
|
216
|
+
if (Array.isArray(value)) {
|
|
217
|
+
for (const item of value) {
|
|
218
|
+
const found = findRequestId(item, seen);
|
|
219
|
+
if (found)
|
|
220
|
+
return found;
|
|
221
|
+
}
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
const object = value;
|
|
225
|
+
for (const key of ["request_id", "requestId"]) {
|
|
226
|
+
const candidate = object[key];
|
|
227
|
+
if (typeof candidate === "string" && candidate.trim())
|
|
228
|
+
return candidate.trim();
|
|
229
|
+
if (typeof candidate === "number" && Number.isFinite(candidate))
|
|
230
|
+
return String(candidate);
|
|
231
|
+
}
|
|
232
|
+
for (const child of Object.values(object)) {
|
|
233
|
+
const found = findRequestId(child, seen);
|
|
234
|
+
if (found)
|
|
235
|
+
return found;
|
|
236
|
+
}
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
function findHistoryList(value, seen = new Set()) {
|
|
240
|
+
if (!value || typeof value !== "object" || seen.has(value))
|
|
241
|
+
return [];
|
|
242
|
+
seen.add(value);
|
|
243
|
+
if (Array.isArray(value))
|
|
244
|
+
return value;
|
|
245
|
+
const object = value;
|
|
246
|
+
for (const key of ["sessions", "history", "items", "results"]) {
|
|
247
|
+
if (Array.isArray(object[key]))
|
|
248
|
+
return object[key];
|
|
249
|
+
}
|
|
250
|
+
for (const child of Object.values(object)) {
|
|
251
|
+
const found = findHistoryList(child, seen);
|
|
252
|
+
if (found.length)
|
|
253
|
+
return found;
|
|
254
|
+
}
|
|
255
|
+
return [];
|
|
256
|
+
}
|
|
257
|
+
function findHistoryItemSessionId(value) {
|
|
258
|
+
const object = asObject(value);
|
|
259
|
+
for (const key of ["session_id", "sessionId", "id"]) {
|
|
260
|
+
const candidate = object[key];
|
|
261
|
+
if (typeof candidate === "string" && candidate.trim())
|
|
262
|
+
return candidate.trim();
|
|
263
|
+
if (typeof candidate === "number" && Number.isFinite(candidate))
|
|
264
|
+
return String(candidate);
|
|
265
|
+
}
|
|
266
|
+
return findSessionId(value);
|
|
267
|
+
}
|
|
268
|
+
//# sourceMappingURL=api.js.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { findRevisionId, findSessionId, IvorleafApi } from "../api.js";
|
|
2
|
+
import { renderHeader, renderResponse } from "../render.js";
|
|
3
|
+
import { getActiveSession, updateActiveSessionMetadata } from "../utils/activeSession.js";
|
|
4
|
+
export async function actionCommand(actionName, sessionArg, options) {
|
|
5
|
+
const sessionId = sessionArg || await getActiveSession();
|
|
6
|
+
if (!sessionId)
|
|
7
|
+
throw new Error("No active session.\nRun:\nivorleaf open <session_id>\nor\nivorleaf explore <prompt>");
|
|
8
|
+
const data = await (await IvorleafApi.fromConfig()).action(sessionId, actionName);
|
|
9
|
+
const responseSessionId = findSessionId(data) || sessionId;
|
|
10
|
+
await updateActiveSessionMetadata({
|
|
11
|
+
sessionId: responseSessionId,
|
|
12
|
+
revisionId: findRevisionId(data),
|
|
13
|
+
command: `action ${actionName}`,
|
|
14
|
+
});
|
|
15
|
+
if (!options.json)
|
|
16
|
+
await renderHeader(`action ${actionName}`, { sessionId: responseSessionId });
|
|
17
|
+
await renderResponse(data, options, { kind: "workflow", actionName, sessionId: responseSessionId, command: actionName });
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=action.js.map
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { ApiError, IvorleafApi, asObject, findString } from "../api.js";
|
|
2
|
+
import { CONFIG_PATH, loadConfig } from "../config.js";
|
|
3
|
+
import { failure, renderHeader, success } from "../render.js";
|
|
4
|
+
export async function doctorCommand() {
|
|
5
|
+
await renderHeader("doctor");
|
|
6
|
+
console.log("Ivorleaf CLI Doctor\n" + "─".repeat(24));
|
|
7
|
+
const config = await loadConfig();
|
|
8
|
+
console.log(`\nConfiguration\n${config ? success("Config Found") : failure("Config Missing")}`);
|
|
9
|
+
if (!config)
|
|
10
|
+
return suggestion(`Configuration was not found at ${CONFIG_PATH}.`);
|
|
11
|
+
console.log(`\nAuthentication\n${config.apiKey ? success("API Key Found") : failure("API Key Missing")}`);
|
|
12
|
+
if (!config.apiKey)
|
|
13
|
+
return suggestion("The configuration does not contain an API key.");
|
|
14
|
+
const start = performance.now();
|
|
15
|
+
try {
|
|
16
|
+
const status = await new IvorleafApi(config.apiKey).usageStatus();
|
|
17
|
+
const latency = Math.round(performance.now() - start);
|
|
18
|
+
const object = asObject(status);
|
|
19
|
+
const plan = findString(object, ["plan", "plan_name", "tier"]) || "Unknown";
|
|
20
|
+
const organization = findString(object, ["organization", "organization_id", "org_id", "org"]) || "Unknown";
|
|
21
|
+
console.log(success("API Key Valid"));
|
|
22
|
+
console.log(`\nConnection\n${success("API Reachable")}\nLatency: ${latency}ms`);
|
|
23
|
+
console.log(`\nAccount\nPlan: ${plan}\nOrganization: ${organization}`);
|
|
24
|
+
console.log(`\nStatus\n${success("System Healthy")}`);
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
const unauthorized = error instanceof ApiError && [401, 403].includes(error.status || 0);
|
|
28
|
+
console.log(unauthorized ? failure("API Key Invalid") : failure("API Unreachable"));
|
|
29
|
+
console.log(`\nStatus\n${failure("Attention Required")}`);
|
|
30
|
+
if (unauthorized)
|
|
31
|
+
suggestion("Verify the API key is active and has not been revoked.");
|
|
32
|
+
else
|
|
33
|
+
console.log("\nSuggested Action:\nVerify internet connectivity and API availability.");
|
|
34
|
+
process.exitCode = 1;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function suggestion(reason) {
|
|
38
|
+
console.log(`\n${reason}\n\nSuggested Action:\nRun:\nivorleaf init`);
|
|
39
|
+
process.exitCode = 1;
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=doctor.js.map
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { IvorleafApi, findFreshHistorySessionId, findPlan, findRevisionId, findSessionId } from "../api.js";
|
|
2
|
+
import { extractSources, renderHeader, renderResponse } from "../render.js";
|
|
3
|
+
import { clearActiveSession, updateActiveSessionMetadata } from "../utils/activeSession.js";
|
|
4
|
+
import { clearRecentSources, setRecentSources } from "../utils/recentSources.js";
|
|
5
|
+
import { workflowMenu } from "./workflowMenu.js";
|
|
6
|
+
export async function exploreCommand(prompt, options) {
|
|
7
|
+
const api = await IvorleafApi.fromConfig();
|
|
8
|
+
const data = await api.createLesson(prompt);
|
|
9
|
+
let sessionId = findSessionId(data);
|
|
10
|
+
if (!sessionId && !options.json && !options.plain) {
|
|
11
|
+
try {
|
|
12
|
+
sessionId = findFreshHistorySessionId(data, await api.history());
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
sessionId = null;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
if (sessionId)
|
|
19
|
+
await updateActiveSessionMetadata({
|
|
20
|
+
sessionId,
|
|
21
|
+
revisionId: findRevisionId(data),
|
|
22
|
+
command: "explore",
|
|
23
|
+
});
|
|
24
|
+
else
|
|
25
|
+
await clearActiveSession();
|
|
26
|
+
const sources = extractSources(data);
|
|
27
|
+
if (sources.length)
|
|
28
|
+
await setRecentSources(sources);
|
|
29
|
+
else
|
|
30
|
+
await clearRecentSources();
|
|
31
|
+
const plan = findPlan(data);
|
|
32
|
+
if (!options.json)
|
|
33
|
+
await renderHeader("explore", { plan, sessionId });
|
|
34
|
+
await renderResponse(data, options, { kind: "lesson", sessionId, plan, command: "explore" });
|
|
35
|
+
if (sessionId)
|
|
36
|
+
await workflowMenu(sessionId, options);
|
|
37
|
+
else if (!options.json && !options.plain)
|
|
38
|
+
console.log("\nNo session_id returned, so workflow actions are unavailable.");
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=explore.js.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { IvorleafApi, asObject, findString } from "../api.js";
|
|
2
|
+
import { printJson, renderHeader } from "../render.js";
|
|
3
|
+
export async function historyCommand(options) {
|
|
4
|
+
const data = await (await IvorleafApi.fromConfig()).history();
|
|
5
|
+
if (options.json)
|
|
6
|
+
return printJson(data);
|
|
7
|
+
await renderHeader("history");
|
|
8
|
+
const o = asObject(data);
|
|
9
|
+
const list = Array.isArray(data) ? data : [o.sessions, o.history, o.items, asObject(o.data).sessions].find(Array.isArray) || [];
|
|
10
|
+
if (!list.length)
|
|
11
|
+
return console.log("No history found.");
|
|
12
|
+
list.forEach((item, index) => {
|
|
13
|
+
const session = asObject(item);
|
|
14
|
+
const title = findString(session, ["title", "summary", "prompt"]) || "Untitled session";
|
|
15
|
+
const id = findString(session, ["session_id", "sessionId", "id"]) || "Unknown ID";
|
|
16
|
+
const date = findString(session, ["created_at", "updated_at", "date", "timestamp"]);
|
|
17
|
+
console.log(`${index + 1}. ${title}\n ${id}${date ? ` · ${date}` : ""}`);
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=history.js.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { password } from "@inquirer/prompts";
|
|
2
|
+
import { IvorleafApi } from "../api.js";
|
|
3
|
+
import { CONFIG_PATH, saveConfig } from "../config.js";
|
|
4
|
+
import { renderHeader, success } from "../render.js";
|
|
5
|
+
export async function initCommand() {
|
|
6
|
+
await renderHeader("init");
|
|
7
|
+
const apiKey = (await password({ message: "Ivorleaf API key:", mask: "*" })).trim();
|
|
8
|
+
if (!apiKey)
|
|
9
|
+
throw new Error("An API key is required. No configuration was saved.");
|
|
10
|
+
process.stdout.write("Validating API key… ");
|
|
11
|
+
await new IvorleafApi(apiKey).usageStatus();
|
|
12
|
+
console.log(success("valid"));
|
|
13
|
+
await saveConfig({ apiKey });
|
|
14
|
+
console.log(`${success("Configuration saved")}\n${CONFIG_PATH}`);
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=init.js.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { IvorleafApi, asObject, findString } from "../api.js";
|
|
2
|
+
import { printJson, renderHeader } from "../render.js";
|
|
3
|
+
export async function limitsCommand(options) {
|
|
4
|
+
const data = await (await IvorleafApi.fromConfig()).usageStatus();
|
|
5
|
+
if (options.json)
|
|
6
|
+
return printJson(data);
|
|
7
|
+
await renderHeader("limits", findString(data, ["plan", "plan_name", "tier"]));
|
|
8
|
+
const o = asObject(data);
|
|
9
|
+
const usage = asObject(o.usage ?? o.data ?? o);
|
|
10
|
+
console.log(`Plan: ${findString(data, ["plan", "plan_name", "tier"]) || "Unknown"}`);
|
|
11
|
+
console.log(formatLimit("Sessions", usage, ["sessions_used", "session_count"], ["sessions_limit", "session_limit"]));
|
|
12
|
+
const workflowLine = formatLimit("Workflows", usage, ["workflows_used", "workflow_count"], ["workflows_limit", "workflow_limit"]);
|
|
13
|
+
if (!workflowLine.endsWith("Unavailable"))
|
|
14
|
+
console.log(workflowLine);
|
|
15
|
+
}
|
|
16
|
+
function formatLimit(label, o, usedKeys, limitKeys) {
|
|
17
|
+
const get = (keys) => keys.map(k => o[k]).find(v => v !== undefined);
|
|
18
|
+
const used = get(usedKeys), limit = get(limitKeys);
|
|
19
|
+
return `${label}: ${used ?? "Unavailable"}${used !== undefined ? ` / ${limit ?? "Unlimited"}` : ""}`;
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=limits.js.map
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { IvorleafApi, findPlan, findRevisionId, findSessionId } from "../api.js";
|
|
2
|
+
import { extractSources, renderHeader, renderResponse } from "../render.js";
|
|
3
|
+
import { updateActiveSessionMetadata } from "../utils/activeSession.js";
|
|
4
|
+
import { clearRecentSources, setRecentSources } from "../utils/recentSources.js";
|
|
5
|
+
import { workflowMenu } from "./workflowMenu.js";
|
|
6
|
+
export async function openCommand(sessionId, options) {
|
|
7
|
+
const data = await (await IvorleafApi.fromConfig()).getSession(sessionId);
|
|
8
|
+
const responseSessionId = findSessionId(data) || sessionId;
|
|
9
|
+
await updateActiveSessionMetadata({
|
|
10
|
+
sessionId: responseSessionId,
|
|
11
|
+
revisionId: findRevisionId(data),
|
|
12
|
+
command: "open",
|
|
13
|
+
});
|
|
14
|
+
const sources = extractSources(data);
|
|
15
|
+
if (sources.length)
|
|
16
|
+
await setRecentSources(sources);
|
|
17
|
+
else
|
|
18
|
+
await clearRecentSources();
|
|
19
|
+
const plan = findPlan(data);
|
|
20
|
+
if (!options.json)
|
|
21
|
+
await renderHeader("open", { plan, sessionId: responseSessionId });
|
|
22
|
+
await renderResponse(data, options, { kind: "lesson", sessionId: responseSessionId, plan, command: "open" });
|
|
23
|
+
await workflowMenu(responseSessionId, options);
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=open.js.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { findRevisionId, findSessionId, IvorleafApi } from "../api.js";
|
|
2
|
+
import { renderHeader, renderResponse } from "../render.js";
|
|
3
|
+
import { getActiveSession, updateActiveSessionMetadata } from "../utils/activeSession.js";
|
|
4
|
+
export async function orchestrateCommand(intent, sessionArg, options) {
|
|
5
|
+
const sessionId = sessionArg || await getActiveSession();
|
|
6
|
+
if (!sessionId)
|
|
7
|
+
throw new Error("No active session.\nRun:\nivorleaf open <session_id>\nor\nivorleaf explore <prompt>");
|
|
8
|
+
const data = await (await IvorleafApi.fromConfig()).orchestrate(sessionId, intent);
|
|
9
|
+
const responseSessionId = findSessionId(data) || sessionId;
|
|
10
|
+
await updateActiveSessionMetadata({
|
|
11
|
+
sessionId: responseSessionId,
|
|
12
|
+
revisionId: findRevisionId(data),
|
|
13
|
+
command: intent,
|
|
14
|
+
});
|
|
15
|
+
if (!options.json)
|
|
16
|
+
await renderHeader(intent, { sessionId: responseSessionId });
|
|
17
|
+
await renderResponse(data, options, { kind: "workflow", actionName: intent, sessionId: responseSessionId, command: intent });
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=orchestrate.js.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { formatSources, renderHeader } from "../render.js";
|
|
2
|
+
import { getRecentSources } from "../utils/recentSources.js";
|
|
3
|
+
export async function sourcesCommand(options) {
|
|
4
|
+
const sources = await getRecentSources();
|
|
5
|
+
if (options.json) {
|
|
6
|
+
console.log(JSON.stringify({ sources }, null, 2));
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
await renderHeader("sources");
|
|
10
|
+
if (!sources.length) {
|
|
11
|
+
console.log("No sources are available yet. Run an explore command first.");
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
console.log(formatSources(sources));
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=sources.js.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { renderHeader } from "../render.js";
|
|
2
|
+
import { getActiveSession, getActiveSessionMetadata } from "../utils/activeSession.js";
|
|
3
|
+
export async function statusCommand() {
|
|
4
|
+
const sessionId = await getActiveSession();
|
|
5
|
+
const metadata = await getActiveSessionMetadata();
|
|
6
|
+
await renderHeader("status", { sessionId });
|
|
7
|
+
if (!sessionId) {
|
|
8
|
+
console.log("No active session.");
|
|
9
|
+
console.log("Run: ivorleaf explore <prompt>");
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
console.log(`Active session: ${sessionId}`);
|
|
13
|
+
if (metadata?.revisionId)
|
|
14
|
+
console.log(`Latest revision: ${metadata.revisionId}`);
|
|
15
|
+
if (metadata?.command)
|
|
16
|
+
console.log(`Last command: ${metadata.command}`);
|
|
17
|
+
if (metadata?.updatedAt)
|
|
18
|
+
console.log(`Updated: ${metadata.updatedAt}`);
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=status.js.map
|