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 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,7 @@
1
+ import { success } from "../render.js";
2
+ import { clearActiveSession } from "../utils/activeSession.js";
3
+ export async function clearCommand() {
4
+ await clearActiveSession();
5
+ console.log(success("Active session cleared"));
6
+ }
7
+ //# sourceMappingURL=clear.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