initial commit

This commit is contained in:
2026-01-16 00:56:55 +00:00
commit 90fd137b77
21 changed files with 1308 additions and 0 deletions

36
.devcontainer/Dockerfile Normal file
View File

@@ -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

View File

@@ -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
// }
// }
}

34
.gitignore vendored Normal file
View File

@@ -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

12
.oxfmtrc.json Normal file
View File

@@ -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": []
}

144
.oxlintrc.json Normal file
View File

@@ -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": []
}

15
README.md Normal file
View File

@@ -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.

0
apps/.gitkeep Normal file
View File

72
bun.lock Normal file
View File

@@ -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=="],
}
}

278
docs/ai-agent-ideas.md Normal file
View File

@@ -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<FeedItem[]>
}
```
**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?

110
docs/architecture-draft.md Normal file
View File

@@ -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

23
package.json Normal file
View File

@@ -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"
}
}

View File

@@ -0,0 +1,10 @@
export interface Location {
lat: number
lng: number
accuracy: number
}
export interface Context {
time: Date
location?: Location
}

View File

@@ -0,0 +1,7 @@
import type { Context } from "./context"
import type { FeedItem } from "./feed"
export interface DataSource<TItem extends FeedItem = FeedItem, TConfig = unknown> {
readonly type: TItem["type"]
query(context: Context, config: TConfig): Promise<TItem[]>
}

View File

@@ -0,0 +1,10 @@
export interface FeedItem<
TType extends string = string,
TData extends Record<string, unknown> = Record<string, unknown>,
> {
id: string
type: TType
priority: number
timestamp: Date
data: TData
}

View File

@@ -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"

View File

@@ -0,0 +1,10 @@
{
"name": "@aris/core",
"version": "0.0.0",
"type": "module",
"main": "index.ts",
"types": "index.ts",
"scripts": {
"test": "bun test ."
}
}

View File

@@ -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<WeatherItem> => ({
type: "weather",
async query() {
if (delay > 0) {
await new Promise((resolve) => setTimeout(resolve, delay))
}
return items
},
})
const createCalendarSource = (items: CalendarItem[]): DataSource<CalendarItem> => ({
type: "calendar",
async query() {
return items
},
})
const createFailingSource = (type: string, error: Error): DataSource<FeedItem> => ({
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")
}
}
})
})

View File

@@ -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<TItem extends FeedItem = FeedItem> {
items: TItem[]
errors: SourceError[]
}
interface RegisteredSource {
source: DataSource<FeedItem, unknown>
config: unknown
}
const DEFAULT_TIMEOUT = 5000
export class Reconciler<TItems extends FeedItem = never> {
private sources = new Map<string, RegisteredSource>()
private timeout: number
constructor(config?: ReconcilerConfig) {
this.timeout = config?.timeout ?? DEFAULT_TIMEOUT
}
register<TItem extends FeedItem, TConfig>(
source: DataSource<TItem, TConfig>,
config?: TConfig,
): Reconciler<TItems | TItem> {
this.sources.set(source.type, {
source: source as DataSource<FeedItem, unknown>,
config,
})
return this as Reconciler<TItems | TItem>
}
unregister<T extends TItems["type"]>(sourceType: T): Reconciler<Exclude<TItems, { type: T }>> {
this.sources.delete(sourceType)
return this as unknown as Reconciler<Exclude<TItems, { type: T }>>
}
async reconcile(context: Context): Promise<ReconcileResult<TItems>> {
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<TItems>
}
}
function withTimeout<T>(promise: Promise<T>, ms: number, sourceType: string): Promise<T> {
return Promise.race([
promise,
new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error(`Source "${sourceType}" timed out after ${ms}ms`)), ms),
),
])
}

158
scripts/setup-git.sh Executable file
View File

@@ -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 <<EOF"
echo "%echo Generating GPG key for $GIT_USER"
echo "Key-Type: RSA"
echo "Key-Length: 4096"
echo "Subkey-Type: RSA"
echo "Subkey-Length: 4096"
echo "Name-Real: $GIT_USER"
echo "Name-Email: $GIT_EMAIL"
echo "Expire-Date: 2y"
echo "Passphrase: "
echo "%commit"
echo "%echo GPG key generation complete"
echo "EOF"
echo ""
echo "After generating the key, configure Git to use it:"
echo "git config --global user.signingkey \$(gpg --list-secret-keys --keyid-format=long $GIT_EMAIL | grep 'sec' | cut -d'/' -f2 | cut -d' ' -f1)"
echo "git config --global commit.gpgsign true"
fi

6
scripts/setup-nvim.sh Executable file
View File

@@ -0,0 +1,6 @@
#!/usr/bin/env bash
set -euo pipefail
# Prime lazy.nvim and install plugins, then ensure Mason tools are installed.
nvim --headless "+Lazy sync" +qall
nvim --headless "+MasonInstall vtsls" +qall

29
tsconfig.json Normal file
View File

@@ -0,0 +1,29 @@
{
"compilerOptions": {
// Environment setup & latest features
"lib": ["ESNext"],
"target": "ESNext",
"module": "Preserve",
"moduleDetection": "force",
"jsx": "react-jsx",
"allowJs": true,
// Bundler mode
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
// Best practices
"strict": true,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
// Some stricter flags (disabled by default)
"noUnusedLocals": false,
"noUnusedParameters": false,
"noPropertyAccessFromIndexSignature": false
}
}