mirror of
https://github.com/kennethnym/aris.git
synced 2026-02-02 05:01: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