commit 90fd137b770e9bb4e19764bd4c05645fddc6415e Author: kenneth Date: Fri Jan 16 00:56:55 2026 +0000 initial commit diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000..745c834 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,36 @@ +FROM mcr.microsoft.com/devcontainers/javascript-node:24-bookworm + +# Install system dependencies +RUN apt-get update && apt-get install -y \ + curl \ + ca-certificates \ + gnupg \ + lsb-release \ + ripgrep \ + && rm -rf /var/lib/apt/lists/* + +# Install latest neovim using pre-built binary +RUN curl -LO https://github.com/neovim/neovim/releases/latest/download/nvim-linux-x86_64.tar.gz \ + && tar -C /opt -xzf nvim-linux-x86_64.tar.gz \ + && ln -s /opt/nvim-linux-x86_64/bin/nvim /usr/local/bin/nvim \ + && rm nvim-linux-x86_64.tar.gz + +# Install Bun as the node user +USER node +RUN curl -fsSL https://bun.sh/install | bash +ENV PATH="/home/node/.bun/bin:$PATH" + +# Switch back to root for any remaining setup +USER root + +# Ensure the node user owns their home directory +RUN chown -R node:node /home/node + +# Set the default user back to node +USER node + +# Set working directory +WORKDIR /workspace + +# Verify bun installation +RUN bun --version diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..b905aae --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,21 @@ +// The Dev Container format allows you to configure your environment. At the heart of it +// is a Docker image or Dockerfile which controls the tools available in your environment. +// +// See https://aka.ms/devcontainer.json for more information. +{ + "name": "Ona", + // Use "image": "mcr.microsoft.com/devcontainers/base:ubuntu-24.04", + // instead of the build to use a pre-built image. + "build": { + "context": ".", + "dockerfile": "Dockerfile" + }, + "postStartCommand": "./scripts/setup-git.sh && ./scripts/setup-nvim.sh" + // Features add additional features to your environment. See https://containers.dev/features + // Beware: features are not supported on all platforms and may have unintended side-effects. + // "features": { + // "ghcr.io/devcontainers/features/docker-in-docker": { + // "moby": false + // } + // } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a14702c --- /dev/null +++ b/.gitignore @@ -0,0 +1,34 @@ +# dependencies (bun install) +node_modules + +# output +out +dist +*.tgz + +# code coverage +coverage +*.lcov + +# logs +logs +_.log +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# caches +.eslintcache +.cache +*.tsbuildinfo + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store diff --git a/.oxfmtrc.json b/.oxfmtrc.json new file mode 100644 index 0000000..16e7a47 --- /dev/null +++ b/.oxfmtrc.json @@ -0,0 +1,12 @@ +{ + "$schema": "./node_modules/oxfmt/configuration_schema.json", + "useTabs": true, + "semi": false, + "trailingComma": "all", + "experimentalSortImports": { + "order": "asc", + "ignoreCase": true, + "newlinesBetween": true + }, + "ignorePatterns": [] +} diff --git a/.oxlintrc.json b/.oxlintrc.json new file mode 100644 index 0000000..b39401c --- /dev/null +++ b/.oxlintrc.json @@ -0,0 +1,144 @@ +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "plugins": ["unicorn", "typescript", "oxc"], + "categories": {}, + "rules": { + "constructor-super": "warn", + "for-direction": "warn", + "no-async-promise-executor": "warn", + "no-caller": "warn", + "no-class-assign": "warn", + "no-compare-neg-zero": "warn", + "no-cond-assign": "warn", + "no-const-assign": "warn", + "no-constant-binary-expression": "warn", + "no-constant-condition": "warn", + "no-control-regex": "warn", + "no-debugger": "warn", + "no-delete-var": "warn", + "no-dupe-class-members": "warn", + "no-dupe-else-if": "warn", + "no-dupe-keys": "warn", + "no-duplicate-case": "warn", + "no-empty-character-class": "warn", + "no-empty-pattern": "warn", + "no-empty-static-block": "warn", + "no-eval": "warn", + "no-ex-assign": "warn", + "no-extra-boolean-cast": "warn", + "no-func-assign": "warn", + "no-global-assign": "warn", + "no-import-assign": "warn", + "no-invalid-regexp": "warn", + "no-irregular-whitespace": "warn", + "no-loss-of-precision": "warn", + "no-new-native-nonconstructor": "warn", + "no-nonoctal-decimal-escape": "warn", + "no-obj-calls": "warn", + "no-self-assign": "warn", + "no-setter-return": "warn", + "no-shadow-restricted-names": "warn", + "no-sparse-arrays": "warn", + "no-this-before-super": "warn", + "no-unassigned-vars": "warn", + "no-unsafe-finally": "warn", + "no-unsafe-negation": "warn", + "no-unsafe-optional-chaining": "warn", + "no-unused-expressions": "warn", + "no-unused-labels": "warn", + "no-unused-private-class-members": "warn", + "no-unused-vars": "warn", + "no-useless-backreference": "warn", + "no-useless-catch": "warn", + "no-useless-escape": "warn", + "no-useless-rename": "warn", + "no-with": "warn", + "require-yield": "warn", + "use-isnan": "warn", + "valid-typeof": "warn", + "oxc/bad-array-method-on-arguments": "warn", + "oxc/bad-char-at-comparison": "warn", + "oxc/bad-comparison-sequence": "warn", + "oxc/bad-min-max-func": "warn", + "oxc/bad-object-literal-comparison": "warn", + "oxc/bad-replace-all-arg": "warn", + "oxc/const-comparisons": "warn", + "oxc/double-comparisons": "warn", + "oxc/erasing-op": "warn", + "oxc/missing-throw": "warn", + "oxc/number-arg-out-of-range": "warn", + "oxc/only-used-in-recursion": "warn", + "oxc/uninvoked-array-callback": "warn", + "typescript/await-thenable": "warn", + "typescript/no-array-delete": "warn", + "typescript/no-base-to-string": "warn", + "typescript/no-duplicate-enum-values": "warn", + "typescript/no-duplicate-type-constituents": "warn", + "typescript/no-extra-non-null-assertion": "warn", + "typescript/no-floating-promises": "warn", + "typescript/no-for-in-array": "warn", + "typescript/no-implied-eval": "warn", + "typescript/no-meaningless-void-operator": "warn", + "typescript/no-misused-new": "warn", + "typescript/no-misused-spread": "warn", + "typescript/no-non-null-asserted-optional-chain": "warn", + "typescript/no-redundant-type-constituents": "warn", + "typescript/no-this-alias": "warn", + "typescript/no-unnecessary-parameter-property-assignment": "warn", + "typescript/no-unsafe-declaration-merging": "warn", + "typescript/no-unsafe-unary-minus": "warn", + "typescript/no-useless-empty-export": "warn", + "typescript/no-wrapper-object-types": "warn", + "typescript/prefer-as-const": "warn", + "typescript/require-array-sort-compare": "warn", + "typescript/restrict-template-expressions": "warn", + "typescript/triple-slash-reference": "warn", + "typescript/unbound-method": "warn", + "unicorn/no-await-in-promise-methods": "warn", + "unicorn/no-empty-file": "warn", + "unicorn/no-invalid-fetch-options": "warn", + "unicorn/no-invalid-remove-event-listener": "warn", + "unicorn/no-new-array": "warn", + "unicorn/no-single-promise-in-promise-methods": "warn", + "unicorn/no-thenable": "warn", + "unicorn/no-unnecessary-await": "warn", + "unicorn/no-useless-fallback-in-spread": "warn", + "unicorn/no-useless-length-check": "warn", + "unicorn/no-useless-spread": "warn", + "unicorn/prefer-set-size": "warn", + "unicorn/prefer-string-starts-ends-with": "warn" + }, + "settings": { + "jsx-a11y": { + "polymorphicPropName": null, + "components": {}, + "attributes": {} + }, + "next": { + "rootDir": [] + }, + "react": { + "formComponents": [], + "linkComponents": [], + "version": null + }, + "jsdoc": { + "ignorePrivate": false, + "ignoreInternal": false, + "ignoreReplacesDocs": true, + "overrideReplacesDocs": true, + "augmentsExtendsReplacesDocs": false, + "implementsReplacesDocs": false, + "exemptDestructuredRootsFromChecks": false, + "tagNamePreference": {} + }, + "vitest": { + "typecheck": false + } + }, + "env": { + "builtin": true + }, + "globals": {}, + "ignorePatterns": [] +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..baa03c9 --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +# aris + +To install dependencies: + +```bash +bun install +``` + +To run: + +```bash +bun run index.ts +``` + +This project was created using `bun init` in bun v1.3.6. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime. diff --git a/apps/.gitkeep b/apps/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..1690925 --- /dev/null +++ b/bun.lock @@ -0,0 +1,72 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "aris", + "devDependencies": { + "@types/bun": "latest", + "oxfmt": "^0.24.0", + "oxlint": "^1.39.0", + }, + "peerDependencies": { + "typescript": "^5", + }, + }, + "packages/aris-core": { + "name": "aris-core", + "version": "0.0.0", + }, + }, + "packages": { + "@oxfmt/darwin-arm64": ["@oxfmt/darwin-arm64@0.24.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-aYXuGf/yq8nsyEcHindGhiz9I+GEqLkVq8CfPbd+6VE259CpPEH+CaGHEO1j6vIOmNr8KHRq+IAjeRO2uJpb8A=="], + + "@oxfmt/darwin-x64": ["@oxfmt/darwin-x64@0.24.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-vs3b8Bs53hbiNvcNeBilzE/+IhDTWKjSBB3v/ztr664nZk65j0xr+5IHMBNz3CFppmX7o/aBta2PxY+t+4KoPg=="], + + "@oxfmt/linux-arm64-gnu": ["@oxfmt/linux-arm64-gnu@0.24.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-ItPDOPoQ0wLj/s8osc5ch57uUcA1Wk8r0YdO8vLRpXA3UNg7KPOm1vdbkIZRRiSUphZcuX5ioOEetEK8H7RlTw=="], + + "@oxfmt/linux-arm64-musl": ["@oxfmt/linux-arm64-musl@0.24.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-JkQO3WnQjQTJONx8nxdgVBfl6BBFfpp9bKhChYhWeakwJdr7QPOAWJ/v3FGZfr0TbqINwnNR74aVZayDDRyXEA=="], + + "@oxfmt/linux-x64-gnu": ["@oxfmt/linux-x64-gnu@0.24.0", "", { "os": "linux", "cpu": "x64" }, "sha512-N/SXlFO+2kak5gMt0oxApi0WXQDhwA0PShR0UbkY0PwtHjfSiDqJSOumyNqgQVoroKr1GNnoRmUqjZIz6DKIcw=="], + + "@oxfmt/linux-x64-musl": ["@oxfmt/linux-x64-musl@0.24.0", "", { "os": "linux", "cpu": "x64" }, "sha512-WM0pek5YDCQf50XQ7GLCE9sMBCMPW/NPAEPH/Hx6Qyir37lEsP4rUmSECo/QFNTU6KBc9NnsviAyJruWPpCMXw=="], + + "@oxfmt/win32-arm64": ["@oxfmt/win32-arm64@0.24.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-vFCseli1KWtwdHrVlT/jWfZ8jP8oYpnPPEjI23mPLW8K/6GEJmmvy0PZP5NpWUFNTzX0lqie58XnrATJYAe9Xw=="], + + "@oxfmt/win32-x64": ["@oxfmt/win32-x64@0.24.0", "", { "os": "win32", "cpu": "x64" }, "sha512-0tmlNzcyewAnauNeBCq0xmAkmiKzl+H09p0IdHy+QKrTQdtixtf+AOjDAADbRfihkS+heF15Pjc4IyJMdAAJjw=="], + + "@oxlint/darwin-arm64": ["@oxlint/darwin-arm64@1.39.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-lT3hNhIa02xCujI6YGgjmYGg3Ht/X9ag5ipUVETaMpx5Rd4BbTNWUPif1WN1YZHxt3KLCIqaAe7zVhatv83HOQ=="], + + "@oxlint/darwin-x64": ["@oxlint/darwin-x64@1.39.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-UT+rfTWd+Yr7iJeSLd/7nF8X4gTYssKh+n77hxl6Oilp3NnG1CKRHxZDy3o3lIBnwgzJkdyUAiYWO1bTMXQ1lA=="], + + "@oxlint/linux-arm64-gnu": ["@oxlint/linux-arm64-gnu@1.39.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-qocBkvS2V6rH0t9AT3DfQunMnj3xkM7srs5/Ycj2j5ZqMoaWd/FxHNVJDFP++35roKSvsRJoS0mtA8/77jqm6Q=="], + + "@oxlint/linux-arm64-musl": ["@oxlint/linux-arm64-musl@1.39.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-arZzAc1PPcz9epvGBBCMHICeyQloKtHX3eoOe62B3Dskn7gf6Q14wnDHr1r9Vp4vtcBATNq6HlKV14smdlC/qA=="], + + "@oxlint/linux-x64-gnu": ["@oxlint/linux-x64-gnu@1.39.0", "", { "os": "linux", "cpu": "x64" }, "sha512-ZVt5qsECpuNprdWxAPpDBwoixr1VTcZ4qAEQA2l/wmFyVPDYFD3oBY/SWACNnWBddMrswjTg9O8ALxYWoEpmXw=="], + + "@oxlint/linux-x64-musl": ["@oxlint/linux-x64-musl@1.39.0", "", { "os": "linux", "cpu": "x64" }, "sha512-pB0hlGyKPbxr9NMIV783lD6cWL3MpaqnZRM9MWni4yBdHPTKyFNYdg5hGD0Bwg+UP4S2rOevq/+OO9x9Bi7E6g=="], + + "@oxlint/win32-arm64": ["@oxlint/win32-arm64@1.39.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-Gg2SFaJohI9+tIQVKXlPw3FsPQFi/eCSWiCgwPtPn5uzQxHRTeQEZKuluz1fuzR5U70TXubb2liZi4Dgl8LJQA=="], + + "@oxlint/win32-x64": ["@oxlint/win32-x64@1.39.0", "", { "os": "win32", "cpu": "x64" }, "sha512-sbi25lfj74hH+6qQtb7s1wEvd1j8OQbTaH8v3xTcDjrwm579Cyh0HBv1YSZ2+gsnVwfVDiCTL1D0JsNqYXszVA=="], + + "@types/bun": ["@types/bun@1.3.6", "", { "dependencies": { "bun-types": "1.3.6" } }, "sha512-uWCv6FO/8LcpREhenN1d1b6fcspAB+cefwD7uti8C8VffIv0Um08TKMn98FynpTiU38+y2dUO55T11NgDt8VAA=="], + + "@types/node": ["@types/node@25.0.9", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-/rpCXHlCWeqClNBwUhDcusJxXYDjZTyE8v5oTO7WbL8eij2nKhUeU89/6xgjU7N4/Vh3He0BtyhJdQbDyhiXAw=="], + + "aris-core": ["aris-core@workspace:packages/aris-core"], + + "bun-types": ["bun-types@1.3.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-OlFwHcnNV99r//9v5IIOgQ9Uk37gZqrNMCcqEaExdkVq3Avwqok1bJFmvGMCkCE0FqzdY8VMOZpfpR3lwI+CsQ=="], + + "oxfmt": ["oxfmt@0.24.0", "", { "dependencies": { "tinypool": "2.0.0" }, "optionalDependencies": { "@oxfmt/darwin-arm64": "0.24.0", "@oxfmt/darwin-x64": "0.24.0", "@oxfmt/linux-arm64-gnu": "0.24.0", "@oxfmt/linux-arm64-musl": "0.24.0", "@oxfmt/linux-x64-gnu": "0.24.0", "@oxfmt/linux-x64-musl": "0.24.0", "@oxfmt/win32-arm64": "0.24.0", "@oxfmt/win32-x64": "0.24.0" }, "bin": { "oxfmt": "bin/oxfmt" } }, "sha512-UjeM3Peez8Tl7IJ9s5UwAoZSiDRMww7BEc21gDYxLq3S3/KqJnM3mjNxsoSHgmBvSeX6RBhoVc2MfC/+96RdSw=="], + + "oxlint": ["oxlint@1.39.0", "", { "optionalDependencies": { "@oxlint/darwin-arm64": "1.39.0", "@oxlint/darwin-x64": "1.39.0", "@oxlint/linux-arm64-gnu": "1.39.0", "@oxlint/linux-arm64-musl": "1.39.0", "@oxlint/linux-x64-gnu": "1.39.0", "@oxlint/linux-x64-musl": "1.39.0", "@oxlint/win32-arm64": "1.39.0", "@oxlint/win32-x64": "1.39.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.10.0" }, "optionalPeers": ["oxlint-tsgolint"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-wSiLr0wjG+KTU6c1LpVoQk7JZ7l8HCKlAkVDVTJKWmCGazsNxexxnOXl7dsar92mQcRnzko5g077ggP3RINSjA=="], + + "tinypool": ["tinypool@2.0.0", "", {}, "sha512-/RX9RzeH2xU5ADE7n2Ykvmi9ED3FBGPAjw9u3zucrNNaEBIO0HPSYgL0NT7+3p147ojeSdaVu08F6hjpv31HJg=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + } +} diff --git a/docs/ai-agent-ideas.md b/docs/ai-agent-ideas.md new file mode 100644 index 0000000..b05787b --- /dev/null +++ b/docs/ai-agent-ideas.md @@ -0,0 +1,278 @@ +# AI Agent Ideas for ARIS + +> Brainstorm document. Not all ideas are feasible or desirable - just capturing possibilities. + +## 1. Feed Curation Agent + +Sits between the reconciler and UI. Reranks/filters the raw feed based on learned preferences and context. + +**Examples:** + +- User always dismisses weather items in the morning → agent deprioritizes them +- User frequently taps calendar items before meetings → agent boosts them 30 minutes prior +- Deduplicates or groups related items ("3 meetings in the next hour") +- Learns time-of-day patterns (work items in morning, personal in evening) + +**Interface:** + +```typescript +interface FeedAgent { + process( + items: FeedItem[], + context: Context, + userPreferences: UserPreferences, + ): Promise +} +``` + +**Fits naturally as a post-processor after reconciliation.** + +--- + +## 2. Query Agent + +User asks natural language questions about their feed or connected data. + +**Examples:** + +- "What's on my calendar tomorrow?" +- "When's my next flight?" +- "Summarize my day" +- "Do I have any conflicts this week?" +- "What did I have scheduled last Tuesday?" + +**Behavior:** + +- Agent queries relevant sources directly or searches recent feed history +- Synthesizes data into conversational response +- Could generate a temporary filtered feed view + +--- + +## 3. Proactive Agent + +Monitors context changes and triggers actions without user prompting. + +**Examples:** + +- Near grocery store + "buy milk" in tasks → surfaces reminder +- Calendar conflict detected → alerts before it happens +- Weather changing → suggests leaving earlier for commute +- Unusual traffic on commute route → notifies user +- Meeting in 10 minutes but user hasn't moved → gentle nudge +- Package delivered + user is home → notification + +**Implementation considerations:** + +- Needs background processing / push capability +- Privacy implications of continuous monitoring +- Battery/resource usage on mobile + +--- + +## 4. Source Configuration Agent + +Helps users set up and tune sources through conversation. + +**Examples:** + +- "Show me fewer emails" +- "Only show calendar events for work" +- "I don't care about weather" +- "Prioritize tasks over calendar" +- "Add my Spotify account" + +**Behavior:** + +- Translates natural language into source config changes +- Can explain what each source does +- Helps troubleshoot when sources aren't working + +--- + +## 5. Feed Item Generation Agent (AI-Native Sources) + +Some sources are AI-powered rather than API-driven. + +**Examples:** + +- Daily briefing: "You have 4 meetings today, busiest is 2-4pm" +- Pattern-based reminders: "You usually go to the gym on Tuesdays" +- Suggested actions: "You haven't responded to Sarah's email from yesterday" +- Weekly review: "You completed 12 tasks this week, 3 are overdue" +- Context synthesis: "Your flight lands at 3pm, you have a meeting at 5pm - that's tight" + +**These are sources that implement `DataSource` but use an LLM internally.** + +--- + +## 6. Action Agent + +Executes actions on behalf of the user based on feed items. + +**Examples:** + +- "Snooze this reminder for 1 hour" +- "RSVP yes to this event" +- "Mark this task as done" +- "Send a quick reply saying I'll be late" +- "Book an Uber to this location" + +**Considerations:** + +- Needs action capabilities per source +- Confirmation UX for destructive/costly actions +- OAuth scopes for write access + +--- + +## 7. Explanation Agent + +Explains why items appear in the feed. + +**Examples:** + +- User asks "Why am I seeing this?" +- Agent explains: "This calendar event starts in 15 minutes and you marked it as important" +- Helps users understand and trust the system +- Useful for debugging source behavior + +--- + +## 8. Cross-Source Reasoning Agent + +Connects information across multiple sources to surface insights. + +**Examples:** + +- Calendar shows dinner reservation + weather source shows rain → "Bring an umbrella to dinner" +- Flight delayed + calendar has meeting after landing → "Your 3pm meeting may be affected by flight delay" +- Task "buy birthday gift" + calendar shows birthday party tomorrow → boosts task priority +- Email mentions address + maps knows traffic → "Leave by 2pm to make your 3pm appointment" + +**This is more complex - requires understanding relationships between items.** + +--- + +## 9. Memory Agent + +Maintains long-term memory of user interactions and preferences. + +**Examples:** + +- Remembers user dismissed a recurring item 5 times → stops showing it +- Knows user's home/work locations from patterns +- Tracks what times user typically checks the feed +- Remembers user's stated preferences from conversations +- Builds implicit preference model over time + +**Feeds into other agents (especially Feed Curation).** + +--- + +## 10. Onboarding Agent + +Guides new users through setup conversationally. + +**Examples:** + +- "What apps do you use for calendar?" +- "Would you like to see weather in your feed?" +- "What's most important to you - tasks, calendar, or communications?" +- Progressively enables sources based on conversation +- Explains privacy implications of each source + +--- + +## 11. Anomaly Detection Agent + +Surfaces unusual patterns or items that break routine. + +**Examples:** + +- "You have a meeting at 6am tomorrow - that's unusual for you" +- "This is your first free afternoon in 2 weeks" +- "You haven't completed any tasks in 3 days" +- "Your calendar is empty tomorrow - did you mean to block time?" + +--- + +## 12. Delegation Agent + +Handles tasks the user delegates via natural language. + +**Examples:** + +- "Remind me about this tomorrow" +- "Schedule a meeting with John next week" +- "Add milk to my shopping list" +- "Find a time that works for both me and Sarah" + +**Requires write access to various sources.** + +--- + +## 13. Summary Agent + +Generates periodic summaries of feed activity. + +**Examples:** + +- Morning briefing: "Here's your day ahead" +- Evening recap: "Here's what happened today" +- Weekly digest: "This week you had 12 meetings, completed 8 tasks" +- Travel summary: "Your trip to NYC: 3 flights, 2 hotels, 5 meetings" + +**Could be a scheduled AI-native source.** + +--- + +## 14. Notification Agent + +Decides what deserves a push notification vs. passive feed presence. + +**Examples:** + +- High-priority items get pushed +- Learns what user actually responds to +- Batches low-priority items into digest notifications +- Respects focus modes / do-not-disturb + +**Reduces notification fatigue while ensuring important items aren't missed.** + +--- + +## 15. Conversation Agent + +General-purpose assistant that can discuss feed items. + +**Examples:** + +- User taps an item and asks "Tell me more about this" +- "What should I prepare for this meeting?" +- "What's the best route to this location?" +- "Who else is attending this event?" + +**Contextual conversation anchored to specific feed items.** + +--- + +## Implementation Priority Suggestion + +If implementing incrementally: + +1. **Feed Curation Agent** - highest value, fits existing architecture +2. **Query Agent** - natural user expectation for AI assistant +3. **Summary Agent** - low risk, high perceived value +4. **Proactive Agent** - differentiator, but complex +5. **Cross-Source Reasoning** - advanced, builds on others + +--- + +## Open Questions + +- Where do agents run? (Client, server, edge?) +- How to handle agent latency in feed rendering? +- Privacy model for agent memory/learning? +- How do agents interact with third-party sources? +- Cost management for LLM calls? diff --git a/docs/architecture-draft.md b/docs/architecture-draft.md new file mode 100644 index 0000000..44bf254 --- /dev/null +++ b/docs/architecture-draft.md @@ -0,0 +1,110 @@ +# ARIS Architecture Draft + +> This is a working draft from initial architecture discussions. Not final documentation. + +## Overview + +ARIS is an AI-powered personal assistant. The core aggregates data from various sources and compiles a feed of contextually relevant items - similar to Google Now. The feed shows users useful information based on their current context (date, time, location). + +Examples of feed items: + +- Upcoming calendar events +- Nearby locations +- Current weather +- Alerts + +## Design Principles + +1. **Extensibility**: The core must support different data sources, including third-party sources. +2. **Separation of concerns**: Core handles data only. UI rendering is a separate system. +3. **Parallel execution**: Sources run in parallel; no inter-source dependencies. +4. **Graceful degradation**: Failed sources are skipped; partial results are returned. + +## Architecture + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Backend │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────┐ │ +│ │ aris-core │ │ Sources │ │ UI Registry │ │ +│ │ │ │ (plugins) │ │ (schemas from │ │ +│ │ - Reconciler│◄───│ - Calendar │ │ third parties)│ │ +│ │ - Context │ │ - Weather │ │ │ │ +│ │ - FeedItem │ │ - Spotify │ │ │ │ +│ └─────────────┘ └─────────────┘ └─────────────────┘ │ +│ │ │ │ +│ ▼ ▼ │ +│ Feed (data only) UI Schemas (JSON) │ +└─────────────────────────────────────────────────────────────┘ + │ │ + ▼ ▼ +┌─────────────────────────────────────────────────────────────┐ +│ Frontend │ +│ ┌──────────────────────────────────────────────────────┐ │ +│ │ Renderer │ │ +│ │ - Receives feed items │ │ +│ │ - Fetches UI schema by item type │ │ +│ │ - Renders using json-render or similar │ │ +│ └──────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────┘ +``` + +## Core Package (`aris-core`) + +The core is responsible for: + +- Defining the context and feed item interfaces +- Providing a reconciler that orchestrates data sources +- Returning a flat list of prioritized feed items + +### Key Concepts + +- **Context**: Time and location (with accuracy) passed to all sources +- **FeedItem**: Has an ID (source-generated, stable), type, priority, timestamp, and JSON-serializable data +- **DataSource**: Interface that third parties implement to provide feed items +- **Reconciler**: Orchestrates sources, runs them in parallel, returns items and any errors + +## Data Sources + +Key decisions: + +- Sources receive the full context and decide internally what to use +- Each source returns a single item type (e.g., separate "Calendar Source" and "Location Suggestion Source" rather than a combined "Google Source") +- Sources live in separate packages, not in the core +- Sources are responsible for: + - Transforming their domain data into feed items + - Assigning priority based on domain logic (e.g., "event starting in 10 minutes" = high priority) + - Returning empty arrays when nothing is relevant + +### Configuration + +Configuration is passed at source registration time, not per reconcile call. Sources can use config for filtering/limiting (e.g., "max 3 calendar events"). + +## Feed Output + +- Flat list of `FeedItem` objects +- No UI information (no icons, card types, etc.) +- Items are a discriminated union by `type` field +- Reconciler sorts by priority; can act as tiebreaker + +## UI Rendering (Separate from Core) + +The core does not handle UI. For extensible third-party UI: + +1. Third-party apps register their UI schemas through the backend (UI Registry) +2. Frontend fetches UI schemas from the backend +3. Frontend matches feed items to schemas by `type` and renders accordingly + +This approach: + +- Keeps the core focused on data +- Works across platforms (web, React Native) +- Avoids the need for third parties to inject code into the app +- Uses a json-render style approach for declarative UI from JSON schemas + +Reference: https://github.com/vercel-labs/json-render + +## Open Questions + +- Exact schema format for UI registry +- How third parties authenticate/register their sources and UI schemas diff --git a/package.json b/package.json new file mode 100644 index 0000000..65fe13c --- /dev/null +++ b/package.json @@ -0,0 +1,23 @@ +{ + "name": "aris", + "private": true, + "workspaces": [ + "packages/*", + "apps/*" + ], + "type": "module", + "scripts": { + "lint": "oxlint .", + "lint:fix": "oxlint --fix .", + "format": "oxfmt --write .", + "format:check": "oxfmt --check ." + }, + "devDependencies": { + "@types/bun": "latest", + "oxfmt": "^0.24.0", + "oxlint": "^1.39.0" + }, + "peerDependencies": { + "typescript": "^5" + } +} diff --git a/packages/aris-core/context.ts b/packages/aris-core/context.ts new file mode 100644 index 0000000..76c7601 --- /dev/null +++ b/packages/aris-core/context.ts @@ -0,0 +1,10 @@ +export interface Location { + lat: number + lng: number + accuracy: number +} + +export interface Context { + time: Date + location?: Location +} diff --git a/packages/aris-core/data-source.ts b/packages/aris-core/data-source.ts new file mode 100644 index 0000000..61da31d --- /dev/null +++ b/packages/aris-core/data-source.ts @@ -0,0 +1,7 @@ +import type { Context } from "./context" +import type { FeedItem } from "./feed" + +export interface DataSource { + readonly type: TItem["type"] + query(context: Context, config: TConfig): Promise +} diff --git a/packages/aris-core/feed.ts b/packages/aris-core/feed.ts new file mode 100644 index 0000000..6b3c323 --- /dev/null +++ b/packages/aris-core/feed.ts @@ -0,0 +1,10 @@ +export interface FeedItem< + TType extends string = string, + TData extends Record = Record, +> { + id: string + type: TType + priority: number + timestamp: Date + data: TData +} diff --git a/packages/aris-core/index.ts b/packages/aris-core/index.ts new file mode 100644 index 0000000..70dca66 --- /dev/null +++ b/packages/aris-core/index.ts @@ -0,0 +1,5 @@ +export type { Context, Location } from "./context" +export type { FeedItem } from "./feed" +export type { DataSource } from "./data-source" +export type { ReconcilerConfig, ReconcileResult, SourceError } from "./reconciler" +export { Reconciler } from "./reconciler" diff --git a/packages/aris-core/package.json b/packages/aris-core/package.json new file mode 100644 index 0000000..86e7bd8 --- /dev/null +++ b/packages/aris-core/package.json @@ -0,0 +1,10 @@ +{ + "name": "@aris/core", + "version": "0.0.0", + "type": "module", + "main": "index.ts", + "types": "index.ts", + "scripts": { + "test": "bun test ." + } +} diff --git a/packages/aris-core/reconciler.test.ts b/packages/aris-core/reconciler.test.ts new file mode 100644 index 0000000..98992ed --- /dev/null +++ b/packages/aris-core/reconciler.test.ts @@ -0,0 +1,240 @@ +import { describe, expect, test } from "bun:test" + +import type { Context } from "./context" +import type { DataSource } from "./data-source" +import type { FeedItem } from "./feed" + +import { Reconciler } from "./reconciler" + +type WeatherData = { temp: number } +type WeatherItem = FeedItem<"weather", WeatherData> + +type CalendarData = { title: string } +type CalendarItem = FeedItem<"calendar", CalendarData> + +const createMockContext = (): Context => ({ + time: new Date("2026-01-15T12:00:00Z"), +}) + +const createWeatherSource = (items: WeatherItem[], delay = 0): DataSource => ({ + type: "weather", + async query() { + if (delay > 0) { + await new Promise((resolve) => setTimeout(resolve, delay)) + } + return items + }, +}) + +const createCalendarSource = (items: CalendarItem[]): DataSource => ({ + type: "calendar", + async query() { + return items + }, +}) + +const createFailingSource = (type: string, error: Error): DataSource => ({ + type, + async query() { + throw error + }, +}) + +describe("Reconciler", () => { + test("returns empty result when no sources registered", async () => { + const reconciler = new Reconciler() + const result = await reconciler.reconcile(createMockContext()) + + expect(result.items).toEqual([]) + expect(result.errors).toEqual([]) + }) + + test("collects items from single source", async () => { + const items: WeatherItem[] = [ + { + id: "weather-1", + type: "weather", + priority: 0.5, + timestamp: new Date(), + data: { temp: 20 }, + }, + ] + + const reconciler = new Reconciler().register(createWeatherSource(items)) + const result = await reconciler.reconcile(createMockContext()) + + expect(result.items).toEqual(items) + expect(result.errors).toEqual([]) + }) + + test("collects items from multiple sources", async () => { + const weatherItems: WeatherItem[] = [ + { + id: "weather-1", + type: "weather", + priority: 0.5, + timestamp: new Date(), + data: { temp: 20 }, + }, + ] + + const calendarItems: CalendarItem[] = [ + { + id: "calendar-1", + type: "calendar", + priority: 0.8, + timestamp: new Date(), + data: { title: "Meeting" }, + }, + ] + + const reconciler = new Reconciler() + .register(createWeatherSource(weatherItems)) + .register(createCalendarSource(calendarItems)) + + const result = await reconciler.reconcile(createMockContext()) + + expect(result.items).toHaveLength(2) + expect(result.errors).toEqual([]) + }) + + test("sorts items by priority descending", async () => { + const weatherItems: WeatherItem[] = [ + { + id: "weather-1", + type: "weather", + priority: 0.2, + timestamp: new Date(), + data: { temp: 20 }, + }, + ] + + const calendarItems: CalendarItem[] = [ + { + id: "calendar-1", + type: "calendar", + priority: 0.9, + timestamp: new Date(), + data: { title: "Meeting" }, + }, + ] + + const reconciler = new Reconciler() + .register(createWeatherSource(weatherItems)) + .register(createCalendarSource(calendarItems)) + + const result = await reconciler.reconcile(createMockContext()) + + expect(result.items[0]?.id).toBe("calendar-1") + expect(result.items[1]?.id).toBe("weather-1") + }) + + test("captures errors from failing sources", async () => { + const error = new Error("Source failed") + + const reconciler = new Reconciler().register(createFailingSource("failing", error)) + + const result = await reconciler.reconcile(createMockContext()) + + expect(result.items).toEqual([]) + expect(result.errors).toHaveLength(1) + expect(result.errors[0]?.sourceType).toBe("failing") + expect(result.errors[0]?.error.message).toBe("Source failed") + }) + + test("returns partial results when some sources fail", async () => { + const items: WeatherItem[] = [ + { + id: "weather-1", + type: "weather", + priority: 0.5, + timestamp: new Date(), + data: { temp: 20 }, + }, + ] + + const reconciler = new Reconciler() + .register(createWeatherSource(items)) + .register(createFailingSource("failing", new Error("Failed"))) + + const result = await reconciler.reconcile(createMockContext()) + + expect(result.items).toHaveLength(1) + expect(result.errors).toHaveLength(1) + }) + + test("times out slow sources", async () => { + const items: WeatherItem[] = [ + { + id: "weather-1", + type: "weather", + priority: 0.5, + timestamp: new Date(), + data: { temp: 20 }, + }, + ] + + const reconciler = new Reconciler({ timeout: 50 }).register(createWeatherSource(items, 100)) + + const result = await reconciler.reconcile(createMockContext()) + + expect(result.items).toEqual([]) + expect(result.errors).toHaveLength(1) + expect(result.errors[0]?.sourceType).toBe("weather") + expect(result.errors[0]?.error.message).toContain("timed out") + }) + + test("unregister removes source", async () => { + const items: WeatherItem[] = [ + { + id: "weather-1", + type: "weather", + priority: 0.5, + timestamp: new Date(), + data: { temp: 20 }, + }, + ] + + const reconciler = new Reconciler().register(createWeatherSource(items)).unregister("weather") + + const result = await reconciler.reconcile(createMockContext()) + expect(result.items).toEqual([]) + }) + + test("infers discriminated union type from chained registers", async () => { + const weatherItems: WeatherItem[] = [ + { + id: "weather-1", + type: "weather", + priority: 0.5, + timestamp: new Date(), + data: { temp: 20 }, + }, + ] + + const calendarItems: CalendarItem[] = [ + { + id: "calendar-1", + type: "calendar", + priority: 0.8, + timestamp: new Date(), + data: { title: "Meeting" }, + }, + ] + + const reconciler = new Reconciler() + .register(createWeatherSource(weatherItems)) + .register(createCalendarSource(calendarItems)) + + const { items } = await reconciler.reconcile(createMockContext()) + + // Type narrowing should work + for (const item of items) { + if (item.type === "weather") { + expect(typeof item.data.temp).toBe("number") + } else if (item.type === "calendar") { + expect(typeof item.data.title).toBe("string") + } + } + }) +}) diff --git a/packages/aris-core/reconciler.ts b/packages/aris-core/reconciler.ts new file mode 100644 index 0000000..35451ad --- /dev/null +++ b/packages/aris-core/reconciler.ts @@ -0,0 +1,88 @@ +import type { Context } from "./context" +import type { DataSource } from "./data-source" +import type { FeedItem } from "./feed" + +export interface ReconcilerConfig { + timeout?: number +} + +export interface SourceError { + sourceType: string + error: Error +} + +export interface ReconcileResult { + items: TItem[] + errors: SourceError[] +} + +interface RegisteredSource { + source: DataSource + config: unknown +} + +const DEFAULT_TIMEOUT = 5000 + +export class Reconciler { + private sources = new Map() + private timeout: number + + constructor(config?: ReconcilerConfig) { + this.timeout = config?.timeout ?? DEFAULT_TIMEOUT + } + + register( + source: DataSource, + config?: TConfig, + ): Reconciler { + this.sources.set(source.type, { + source: source as DataSource, + config, + }) + return this as Reconciler + } + + unregister(sourceType: T): Reconciler> { + this.sources.delete(sourceType) + return this as unknown as Reconciler> + } + + async reconcile(context: Context): Promise> { + const entries = Array.from(this.sources.values()) + + const results = await Promise.allSettled( + entries.map(({ source, config }) => + withTimeout(source.query(context, config), this.timeout, source.type), + ), + ) + + const items: FeedItem[] = [] + const errors: SourceError[] = [] + + results.forEach((result, i) => { + const sourceType = entries[i]!.source.type + + if (result.status === "fulfilled") { + items.push(...result.value) + } else { + errors.push({ + sourceType, + error: result.reason instanceof Error ? result.reason : new Error(String(result.reason)), + }) + } + }) + + items.sort((a, b) => b.priority - a.priority) + + return { items, errors } as ReconcileResult + } +} + +function withTimeout(promise: Promise, ms: number, sourceType: string): Promise { + return Promise.race([ + promise, + new Promise((_, reject) => + setTimeout(() => reject(new Error(`Source "${sourceType}" timed out after ${ms}ms`)), ms), + ), + ]) +} diff --git a/scripts/setup-git.sh b/scripts/setup-git.sh new file mode 100755 index 0000000..912a71e --- /dev/null +++ b/scripts/setup-git.sh @@ -0,0 +1,158 @@ +#!/bin/bash + +# Git setup script +# Sets up user info, email, and credential helpers with Gitea access token + +set -e + +echo "Setting up Git configuration..." + +# Check if required environment variables are set +if [ -z "$GIT_USER" ]; then + echo "Error: GIT_USER environment variable is not set" + exit 1 +fi + +if [ -z "$GIT_EMAIL" ]; then + echo "Error: GIT_EMAIL environment variable is not set" + exit 1 +fi + +# Set user name and email from environment variables +git config --global user.name "$GIT_USER" +git config --global user.email "$GIT_EMAIL" + +# Set up credential helper for HTTPS authentication +git config --global credential.helper store + +# Check if GITEA_ACCESS_TOKEN is set +if [ -z "$GITEA_ACCESS_TOKEN" ]; then + echo "Warning: GITEA_ACCESS_TOKEN environment variable is not set" + echo "You'll need to set this environment variable for automatic authentication" + exit 1 +fi + +# Set up credential store with the access token +# This assumes your Gitea instance is accessible via HTTPS +# Adjust the URL pattern to match your Gitea instance +echo "Setting up credential store..." + +# Create credentials file if it doesn't exist +CREDENTIAL_FILE="$HOME/.git-credentials" +touch "$CREDENTIAL_FILE" +chmod 600 "$CREDENTIAL_FILE" + +# Add Gitea credentials (adjust URL to match your Gitea instance) +# Format: https://username:token@gitea.example.com +# Using the token as both username and password is common for API tokens +echo "https://$GITEA_USERNAME:$GITEA_ACCESS_TOKEN@code.nym.sh" >> "$CREDENTIAL_FILE" + +# Additional Git configurations for better experience +git config --global init.defaultBranch main +git config --global pull.rebase false +git config --global push.default simple +git config --global core.autocrlf input + +echo "Git configuration completed successfully!" +echo "User: $(git config --global user.name)" +echo "Email: $(git config --global user.email)" +echo "Credential helper: $(git config --global credential.helper)" + +# Verify setup by testing credential access (optional) +echo "Git setup complete. Credentials are stored for automatic authentication." + +# GPG key setup +echo "" +echo "Setting up GPG key for commit signing..." + +if [ -n "$GPG_PRIVATE_KEY" ]; then + echo "Importing GPG private key from environment variable..." + + # Import the private key with passphrase if provided + if [ -n "$GPG_PRIVATE_KEY_PASSPHRASE" ]; then + echo "Using provided passphrase for key import..." + # Create temporary file for the key + TEMP_KEY_FILE=$(mktemp) + echo -e "$GPG_PRIVATE_KEY" > "$TEMP_KEY_FILE" + chmod 600 "$TEMP_KEY_FILE" + gpg --batch --yes --pinentry-mode loopback --passphrase "$GPG_PRIVATE_KEY_PASSPHRASE" --import "$TEMP_KEY_FILE" + rm -f "$TEMP_KEY_FILE" + else + echo "No passphrase provided, importing key..." + # Create temporary file for the key + TEMP_KEY_FILE=$(mktemp) + echo -e "$GPG_PRIVATE_KEY" > "$TEMP_KEY_FILE" + chmod 600 "$TEMP_KEY_FILE" + gpg --batch --import "$TEMP_KEY_FILE" + rm -f "$TEMP_KEY_FILE" + fi + + if [ $? -eq 0 ]; then + echo "GPG key imported successfully!" + + # Get the key ID + KEY_ID=$(gpg --list-secret-keys --keyid-format=long "$GIT_EMAIL" | grep 'sec' | cut -d'/' -f2 | cut -d' ' -f1) + + if [ -n "$KEY_ID" ]; then + # Configure Git to use the imported key + git config --global user.signingkey "$KEY_ID" + git config --global commit.gpgsign true + git config --global gpg.program gpg + + echo "Git configured to use GPG key: $KEY_ID" + + # Set ultimate trust for the imported key (since it's our own key) + if [ -n "$GPG_PRIVATE_KEY_PASSPHRASE" ]; then + echo -e "5\ny\n" | gpg --batch --command-fd 0 --expert --pinentry-mode loopback --passphrase "$GPG_PRIVATE_KEY_PASSPHRASE" --edit-key "$KEY_ID" trust quit 2>/dev/null + else + echo -e "5\ny\n" | gpg --batch --command-fd 0 --expert --edit-key "$KEY_ID" trust quit 2>/dev/null + fi + + # Configure GPG for non-interactive use + echo "Configuring GPG for non-interactive signing..." + mkdir -p ~/.gnupg + chmod 700 ~/.gnupg + + # Configure gpg.conf for loopback pinentry + cat > ~/.gnupg/gpg.conf << EOF +use-agent +pinentry-mode loopback +EOF + + # Configure gpg-agent for passphrase caching + cat > ~/.gnupg/gpg-agent.conf << EOF +default-cache-ttl 28800 +max-cache-ttl 28800 +allow-loopback-pinentry +EOF + # Restart GPG agent + gpg-connect-agent reloadagent /bye 2>/dev/null || true + + echo "GPG key setup complete!" + else + echo "Warning: Could not find key ID for $GIT_EMAIL" + fi + else + echo "Error: Failed to import GPG key" + fi +else + echo "GPG_PRIVATE_KEY environment variable not set." + echo "To generate a new GPG key for commit signing, run:" + echo "gpg --batch --full-generate-key <