iris-chatbot 0.2.4

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.
Files changed (66) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +49 -0
  3. package/bin/iris.mjs +267 -0
  4. package/package.json +61 -0
  5. package/template/LICENSE +21 -0
  6. package/template/README.md +49 -0
  7. package/template/eslint.config.mjs +18 -0
  8. package/template/next.config.ts +7 -0
  9. package/template/package-lock.json +9193 -0
  10. package/template/package.json +46 -0
  11. package/template/postcss.config.mjs +7 -0
  12. package/template/public/file.svg +1 -0
  13. package/template/public/globe.svg +1 -0
  14. package/template/public/next.svg +1 -0
  15. package/template/public/vercel.svg +1 -0
  16. package/template/public/window.svg +1 -0
  17. package/template/src/app/api/chat/route.ts +2445 -0
  18. package/template/src/app/api/connections/models/route.ts +255 -0
  19. package/template/src/app/api/connections/test/route.ts +124 -0
  20. package/template/src/app/api/local-sync/route.ts +74 -0
  21. package/template/src/app/api/tool-approval/route.ts +47 -0
  22. package/template/src/app/favicon.ico +0 -0
  23. package/template/src/app/globals.css +808 -0
  24. package/template/src/app/layout.tsx +74 -0
  25. package/template/src/app/page.tsx +444 -0
  26. package/template/src/components/ChatView.tsx +1537 -0
  27. package/template/src/components/Composer.tsx +160 -0
  28. package/template/src/components/MapView.tsx +244 -0
  29. package/template/src/components/MessageCard.tsx +955 -0
  30. package/template/src/components/SearchModal.tsx +72 -0
  31. package/template/src/components/SettingsModal.tsx +1257 -0
  32. package/template/src/components/Sidebar.tsx +153 -0
  33. package/template/src/components/TopBar.tsx +164 -0
  34. package/template/src/lib/connections.ts +275 -0
  35. package/template/src/lib/data.ts +324 -0
  36. package/template/src/lib/db.ts +49 -0
  37. package/template/src/lib/hooks.ts +76 -0
  38. package/template/src/lib/local-sync.ts +192 -0
  39. package/template/src/lib/memory.ts +695 -0
  40. package/template/src/lib/model-presets.ts +251 -0
  41. package/template/src/lib/store.ts +36 -0
  42. package/template/src/lib/tooling/approvals.ts +78 -0
  43. package/template/src/lib/tooling/providers/anthropic.ts +155 -0
  44. package/template/src/lib/tooling/providers/ollama.ts +73 -0
  45. package/template/src/lib/tooling/providers/openai.ts +267 -0
  46. package/template/src/lib/tooling/providers/openai_compatible.ts +16 -0
  47. package/template/src/lib/tooling/providers/types.ts +44 -0
  48. package/template/src/lib/tooling/registry.ts +103 -0
  49. package/template/src/lib/tooling/runtime.ts +189 -0
  50. package/template/src/lib/tooling/safety.ts +165 -0
  51. package/template/src/lib/tooling/tools/apps.ts +108 -0
  52. package/template/src/lib/tooling/tools/apps_plus.ts +153 -0
  53. package/template/src/lib/tooling/tools/communication.ts +883 -0
  54. package/template/src/lib/tooling/tools/files.ts +395 -0
  55. package/template/src/lib/tooling/tools/music.ts +988 -0
  56. package/template/src/lib/tooling/tools/notes.ts +461 -0
  57. package/template/src/lib/tooling/tools/notes_plus.ts +294 -0
  58. package/template/src/lib/tooling/tools/numbers.ts +175 -0
  59. package/template/src/lib/tooling/tools/schedule.ts +579 -0
  60. package/template/src/lib/tooling/tools/system.ts +142 -0
  61. package/template/src/lib/tooling/tools/web.ts +212 -0
  62. package/template/src/lib/tooling/tools/workflow.ts +218 -0
  63. package/template/src/lib/tooling/types.ts +27 -0
  64. package/template/src/lib/types.ts +309 -0
  65. package/template/src/lib/utils.ts +108 -0
  66. package/template/tsconfig.json +34 -0
