{"id":50413551,"url":"https://github.com/devchitchat/chatopsjs","last_synced_at":"2026-05-31T05:00:44.631Z","repository":{"id":346871676,"uuid":"1191051202","full_name":"devchitchat/chatopsjs","owner":"devchitchat","description":"A Bun chatops framework.","archived":false,"fork":false,"pushed_at":"2026-04-09T04:55:54.000Z","size":836,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-26T17:09:22.444Z","etag":null,"topics":["bun","chatops"],"latest_commit_sha":null,"homepage":"https://devchitchat.github.io/chatopsjs/","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/devchitchat.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-03-24T21:51:02.000Z","updated_at":"2026-04-09T04:56:01.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/devchitchat/chatopsjs","commit_stats":null,"previous_names":["devchitchat/chatopsjs"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/devchitchat/chatopsjs","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/devchitchat%2Fchatopsjs","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/devchitchat%2Fchatopsjs/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/devchitchat%2Fchatopsjs/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/devchitchat%2Fchatopsjs/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/devchitchat","download_url":"https://codeload.github.com/devchitchat/chatopsjs/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/devchitchat%2Fchatopsjs/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33719601,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-05-31T02:00:06.040Z","response_time":95,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["bun","chatops"],"created_at":"2026-05-31T05:00:43.939Z","updated_at":"2026-05-31T05:00:44.616Z","avatar_url":"https://github.com/devchitchat.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# chatopsjs\n\n`chatopsjs` is a command-first chatbot framework for Slack, Discord, Teams, CLI, and future adapters. It is designed as a modern successor to Hubot for teams that want deterministic command execution, auditable behavior, strong plugin boundaries, and adapter-specific richness without collapsing everything into a lowest-common-denominator messaging API.\n\nThe framework bets on commands as the primary unit of behavior:\n\n- canonical command IDs like `tickets.create`\n- declarative command schemas\n- permission checks before execution\n- confirmation flows for side-effecting operations\n- ordered async middleware\n- explicit adapter capabilities\n- safe default handling for unknown input\n- structured logs, lifecycle events, audit records, and correlation IDs\n\nThe current codebase includes a minimal kernel and test-backed command pipeline in JavaScript with ESM imports and no semicolons. The intended workflow is strict TDD: write a failing test, implement the smallest passing slice, then refactor.\n\n## Install\n\n```bash\nbun install\n```\n\n## Test\n\n```bash\nbun test\n```\n\n## Run The Program\n\n```bash\nbun run cli\n```\n\nThat starts the default `cli` adapter with a local command shell. Useful commands:\n\n```text\nhelp\ntickets.create --title \"Broken login\"\nyes\nalerts.ack\nexit\n```\n\nYou can choose the adapter explicitly:\n\n```bash\nbun run cli --adapter ./examples/discord/app.js\n```\n\nShort flag:\n\n```bash\nbun run cli -a ./examples/discord/app.js\n```\n\nThe launcher is [src/cli.js](/Users/joeyguerra/src/joeyguerra/chatopsjs/src/cli.js), the built-in default CLI path is [src/default-cli.js](/Users/joeyguerra/src/joeyguerra/chatopsjs/src/default-cli.js), and argument parsing plus external adapter loading are in [src/program.js](/Users/joeyguerra/src/joeyguerra/chatopsjs/src/program.js).\n\nThe contract is intentionally generic: `--adapter` takes any ESM module specifier, relative path, absolute path, or package name. The loaded module must export `runAdapterApp`. If `--adapter` is omitted, the program does not go through the adapter loader and runs the built-in CLI directly.\n\nModules are autoloaded from the top-level [modules/](/Users/joeyguerra/src/joeyguerra/chatopsjs/modules) directory by the shared runtime bootstrap in [src/default-runtime.js](/Users/joeyguerra/src/joeyguerra/chatopsjs/src/default-runtime.js). Each module should export either a command or a plugin contract.\n\n## Discord Adapter Example\n\nA runnable Discord example lives in [examples/discord/app.js](/Users/joeyguerra/src/joeyguerra/chatopsjs/examples/discord/app.js).\n\nInstall dependencies, set a bot token, then start it:\n\n```bash\nbun install\nDISCORD_TOKEN=your-token bun run cli --adapter ./examples/discord/app.js\n```\n\nThe adapter surface itself is implemented in [examples/discord/adapter.js](/Users/joeyguerra/src/joeyguerra/chatopsjs/examples/discord/adapter.js). It follows the framework design rules:\n\n- normalize inbound Discord messages into framework events\n- declare Discord capabilities explicitly\n- render portable responses into Discord `content` and basic embeds\n- allow Discord-native payloads through `createNativeResponse({ provider: 'discord', ... })`\n\nThe example runtime is in [examples/discord/runtime.js](/Users/joeyguerra/src/joeyguerra/chatopsjs/examples/discord/runtime.js), and it reuses the same shared `modules/` autoload model as the built-in CLI. Usage notes are in [examples/discord/README.md](/Users/joeyguerra/src/joeyguerra/chatopsjs/examples/discord/README.md).\n\n## Bun REPL\n\nThis does not embed `bun repl` directly.\n\n`bun repl` is a JavaScript evaluator, not a command-shell API for hosting a ChatOps loop, so using it as the transport would blur command input with JS execution and weaken determinism. The better fit is a dedicated readline-based CLI adapter running on Bun, which is what this repo now uses.\n\nIf you want REPL-like ergonomics later, the next step is to add shell history, tab completion, and command introspection on top of the CLI adapter rather than delegating command handling to the JavaScript REPL.\n\n## Architecture Overview\n\nThe system is split into five layers:\n\n1. **Kernel / runtime**\n   Owns lifecycle, middleware execution, command dispatch, correlation IDs, audit records, storage handles, and logging hooks.\n2. **Adapters**\n   Normalize inbound provider events into framework events and deliver outbound responses. Adapters stay thin and declare capabilities explicitly.\n3. **Command bus**\n   Registers commands, resolves aliases, parses arguments, validates input, authorizes actors, enforces confirmations, executes handlers, and exposes discovery/help metadata.\n4. **Plugins**\n   Package commands, middleware, setup hooks, and future provider integrations as isolated modules.\n5. **Storage and state**\n   Separates ephemeral per-invocation state from pluggable durable storage used for workflows, approvals, tokens, or cache.\n\nThis keeps the runtime deterministic. Adapters only translate. Commands only express business behavior. Middleware only augments the execution pipeline in ordered, testable steps.\n\n## Core Components And Responsibilities\n\n### `createRuntime`\n\nThe central kernel. Responsibilities:\n\n- accept adapter registrations and capability declarations\n- register commands and plugins\n- execute ordered async middleware\n- construct per-event execution context\n- validate and authorize commands\n- enforce confirmation policy\n- emit structured lifecycle logs\n- produce audit records and correlation IDs\n\n### `createCommand`\n\nDefines a command contract:\n\n- `id`: canonical identifier such as `tickets.create`\n- `aliases`: optional alternate invocations\n- `description`: human-readable discovery text\n- `args`: declarative argument schema\n- `permissions`: required grants\n- `confirm`: confirmation policy for side effects\n- `execute(ctx)`: command handler\n\n### Response Builders\n\nOutbound responses are intentionally layered:\n\n- `createTextResponse(text)` for portable plain text\n- `createMessageResponse({...})` for framework-defined structured message IR plus fallback text\n- `createNativeResponse({...})` for adapter-native escape hatches\n\n### `definePlugin`\n\nDefines a plugin bundle:\n\n- `name`\n- `version`\n- `commands`\n- `middleware`\n- `setup(runtime)`\n\n### Storage Model\n\nThe runtime owns two state scopes:\n\n- `ctx.state`: ephemeral per-command state, safe for middleware coordination\n- `ctx.storage`: pluggable durable storage, used for cross-command state and long-lived workflows\n\n## Command Lifecycle\n\nEach inbound event follows the same pipeline:\n\n1. Adapter normalizes provider-specific input into a framework event.\n2. Runtime assigns or propagates a correlation ID.\n3. Command bus parses the input and resolves the canonical command ID from aliases.\n4. Declarative argument validation runs.\n5. Permission checks run against actor grants.\n6. Confirmation policy runs for side-effecting commands.\n7. Ordered async middleware executes around the handler.\n8. Command executes and returns portable, structured, or native output.\n9. Runtime applies adapter capability filtering.\n10. Logs, audit records, and delivery metadata are finalized.\n\nSafe default behavior matters: unknown input never falls through to ad hoc natural-language behavior. It returns an explicit unknown-command response with a path to help/discovery.\n\n## Output And Rendering Model\n\nThe outbound model is layered, not flattened:\n\n### 1. Portable response primitives\n\nThe lowest layer is adapter-portable intent: text, notices, confirmations, status lines, and similar simple primitives.\n\n### 2. Framework message IR\n\nThe middle layer is a structured message shape defined by the framework. This can represent sections, fields, actions, metadata, fallback text, and future portable composition rules.\n\n### 3. Adapter-native payloads\n\nThe top layer is an explicit escape hatch for provider-specific richness:\n\n- Slack Block Kit\n- Discord embeds and components\n- Teams cards\n- threads\n- ephemeral replies\n- edits\n- reactions\n\nThe rule is simple: use portable output by default, structured IR when the framework can model the intent, and native payloads when the provider has valuable features the framework should not erase.\n\n### Example Structured Response\n\n```js\ncreateMessageResponse({\n  fallbackText: 'Created ticket Broken login',\n  blocks: [\n    {\n      type: 'section',\n      text: 'Created ticket Broken login'\n    },\n    {\n      type: 'facts',\n      items: [\n        { label: 'Priority', value: 'high' },\n        { label: 'Owner', value: 'infra-oncall' }\n      ]\n    }\n  ]\n})\n```\n\n### Example Native Adapter Response\n\n```js\ncreateNativeResponse({\n  fallbackText: 'Alert acknowledged',\n  provider: 'slack',\n  payload: {\n    blocks: [\n      {\n        type: 'section',\n        text: {\n          type: 'mrkdwn',\n          text: '*Alert acknowledged*'\n        }\n      }\n    ]\n  }\n})\n```\n\nIf the active adapter does not declare support for the native payload provider, the runtime drops the native payload and falls back to `fallbackText`.\n\n## Adapter Capability Model\n\nAdapters declare capabilities explicitly. Do not infer them from adapter names.\n\nExample capability declaration:\n\n```js\nconst slackAdapter = {\n  name: 'slack',\n  capabilities: {\n    nativePayload: ['slack'],\n    threads: true,\n    ephemeralReplies: true,\n    edits: true,\n    reactions: true\n  }\n}\n```\n\nThis makes rendering and delivery predictable:\n\n- the runtime knows when native output is allowed\n- commands can branch on explicit capabilities instead of provider guessing\n- tests can assert behavior against capability flags, not integration side effects\n\n## Plugin Contract\n\nPlugins are the unit of extensibility. A plugin should package related commands, middleware, and bootstrapping without taking ownership of the entire runtime.\n\nExample contract:\n\n```js\ndefinePlugin({\n  name: 'tickets',\n  version: '1.0.0',\n  commands: [\n    createCommand({\n      id: 'tickets.list',\n      description: 'List tickets',\n      execute: async () =\u003e createTextResponse('No tickets')\n    })\n  ],\n  middleware: [\n    async (ctx, next) =\u003e {\n      ctx.state.plugin = 'tickets'\n      await next()\n    }\n  ],\n  setup(runtime) {\n    runtime.use(async (ctx, next) =\u003e {\n      ctx.state.startedAt = Date.now()\n      await next()\n    })\n  }\n})\n```\n\nPlugin design rules:\n\n- commands must use canonical IDs\n- middleware execution order is explicit\n- plugins may add setup hooks but should not mutate runtime internals directly\n- durable storage access should happen through `ctx.storage`, not hidden globals\n\n## Example Folder Structure\n\nAn implementation-friendly structure for the next phase:\n\n```text\nchatopsjs/\n  modules/\n    tickets.create.js\n    tickets.list.js\n    alerts.ack.js\n  src/\n    index.js\n    kernel/\n      runtime.js\n      lifecycle.js\n      middleware.js\n    commands/\n      registry.js\n      parser.js\n      validation.js\n      authorization.js\n      confirmation.js\n      discovery.js\n    responses/\n      primitives.js\n      message-ir.js\n      native.js\n      renderer.js\n    adapters/\n      base.js\n      slack/\n        inbound.js\n        outbound.js\n      discord/\n        inbound.js\n        outbound.js\n      teams/\n        inbound.js\n        outbound.js\n      cli/\n        inbound.js\n        outbound.js\n    plugins/\n      loader.js\n    storage/\n      memory.js\n      sqlite.js\n      redis.js\n    observability/\n      logger.js\n      audit.js\n      correlation.js\n  test/\n    runtime.test.js\n    command-bus.test.js\n    adapters.test.js\n    plugins.test.js\n```\n\nThe current repo implements a smaller subset under `src/lib/`, but the structure above is the natural direction once the prototype grows.\n\n## Example Command\n\n```js\nimport { createCommand, createMessageResponse } from '@joeyguerra/chatopsjs'\n\nexport const createTicketCommand = createCommand({\n  id: 'tickets.create',\n  aliases: ['ticket.create'],\n  description: 'Create a support ticket',\n  args: {\n    title: { type: 'string', required: true },\n    priority: { type: 'string', required: false }\n  },\n  permissions: ['tickets:write'],\n  confirm: {\n    mode: 'required',\n    message: 'Create ticket?'\n  },\n  async execute(ctx) {\n    const priority = ctx.args.priority ?? 'normal'\n\n    await ctx.storage.set(`ticket:${ctx.args.title}`, {\n      title: ctx.args.title,\n      priority,\n      requestedBy: ctx.event.actor.id\n    })\n\n    return createMessageResponse({\n      fallbackText: `Created ticket ${ctx.args.title}`,\n      blocks: [\n        {\n          type: 'section',\n          text: `Created ticket ${ctx.args.title}`\n        },\n        {\n          type: 'facts',\n          items: [\n            { label: 'Priority', value: priority },\n            { label: 'Requested by', value: ctx.event.actor.id }\n          ]\n        }\n      ]\n    })\n  }\n})\n```\n\n## Code Example\n\n```js\nimport {\n  createCommand,\n  createMessageResponse,\n  createNativeResponse,\n  createRuntime,\n  definePlugin\n} from './src/index.js'\n\nconst runtime = createRuntime({\n  adapters: [\n    {\n      name: 'slack',\n      capabilities: {\n        nativePayload: ['slack'],\n        threads: true,\n        ephemeralReplies: true\n      }\n    },\n    {\n      name: 'cli',\n      capabilities: {\n        nativePayload: []\n      }\n    }\n  ]\n})\n\nruntime.use(async (ctx, next) =\u003e {\n  ctx.state.startedAt = Date.now()\n  await next()\n})\n\nruntime.loadPlugin(definePlugin({\n  name: 'alerts',\n  version: '1.0.0',\n  commands: [\n    createCommand({\n      id: 'alerts.ack',\n      description: 'Acknowledge an alert',\n      async execute(ctx) {\n        if (ctx.adapter.capabilities.nativePayload?.includes('slack')) {\n          return createNativeResponse({\n            fallbackText: 'Alert acknowledged',\n            provider: 'slack',\n            payload: {\n              blocks: [\n                {\n                  type: 'section',\n                  text: {\n                    type: 'mrkdwn',\n                    text: '*Alert acknowledged*'\n                  }\n                }\n              ]\n            }\n          })\n        }\n\n        return createMessageResponse({\n          fallbackText: 'Alert acknowledged',\n          blocks: [\n            {\n              type: 'section',\n              text: 'Alert acknowledged'\n            }\n          ]\n        })\n      }\n    })\n  ]\n}))\n```\n\n## Rationale For Major Design Decisions\n\n### Command-first instead of ambient listeners\n\nHubot made arbitrary chat listeners easy, but that flexibility often reduced determinism and safety. A command bus gives clear ownership, stable discoverability, and auditable execution paths.\n\n### Canonical IDs over free-form names\n\nCanonical IDs like `tickets.create` make aliasing, permissions, analytics, and plugin composition much cleaner than stringly typed pattern handlers.\n\n### Thin adapters\n\nProvider integrations should translate, not own business logic. That keeps tests fast and portability realistic.\n\n### Layered outbound model\n\nA pure lowest-common-denominator API throws away valuable provider features. A pure provider-native model destroys portability. The layered model keeps both.\n\n### Explicit capabilities\n\nCapability flags are operationally safer than implicit provider branching. They make behavior visible, testable, and easier to evolve.\n\n### Confirmation as a first-class concern\n\nSide-effecting commands should not reinvent confirmation flows ad hoc. The runtime should enforce them consistently.\n\n### Ephemeral state plus durable storage\n\nPer-invocation state is useful for middleware composition, but long-lived workflow state needs a pluggable store. Treating them separately keeps handlers easier to reason about.\n\n### Logs, lifecycle, and audit records built into the kernel\n\nOperational safety is not optional in ChatOps systems. Correlation IDs, lifecycle logs, and audit trails need to be part of the core execution model, not bolted on later.\n\n## Current Status\n\nThe repo currently includes:\n\n- a test-backed runtime skeleton in [src/index.js](/Users/joeyguerra/src/joeyguerra/chatopsjs/src/index.js)\n- a command and plugin API under [src/lib](/Users/joeyguerra/src/joeyguerra/chatopsjs/src/lib)\n- TDD coverage for command execution, alias resolution, permission checks, confirmation flow, plugin loading, safe unknown-command behavior, and adapter-native fallback in [test/chatops.test.js](/Users/joeyguerra/src/joeyguerra/chatopsjs/test/chatops.test.js)\n\nThe next logical build-out is to split the current runtime into dedicated kernel, parser, authorization, confirmation, renderer, and adapter packages while preserving the same command-first contract.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdevchitchat%2Fchatopsjs","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdevchitchat%2Fchatopsjs","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdevchitchat%2Fchatopsjs/lists"}