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,461 @@
1
+ import { ensureMacOS } from "../safety";
2
+ import { runCommandSafe } from "../runtime";
3
+ import type { ToolDefinition, ToolExecutionContext } from "../types";
4
+
5
+ type NotesInput = {
6
+ title?: string;
7
+ body?: string;
8
+ folder?: string;
9
+ mode?: "create" | "append";
10
+ query?: string;
11
+ limit?: number;
12
+ };
13
+
14
+ const APPLESCRIPT_TIMEOUT_MS = 30_000;
15
+
16
+ function normalizeHeadingLikeText(input: string): string {
17
+ return input
18
+ .trim()
19
+ .replace(/^#{1,6}\s+/, "")
20
+ .replace(/^\*\*(.+)\*\*$/u, "$1")
21
+ .replace(/^__(.+)__$/u, "$1")
22
+ .replace(/\s+/g, " ")
23
+ .toLowerCase();
24
+ }
25
+
26
+ function escapeHtml(input: string): string {
27
+ return input
28
+ .replace(/&/g, "&")
29
+ .replace(/</g, "&lt;")
30
+ .replace(/>/g, "&gt;")
31
+ .replace(/"/g, "&quot;")
32
+ .replace(/'/g, "&#39;");
33
+ }
34
+
35
+ function formatInlineMarkdown(input: string): string {
36
+ const escaped = escapeHtml(input);
37
+ return escaped
38
+ .replace(/\*\*(.+?)\*\*/g, "<b>$1</b>")
39
+ .replace(/__(.+?)__/g, "<b>$1</b>")
40
+ .replace(/\*(.+?)\*/g, "<i>$1</i>")
41
+ .replace(/_(.+?)_/g, "<i>$1</i>")
42
+ .replace(/`([^`]+)`/g, "<code>$1</code>");
43
+ }
44
+
45
+ function paragraphHtml(content: string): string {
46
+ return `<p>${content}</p>`;
47
+ }
48
+
49
+ function headingHtml(level: number, content: string): string {
50
+ return `<h${level}>${content}</h${level}>`;
51
+ }
52
+
53
+ function listHtml(type: "ul" | "ol", items: string[]): string {
54
+ const renderedItems = items.map((item) => `<li>${formatInlineMarkdown(item)}</li>`).join("");
55
+ return `<${type}>${renderedItems}</${type}>`;
56
+ }
57
+
58
+ type NotesBlockKind = "heading" | "paragraph" | "list" | "table";
59
+
60
+ type NotesBlock = {
61
+ kind: NotesBlockKind;
62
+ html: string;
63
+ };
64
+
65
+ function renderBlocksWithSpacing(blocks: NotesBlock[]): string {
66
+ if (blocks.length === 0) {
67
+ return "";
68
+ }
69
+
70
+ const output: string[] = [];
71
+ for (let index = 0; index < blocks.length; index += 1) {
72
+ const current = blocks[index];
73
+ const previous = index > 0 ? blocks[index - 1] : null;
74
+
75
+ if (previous) {
76
+ const needsSectionGap =
77
+ current.kind === "heading" ||
78
+ previous.kind === "heading" ||
79
+ previous.kind === "list" ||
80
+ previous.kind === "table";
81
+
82
+ if (needsSectionGap) {
83
+ // Apple Notes reliably keeps explicit line breaks.
84
+ output.push("<div><br/></div>");
85
+ }
86
+ }
87
+
88
+ output.push(current.html);
89
+ }
90
+
91
+ return output.join("");
92
+ }
93
+
94
+ function splitTableRow(line: string): string[] {
95
+ const trimmed = line.trim().replace(/^\|/, "").replace(/\|$/, "");
96
+ return trimmed.split("|").map((cell) => cell.trim());
97
+ }
98
+
99
+ function isMarkdownTableSeparator(line: string): boolean {
100
+ const trimmed = line.trim();
101
+ if (!trimmed.includes("|")) {
102
+ return false;
103
+ }
104
+ const cells = splitTableRow(trimmed);
105
+ if (cells.length === 0) {
106
+ return false;
107
+ }
108
+ return cells.every((cell) => /^:?-{3,}:?$/.test(cell));
109
+ }
110
+
111
+ function toMarkdownTable(lines: string[]): string | null {
112
+ if (lines.length < 2) {
113
+ return null;
114
+ }
115
+ if (!lines[0].includes("|") || !isMarkdownTableSeparator(lines[1])) {
116
+ return null;
117
+ }
118
+
119
+ const headerCells = splitTableRow(lines[0]);
120
+ const bodyRows = lines.slice(2).filter((line) => line.includes("|"));
121
+ if (headerCells.length === 0 || bodyRows.length === 0) {
122
+ return null;
123
+ }
124
+
125
+ const thead = `<thead><tr>${headerCells
126
+ .map((cell) => `<th>${formatInlineMarkdown(cell)}</th>`)
127
+ .join("")}</tr></thead>`;
128
+ const tbody = `<tbody>${bodyRows
129
+ .map((row) => {
130
+ const cells = splitTableRow(row);
131
+ return `<tr>${cells.map((cell) => `<td>${formatInlineMarkdown(cell)}</td>`).join("")}</tr>`;
132
+ })
133
+ .join("")}</tbody>`;
134
+
135
+ return `<table>${thead}${tbody}</table>`;
136
+ }
137
+
138
+ export function normalizeNotesBody(rawBody: string): string {
139
+ let source = rawBody.replace(/\r\n/g, "\n").trim();
140
+ if (!source) {
141
+ return "";
142
+ }
143
+
144
+ // Heuristic for single-line "dash separated" dumps.
145
+ if (!source.includes("\n") && source.length > 240 && source.includes(" - ")) {
146
+ source = source.replace(/\s-\s/g, "\n- ");
147
+ }
148
+
149
+ // Heuristic for collapsed markdown table rows emitted on one line.
150
+ if (source.includes("|") && source.includes("||")) {
151
+ source = source
152
+ .replace(/\s*\|\|\s*/g, "\n")
153
+ .replace(/\n{3,}/g, "\n\n");
154
+ }
155
+
156
+ const lines = source.split("\n");
157
+ const blocks: NotesBlock[] = [];
158
+ let paragraphLines: string[] = [];
159
+ let listType: "ul" | "ol" | null = null;
160
+ let listItems: string[] = [];
161
+
162
+ const flushParagraph = () => {
163
+ if (paragraphLines.length === 0) {
164
+ return;
165
+ }
166
+ const text = paragraphLines.join(" ").trim();
167
+ if (text) {
168
+ blocks.push({ kind: "paragraph", html: paragraphHtml(formatInlineMarkdown(text)) });
169
+ }
170
+ paragraphLines = [];
171
+ };
172
+
173
+ const flushList = () => {
174
+ if (!listType || listItems.length === 0) {
175
+ listType = null;
176
+ listItems = [];
177
+ return;
178
+ }
179
+ blocks.push({ kind: "list", html: listHtml(listType, listItems) });
180
+ listType = null;
181
+ listItems = [];
182
+ };
183
+
184
+ for (let index = 0; index < lines.length; index += 1) {
185
+ const line = lines[index].trim();
186
+ if (!line) {
187
+ flushParagraph();
188
+ flushList();
189
+ continue;
190
+ }
191
+
192
+ const headingMatch = line.match(/^(#{1,6})\s+(.+)$/);
193
+ if (headingMatch) {
194
+ flushParagraph();
195
+ flushList();
196
+ const level = headingMatch[1].length;
197
+ blocks.push({
198
+ kind: "heading",
199
+ html: headingHtml(level, formatInlineMarkdown(headingMatch[2])),
200
+ });
201
+ continue;
202
+ }
203
+
204
+ if (line.includes("|")) {
205
+ const block: string[] = [];
206
+ for (let lookAhead = index; lookAhead < lines.length; lookAhead += 1) {
207
+ const candidate = lines[lookAhead].trim();
208
+ if (!candidate || !candidate.includes("|")) {
209
+ break;
210
+ }
211
+ block.push(candidate);
212
+ }
213
+ const tableHtml = toMarkdownTable(block);
214
+ if (tableHtml) {
215
+ flushParagraph();
216
+ flushList();
217
+ blocks.push({ kind: "table", html: tableHtml });
218
+ // Skip rows that were consumed by the table parser.
219
+ index += block.length - 1;
220
+ continue;
221
+ }
222
+ }
223
+
224
+ const bulletMatch = line.match(/^[-*]\s+(.+)$/);
225
+ if (bulletMatch) {
226
+ flushParagraph();
227
+ if (listType && listType !== "ul") {
228
+ flushList();
229
+ }
230
+ listType = "ul";
231
+ listItems.push(bulletMatch[1]);
232
+ continue;
233
+ }
234
+
235
+ const numberMatch = line.match(/^\d+\.\s+(.+)$/);
236
+ if (numberMatch) {
237
+ flushParagraph();
238
+ if (listType && listType !== "ol") {
239
+ flushList();
240
+ }
241
+ listType = "ol";
242
+ listItems.push(numberMatch[1]);
243
+ continue;
244
+ }
245
+
246
+ if (listType) {
247
+ flushList();
248
+ }
249
+ paragraphLines.push(line);
250
+ }
251
+
252
+ flushParagraph();
253
+ flushList();
254
+
255
+ if (blocks.length === 0) {
256
+ return paragraphHtml(formatInlineMarkdown(source));
257
+ }
258
+
259
+ return renderBlocksWithSpacing(blocks);
260
+ }
261
+
262
+ function asObject(input: unknown): Record<string, unknown> {
263
+ if (!input || typeof input !== "object") {
264
+ throw new Error("Tool input must be an object.");
265
+ }
266
+ return input as Record<string, unknown>;
267
+ }
268
+
269
+ function asString(input: unknown, field: string): string {
270
+ if (typeof input !== "string" || !input.trim()) {
271
+ throw new Error(`Missing required string field: ${field}`);
272
+ }
273
+ return input.trim();
274
+ }
275
+
276
+ async function runAppleScript(script: string, args: string[], signal?: AbortSignal): Promise<string> {
277
+ const { stdout } = await runCommandSafe({
278
+ command: "osascript",
279
+ args: ["-e", script, ...args],
280
+ signal,
281
+ timeoutMs: APPLESCRIPT_TIMEOUT_MS,
282
+ });
283
+ return stdout;
284
+ }
285
+
286
+ async function runNotesCreateOrAppend(input: unknown, context: ToolExecutionContext) {
287
+ const payload = asObject(input) as NotesInput;
288
+ const title = asString(payload.title, "title");
289
+ const body = asString(payload.body, "body");
290
+ const normalizedLines = body.replace(/\r\n/g, "\n").split("\n");
291
+ // Remove duplicated leading title lines while keeping the richer/markdown heading when present.
292
+ while (
293
+ normalizedLines.length > 1 &&
294
+ normalizeHeadingLikeText(normalizedLines[0]) &&
295
+ normalizeHeadingLikeText(normalizedLines[0]) === normalizeHeadingLikeText(normalizedLines[1])
296
+ ) {
297
+ normalizedLines.shift();
298
+ }
299
+ // Avoid repeating the note title as the first body line.
300
+ if (
301
+ normalizedLines.length > 0 &&
302
+ normalizeHeadingLikeText(normalizedLines[0]) === normalizeHeadingLikeText(title)
303
+ ) {
304
+ normalizedLines.shift();
305
+ }
306
+ const normalizedBodyInput = normalizedLines.join("\n").trim();
307
+ const formattedBody = normalizeNotesBody(normalizedBodyInput);
308
+ const folder = typeof payload.folder === "string" ? payload.folder.trim() : "";
309
+ const mode = payload.mode === "append" ? "append" : "create";
310
+
311
+ ensureMacOS("Apple Notes automation");
312
+
313
+ if (context.localTools.dryRun) {
314
+ return {
315
+ dryRun: true,
316
+ action: "notes_create_or_append",
317
+ title,
318
+ folder,
319
+ mode,
320
+ bodyPreview: formattedBody.slice(0, 200),
321
+ };
322
+ }
323
+
324
+ const script =
325
+ 'on run argv\n' +
326
+ 'set noteTitle to item 1 of argv\n' +
327
+ 'set noteBody to item 2 of argv\n' +
328
+ 'set folderName to item 3 of argv\n' +
329
+ 'set noteMode to item 4 of argv\n' +
330
+ 'tell application "Notes"\n' +
331
+ 'if not (exists account 1) then error "No Notes account is available."\n' +
332
+ 'set targetAccount to account 1\n' +
333
+ 'set targetFolder to missing value\n' +
334
+ 'if folderName is not "" then\n' +
335
+ 'repeat with f in folders of targetAccount\n' +
336
+ 'if name of f is folderName then set targetFolder to f\n' +
337
+ 'end repeat\n' +
338
+ 'if targetFolder is missing value then\n' +
339
+ 'set targetFolder to make new folder at targetAccount with properties {name:folderName}\n' +
340
+ 'end if\n' +
341
+ 'else\n' +
342
+ 'set targetFolder to folder "Notes" of targetAccount\n' +
343
+ 'end if\n' +
344
+ 'set existingNote to missing value\n' +
345
+ 'repeat with n in notes of targetFolder\n' +
346
+ 'if name of n is noteTitle then\n' +
347
+ 'set existingNote to n\n' +
348
+ 'exit repeat\n' +
349
+ 'end if\n' +
350
+ 'end repeat\n' +
351
+ 'if noteMode is "append" and existingNote is not missing value then\n' +
352
+ 'set body of existingNote to (body of existingNote) & "<br/>" & noteBody\n' +
353
+ 'return "appended"\n' +
354
+ 'else if existingNote is missing value then\n' +
355
+ 'set newNote to make new note at targetFolder with properties {name:noteTitle, body:noteBody}\n' +
356
+ 'return "created"\n' +
357
+ 'else\n' +
358
+ 'set body of existingNote to noteBody\n' +
359
+ 'return "updated"\n' +
360
+ 'end if\n' +
361
+ 'end tell\n' +
362
+ 'end run';
363
+
364
+ const status = await runAppleScript(script, [title, formattedBody, folder, mode], context.signal);
365
+ return {
366
+ status,
367
+ title,
368
+ folder,
369
+ mode,
370
+ };
371
+ }
372
+
373
+ async function runNotesFind(input: unknown, context: ToolExecutionContext) {
374
+ const payload = asObject(input) as NotesInput;
375
+ const query = asString(payload.query, "query").toLowerCase();
376
+ const limit =
377
+ typeof payload.limit === "number" && Number.isFinite(payload.limit)
378
+ ? Math.max(1, Math.min(100, Math.floor(payload.limit)))
379
+ : 10;
380
+
381
+ ensureMacOS("Apple Notes automation");
382
+
383
+ const script =
384
+ 'on run argv\n' +
385
+ 'set needle to item 1 of argv\n' +
386
+ 'set maxCount to (item 2 of argv) as integer\n' +
387
+ 'set outText to ""\n' +
388
+ 'set itemCount to 0\n' +
389
+ 'tell application "Notes"\n' +
390
+ 'if not (exists account 1) then error "No Notes account is available."\n' +
391
+ 'ignoring case\n' +
392
+ 'repeat with acc in accounts\n' +
393
+ 'repeat with f in folders of acc\n' +
394
+ 'repeat with n in notes of f\n' +
395
+ 'set noteTitle to name of n\n' +
396
+ 'set noteBody to body of n\n' +
397
+ 'if ((noteTitle as text) contains needle) or ((noteBody as text) contains needle) then\n' +
398
+ 'set outText to outText & noteTitle & tab & name of f & linefeed\n' +
399
+ 'set itemCount to itemCount + 1\n' +
400
+ 'if itemCount is greater than or equal to maxCount then return outText\n' +
401
+ 'end if\n' +
402
+ 'end if\n' +
403
+ 'end repeat\n' +
404
+ 'end repeat\n' +
405
+ 'end repeat\n' +
406
+ 'end ignoring\n' +
407
+ 'end tell\n' +
408
+ 'return outText\n' +
409
+ 'end run';
410
+
411
+ const output = await runAppleScript(script, [query, String(limit)], context.signal);
412
+ const entries = output
413
+ .split("\n")
414
+ .map((line) => line.trim())
415
+ .filter(Boolean)
416
+ .map((line) => {
417
+ const [title, folder] = line.split("\t");
418
+ return { title, folder };
419
+ });
420
+
421
+ return {
422
+ query,
423
+ count: entries.length,
424
+ entries,
425
+ };
426
+ }
427
+
428
+ export const notesTools: ToolDefinition[] = [
429
+ {
430
+ name: "notes_create_or_append",
431
+ description: "Create or append text in Apple Notes.",
432
+ inputSchema: {
433
+ type: "object",
434
+ required: ["title", "body", "mode"],
435
+ properties: {
436
+ title: { type: "string" },
437
+ body: { type: "string" },
438
+ folder: { type: "string" },
439
+ mode: { type: "string", enum: ["create", "append"] },
440
+ },
441
+ additionalProperties: false,
442
+ },
443
+ risk: "write",
444
+ execute: runNotesCreateOrAppend,
445
+ },
446
+ {
447
+ name: "notes_find",
448
+ description: "Search Apple Notes by title/body text.",
449
+ inputSchema: {
450
+ type: "object",
451
+ required: ["query"],
452
+ properties: {
453
+ query: { type: "string" },
454
+ limit: { type: "number" },
455
+ },
456
+ additionalProperties: false,
457
+ },
458
+ risk: "read",
459
+ execute: runNotesFind,
460
+ },
461
+ ];