@@ -0,0 +1,255 @@
1
+ import OpenAI from "openai";
2
+ import type { ChatConnectionPayload } from "../../../../lib/types";
3
+ import { GEMINI_OPENAI_BASE_URL } from "../../../../lib/connections";
4
+ import { getBuiltinModelPresets } from "../../../../lib/model-presets";
5
+
6
+ export const runtime = "nodejs";
7
+ export const dynamic = "force-dynamic";
8
+
9
+ type ModelsRequest = {
10
+ connection?: ChatConnectionPayload;
11
+ };
12
+
13
+ type AnthropicModelItem = {
14
+ id?: string;
15
+ type?: string;
16
+ };
17
+
18
+ type AnthropicModelsResponse = {
19
+ data?: AnthropicModelItem[];
20
+ has_more?: boolean;
21
+ last_id?: string;
22
+ };
23
+
24
+ type GoogleModelItem = {
25
+ name?: string;
26
+ supportedGenerationMethods?: string[];
27
+ };
28
+
29
+ type GoogleModelsResponse = {
30
+ models?: GoogleModelItem[];
31
+ nextPageToken?: string;
32
+ };
33
+
34
+ function normalizeConnection(connection?: ChatConnectionPayload) {
35
+ if (!connection) {
36
+ return null;
37
+ }
38
+ const kind =
39
+ connection.kind === "builtin" || connection.kind === "openai_compatible" || connection.kind === "ollama"
40
+ ? connection.kind
41
+ : null;
42
+ if (!kind) {
43
+ return null;
44
+ }
45
+ const provider =
46
+ connection.provider === "openai" ||
47
+ connection.provider === "anthropic" ||
48
+ connection.provider === "google"
49
+ ? connection.provider
50
+ : undefined;
51
+ const headers = Array.isArray(connection.headers)
52
+ ? Object.fromEntries(
53
+ connection.headers
54
+ .filter((item) => item && typeof item.key === "string" && item.key.trim())
55
+ .map((item) => [item.key.trim(), item.value ?? ""]),
56
+ )
57
+ : undefined;
58
+ return {
59
+ kind,
60
+ provider,
61
+ baseUrl:
62
+ typeof connection.baseUrl === "string" && connection.baseUrl.trim()
63
+ ? connection.baseUrl.trim()
64
+ : kind === "builtin" && provider === "google"
65
+ ? GEMINI_OPENAI_BASE_URL
66
+ : kind === "ollama"
67
+ ? "http://localhost:11434"
68
+ : undefined,
69
+ apiKey: typeof connection.apiKey === "string" && connection.apiKey.trim() ? connection.apiKey.trim() : undefined,
70
+ headers,
71
+ };
72
+ }
73
+
74
+ function dedupeSorted(models: string[]): string[] {
75
+ return [...new Set(models.filter(Boolean))].sort((a, b) => a.localeCompare(b));
76
+ }
77
+
78
+ async function listAnthropicModels(apiKey: string): Promise<string[]> {
79
+ const collected: string[] = [];
80
+ let afterId: string | null = null;
81
+
82
+ for (let page = 0; page < 10; page += 1) {
83
+ const url = new URL("https://api.anthropic.com/v1/models");
84
+ url.searchParams.set("limit", "100");
85
+ if (afterId) {
86
+ url.searchParams.set("after_id", afterId);
87
+ }
88
+
89
+ const response = await fetch(url.toString(), {
90
+ headers: {
91
+ "anthropic-version": "2023-06-01",
92
+ "x-api-key": apiKey,
93
+ },
94
+ });
95
+
96
+ if (!response.ok) {
97
+ const text = await response.text().catch(() => "");
98
+ throw new Error(text || `Anthropic models request failed (${response.status})`);
99
+ }
100
+
101
+ const payload = (await response.json()) as AnthropicModelsResponse;
102
+ const ids = Array.isArray(payload.data)
103
+ ? payload.data
104
+ .map((item) => item.id)
105
+ .filter((id): id is string => typeof id === "string" && id.trim().length > 0)
106
+ : [];
107
+ collected.push(...ids);
108
+
109
+ if (!payload.has_more || !payload.last_id) {
110
+ break;
111
+ }
112
+ afterId = payload.last_id;
113
+ }
114
+
115
+ return dedupeSorted(collected);
116
+ }
117
+
118
+ async function listGoogleModels(apiKey: string): Promise<string[]> {
119
+ const collected: string[] = [];
120
+ let pageToken = "";
121
+
122
+ for (let page = 0; page < 10; page += 1) {
123
+ const url = new URL("https://generativelanguage.googleapis.com/v1beta/models");
124
+ url.searchParams.set("key", apiKey);
125
+ url.searchParams.set("pageSize", "100");
126
+ if (pageToken) {
127
+ url.searchParams.set("pageToken", pageToken);
128
+ }
129
+
130
+ const response = await fetch(url.toString());
131
+ if (!response.ok) {
132
+ const text = await response.text().catch(() => "");
133
+ throw new Error(text || `Google models request failed (${response.status})`);
134
+ }
135
+
136
+ const payload = (await response.json()) as GoogleModelsResponse;
137
+ const ids = Array.isArray(payload.models)
138
+ ? payload.models
139
+ .filter((item) =>
140
+ Array.isArray(item.supportedGenerationMethods)
141
+ ? item.supportedGenerationMethods.includes("generateContent")
142
+ : true,
143
+ )
144
+ .map((item) => (typeof item.name === "string" ? item.name.replace(/^models\//, "").trim() : ""))
145
+ .filter((id): id is string => id.length > 0)
146
+ : [];
147
+ collected.push(...ids);
148
+
149
+ if (!payload.nextPageToken) {
150
+ break;
151
+ }
152
+ pageToken = payload.nextPageToken;
153
+ }
154
+
155
+ return dedupeSorted(collected);
156
+ }
157
+
158
+ export async function POST(request: Request) {
159
+ try {
160
+ const body = (await request.json()) as ModelsRequest;
161
+ const connection = normalizeConnection(body.connection);
162
+ if (!connection) {
163
+ return new Response(JSON.stringify({ ok: false, error: "Missing connection." }), {
164
+ status: 400,
165
+ headers: { "Content-Type": "application/json" },
166
+ });
167
+ }
168
+
169
+ if (connection.kind === "ollama") {
170
+ const baseUrl = connection.baseUrl ?? "http://localhost:11434";
171
+ const response = await fetch(`${baseUrl.replace(/\/$/, "")}/api/tags`);
172
+ if (!response.ok) {
173
+ const text = await response.text().catch(() => "");
174
+ throw new Error(text || `Ollama models request failed (${response.status})`);
175
+ }
176
+ const payload = (await response.json()) as { models?: Array<{ name?: string }> };
177
+ const models = Array.isArray(payload.models)
178
+ ? payload.models.map((model) => model.name).filter((name): name is string => Boolean(name))
179
+ : [];
180
+ return new Response(JSON.stringify({ ok: true, models, source: "ollama" }), {
181
+ headers: { "Content-Type": "application/json" },
182
+ });
183
+ }
184
+
185
+ if (connection.kind === "builtin" && connection.provider === "anthropic") {
186
+ if (connection.apiKey) {
187
+ const models = await listAnthropicModels(connection.apiKey);
188
+ if (models.length > 0) {
189
+ return new Response(JSON.stringify({ ok: true, models, source: "anthropic-api" }), {
190
+ headers: { "Content-Type": "application/json" },
191
+ });
192
+ }
193
+ }
194
+ return new Response(
195
+ JSON.stringify({ ok: true, models: getBuiltinModelPresets("anthropic"), source: "builtin-fallback" }),
196
+ { headers: { "Content-Type": "application/json" } },
197
+ );
198
+ }
199
+
200
+ if (connection.kind === "builtin" && connection.provider === "google") {
201
+ if (connection.apiKey) {
202
+ const models = await listGoogleModels(connection.apiKey);
203
+ if (models.length > 0) {
204
+ return new Response(JSON.stringify({ ok: true, models, source: "google-api" }), {
205
+ headers: { "Content-Type": "application/json" },
206
+ });
207
+ }
208
+ }
209
+ return new Response(
210
+ JSON.stringify({ ok: true, models: getBuiltinModelPresets("google"), source: "builtin-fallback" }),
211
+ { headers: { "Content-Type": "application/json" } },
212
+ );
213
+ }
214
+
215
+ const client = new OpenAI({
216
+ apiKey: connection.apiKey ?? "no-key-required",
217
+ baseURL: connection.baseUrl,
218
+ defaultHeaders: connection.headers,
219
+ });
220
+
221
+ const listed = await client.models.list();
222
+ const models = dedupeSorted(
223
+ listed.data
224
+ .map((item) => item.id)
225
+ .filter((value): value is string => typeof value === "string" && value.length > 0),
226
+ );
227
+
228
+ if (models.length > 0) {
229
+ return new Response(JSON.stringify({ ok: true, models, source: "remote" }), {
230
+ headers: { "Content-Type": "application/json" },
231
+ });
232
+ }
233
+
234
+ if (connection.kind === "builtin" && connection.provider) {
235
+ return new Response(
236
+ JSON.stringify({
237
+ ok: true,
238
+ models: getBuiltinModelPresets(connection.provider),
239
+ source: "builtin-fallback",
240
+ }),
241
+ { headers: { "Content-Type": "application/json" } },
242
+ );
243
+ }
244
+
245
+ return new Response(JSON.stringify({ ok: true, models: [], source: "empty" }), {
246
+ headers: { "Content-Type": "application/json" },
247
+ });
248
+ } catch (error) {
249
+ const message = error instanceof Error ? error.message : "Unexpected error.";
250
+ return new Response(JSON.stringify({ ok: false, error: message }), {
251
+ status: 500,
252
+ headers: { "Content-Type": "application/json" },
253
+ });
254
+ }
255
+ }
@@ -0,0 +1,124 @@
1
+ import OpenAI from "openai";
2
+ import Anthropic from "@anthropic-ai/sdk";
3
+ import type { ChatConnectionPayload } from "../../../../lib/types";
4
+ import { GEMINI_OPENAI_BASE_URL } from "../../../../lib/connections";
5
+
6
+ export const runtime = "nodejs";
7
+ export const dynamic = "force-dynamic";
8
+
9
+ type TestRequest = {
10
+ connection?: ChatConnectionPayload;
11
+ model?: string;
12
+ };
13
+
14
+ function normalizeConnection(connection?: ChatConnectionPayload) {
15
+ if (!connection) {
16
+ return null;
17
+ }
18
+ const kind =
19
+ connection.kind === "builtin" || connection.kind === "openai_compatible" || connection.kind === "ollama"
20
+ ? connection.kind
21
+ : null;
22
+ if (!kind) {
23
+ return null;
24
+ }
25
+ const provider =
26
+ connection.provider === "openai" ||
27
+ connection.provider === "anthropic" ||
28
+ connection.provider === "google"
29
+ ? connection.provider
30
+ : undefined;
31
+ const headers = Array.isArray(connection.headers)
32
+ ? Object.fromEntries(
33
+ connection.headers
34
+ .filter((item) => item && typeof item.key === "string" && item.key.trim())
35
+ .map((item) => [item.key.trim(), item.value ?? ""]),
36
+ )
37
+ : undefined;
38
+ return {
39
+ id: connection.id || "ad-hoc",
40
+ name: connection.name || "Connection",
41
+ kind,
42
+ provider,
43
+ baseUrl:
44
+ typeof connection.baseUrl === "string" && connection.baseUrl.trim()
45
+ ? connection.baseUrl.trim()
46
+ : kind === "builtin" && provider === "google"
47
+ ? GEMINI_OPENAI_BASE_URL
48
+ : kind === "ollama"
49
+ ? "http://localhost:11434"
50
+ : undefined,
51
+ apiKey: typeof connection.apiKey === "string" && connection.apiKey.trim() ? connection.apiKey.trim() : undefined,
52
+ headers,
53
+ };
54
+ }
55
+
56
+ export async function POST(request: Request) {
57
+ try {
58
+ const body = (await request.json()) as TestRequest;
59
+ const connection = normalizeConnection(body.connection);
60
+ if (!connection) {
61
+ return new Response(JSON.stringify({ ok: false, error: "Missing connection." }), {
62
+ status: 400,
63
+ headers: { "Content-Type": "application/json" },
64
+ });
65
+ }
66
+
67
+ if (connection.kind === "ollama") {
68
+ const baseUrl = connection.baseUrl ?? "http://localhost:11434";
69
+ const response = await fetch(`${baseUrl.replace(/\/$/, "")}/api/tags`);
70
+ if (!response.ok) {
71
+ const text = await response.text().catch(() => "");
72
+ throw new Error(text || `Ollama test failed (${response.status})`);
73
+ }
74
+ return new Response(
75
+ JSON.stringify({ ok: true, provider: "ollama", message: "Connected to Ollama." }),
76
+ { headers: { "Content-Type": "application/json" } },
77
+ );
78
+ }
79
+
80
+ if (connection.kind === "builtin" && connection.provider === "anthropic") {
81
+ if (!connection.apiKey) {
82
+ throw new Error("Missing Anthropic API key.");
83
+ }
84
+ const client = new Anthropic({ apiKey: connection.apiKey });
85
+ await client.messages.create({
86
+ model: "claude-haiku-4-5",
87
+ max_tokens: 8,
88
+ messages: [{ role: "user", content: "ping" }],
89
+ });
90
+ return new Response(
91
+ JSON.stringify({ ok: true, provider: "anthropic", message: "Connected to Anthropic." }),
92
+ { headers: { "Content-Type": "application/json" } },
93
+ );
94
+ }
95
+
96
+ const client = new OpenAI({
97
+ apiKey: connection.apiKey ?? "no-key-required",
98
+ baseURL: connection.baseUrl,
99
+ defaultHeaders: connection.headers,
100
+ });
101
+ const pingModel =
102
+ typeof body.model === "string" && body.model.trim()
103
+ ? body.model.trim()
104
+ : connection.kind === "builtin" && connection.provider === "google"
105
+ ? "gemini-2.5-flash"
106
+ : "gpt-4o-mini";
107
+ await client.responses.create({
108
+ model: pingModel,
109
+ input: [{ role: "user", content: "ping" }],
110
+ max_output_tokens: 8,
111
+ });
112
+ return new Response(
113
+ JSON.stringify({ ok: true, provider: connection.provider ?? connection.kind, message: "Connection test succeeded." }),
114
+ { headers: { "Content-Type": "application/json" } },
115
+ );
116
+ } catch (error) {
117
+ const message = error instanceof Error ? error.message : "Unexpected error.";
118
+ return new Response(JSON.stringify({ ok: false, error: message }), {
119
+ status: 500,
120
+ headers: { "Content-Type": "application/json" },
121
+ });
122
+ }
123
+ }
124
+
@@ -0,0 +1,74 @@
1
+ import { NextRequest } from "next/server";
2
+ import { promises as fs } from "fs";
3
+ import path from "path";
4
+
5
+ export const runtime = "nodejs";
6
+ export const dynamic = "force-dynamic";
7
+
8
+ const DATA_DIR = ".zenith-data";
9
+ const STATE_FILE = "state.json";
10
+
11
+ function getStatePath(): string {
12
+ return path.join(process.cwd(), DATA_DIR, STATE_FILE);
13
+ }
14
+
15
+ function getDataDir(): string {
16
+ return path.join(process.cwd(), DATA_DIR);
17
+ }
18
+
19
+ const EMPTY_PAYLOAD = {
20
+ threads: [] as unknown[],
21
+ messages: [] as unknown[],
22
+ settings: [] as unknown[],
23
+ toolEvents: [] as unknown[],
24
+ toolApprovals: [] as unknown[],
25
+ memories: [] as unknown[],
26
+ };
27
+
28
+ export async function GET() {
29
+ const statePath = getStatePath();
30
+ try {
31
+ const raw = await fs.readFile(statePath, "utf-8");
32
+ const data = JSON.parse(raw);
33
+ const payload = {
34
+ threads: Array.isArray(data.threads) ? data.threads : EMPTY_PAYLOAD.threads,
35
+ messages: Array.isArray(data.messages) ? data.messages : EMPTY_PAYLOAD.messages,
36
+ settings: Array.isArray(data.settings) ? data.settings : EMPTY_PAYLOAD.settings,
37
+ toolEvents: Array.isArray(data.toolEvents) ? data.toolEvents : EMPTY_PAYLOAD.toolEvents,
38
+ toolApprovals: Array.isArray(data.toolApprovals) ? data.toolApprovals : EMPTY_PAYLOAD.toolApprovals,
39
+ memories: Array.isArray(data.memories) ? data.memories : EMPTY_PAYLOAD.memories,
40
+ };
41
+ return Response.json(payload);
42
+ } catch (err) {
43
+ const code = err && typeof err === "object" && "code" in err ? (err as NodeJS.ErrnoException).code : null;
44
+ if (code === "ENOENT") {
45
+ return Response.json(EMPTY_PAYLOAD);
46
+ }
47
+ throw err;
48
+ }
49
+ }
50
+
51
+ export async function POST(request: NextRequest) {
52
+ const statePath = getStatePath();
53
+ const dir = getDataDir();
54
+ let body: unknown;
55
+ try {
56
+ body = await request.json();
57
+ } catch {
58
+ return new Response("Invalid JSON", { status: 400 });
59
+ }
60
+ const data = (typeof body === "object" && body !== null ? body : {}) as Record<string, unknown>;
61
+ const payload = {
62
+ threads: Array.isArray(data.threads) ? data.threads : EMPTY_PAYLOAD.threads,
63
+ messages: Array.isArray(data.messages) ? data.messages : EMPTY_PAYLOAD.messages,
64
+ settings: Array.isArray(data.settings) ? data.settings : EMPTY_PAYLOAD.settings,
65
+ toolEvents: Array.isArray(data.toolEvents) ? data.toolEvents : EMPTY_PAYLOAD.toolEvents,
66
+ toolApprovals: Array.isArray(data.toolApprovals) ? data.toolApprovals : EMPTY_PAYLOAD.toolApprovals,
67
+ memories: Array.isArray(data.memories) ? data.memories : EMPTY_PAYLOAD.memories,
68
+ };
69
+ await fs.mkdir(dir, { recursive: true });
70
+ const tmpPath = path.join(dir, `${STATE_FILE}.${Date.now()}.tmp`);
71
+ await fs.writeFile(tmpPath, JSON.stringify(payload, null, 2), "utf-8");
72
+ await fs.rename(tmpPath, statePath);
73
+ return new Response(null, { status: 200 });
74
+ }
@@ -0,0 +1,47 @@
1
+ import { resolveApprovalDecision } from "../../../lib/tooling/approvals";
2
+
3
+ export const runtime = "nodejs";
4
+ export const dynamic = "force-dynamic";
5
+
6
+ export async function POST(request: Request) {
7
+ try {
8
+ const body = (await request.json()) as {
9
+ approvalId?: unknown;
10
+ decision?: unknown;
11
+ };
12
+
13
+ if (typeof body.approvalId !== "string" || !body.approvalId.trim()) {
14
+ return new Response(JSON.stringify({ ok: false, error: "Missing approvalId." }), {
15
+ status: 400,
16
+ headers: { "Content-Type": "application/json" },
17
+ });
18
+ }
19
+
20
+ if (body.decision !== "approve" && body.decision !== "deny") {
21
+ return new Response(JSON.stringify({ ok: false, error: "Invalid decision." }), {
22
+ status: 400,
23
+ headers: { "Content-Type": "application/json" },
24
+ });
25
+ }
26
+
27
+ const approvalId = body.approvalId.trim();
28
+
29
+ const resolved = resolveApprovalDecision(approvalId, body.decision);
30
+ if (!resolved) {
31
+ return new Response(JSON.stringify({ ok: false, error: "Unable to resolve approval." }), {
32
+ status: 404,
33
+ headers: { "Content-Type": "application/json" },
34
+ });
35
+ }
36
+
37
+ return new Response(JSON.stringify({ ok: true }), {
38
+ headers: { "Content-Type": "application/json" },
39
+ });
40
+ } catch (error) {
41
+ const message = error instanceof Error ? error.message : "Unexpected error.";
42
+ return new Response(JSON.stringify({ ok: false, error: message }), {
43
+ status: 500,
44
+ headers: { "Content-Type": "application/json" },
45
+ });
46
+ }
47
+ }
Binary file