mirror of
https://github.com/kennethnym/aris.git
synced 2026-02-01 20:51:17 +00:00
initial commit
This commit is contained in:
36
.devcontainer/Dockerfile
Normal file
36
.devcontainer/Dockerfile
Normal 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
|
||||
21
.devcontainer/devcontainer.json
Normal file
21
.devcontainer/devcontainer.json
Normal 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
34
.gitignore
vendored
Normal 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
12
.oxfmtrc.json
Normal 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
144
.oxlintrc.json
Normal 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
15
README.md
Normal 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
0
apps/.gitkeep
Normal file
72
bun.lock
Normal file
72
bun.lock
Normal 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
278
docs/ai-agent-ideas.md
Normal 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
110
docs/architecture-draft.md
Normal 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
23
package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
10
packages/aris-core/context.ts
Normal file
10
packages/aris-core/context.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export interface Location {
|
||||
lat: number
|
||||
lng: number
|
||||
accuracy: number
|
||||
}
|
||||
|
||||
export interface Context {
|
||||
time: Date
|
||||
location?: Location
|
||||
}
|
||||
7
packages/aris-core/data-source.ts
Normal file
7
packages/aris-core/data-source.ts
Normal 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[]>
|
||||
}
|
||||
10
packages/aris-core/feed.ts
Normal file
10
packages/aris-core/feed.ts
Normal 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
|
||||
}
|
||||
5
packages/aris-core/index.ts
Normal file
5
packages/aris-core/index.ts
Normal 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"
|
||||
10
packages/aris-core/package.json
Normal file
10
packages/aris-core/package.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "@aris/core",
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"main": "index.ts",
|
||||
"types": "index.ts",
|
||||
"scripts": {
|
||||
"test": "bun test ."
|
||||
}
|
||||
}
|
||||
240
packages/aris-core/reconciler.test.ts
Normal file
240
packages/aris-core/reconciler.test.ts
Normal 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")
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
88
packages/aris-core/reconciler.ts
Normal file
88
packages/aris-core/reconciler.ts
Normal 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
158
scripts/setup-git.sh
Executable 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
6
scripts/setup-nvim.sh
Executable 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
29
tsconfig.json
Normal 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user