@cmetech/otto 1.1.0 → 1.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/coworker/persona-commands.d.ts +1 -0
- package/dist/coworker/persona-commands.js +5 -0
- package/dist/coworker/persona-commands.test.d.ts +1 -0
- package/dist/coworker/persona-commands.test.js +45 -0
- package/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/_coworker-paths.js +8 -0
- package/dist/resources/extensions/coworker-artifacts/artifacts-command.js +31 -0
- package/dist/resources/extensions/coworker-artifacts/artifacts-singleton.js +17 -0
- package/dist/resources/extensions/coworker-artifacts/extension-manifest.json +13 -0
- package/dist/resources/extensions/coworker-artifacts/index.js +125 -0
- package/dist/resources/extensions/coworker-artifacts/list-tool.js +27 -0
- package/dist/resources/extensions/coworker-artifacts/open-tool.js +25 -0
- package/dist/resources/extensions/coworker-memory/extension-manifest.json +13 -0
- package/dist/resources/extensions/coworker-memory/index.js +219 -0
- package/dist/resources/extensions/coworker-memory/memorize-tool.js +10 -0
- package/dist/resources/extensions/coworker-memory/memory-command.js +157 -0
- package/dist/resources/extensions/coworker-memory/memory-singleton.js +55 -0
- package/dist/resources/extensions/coworker-memory/recall-tool.js +18 -0
- package/dist/resources/extensions/coworker-memory/session-hooks.js +45 -0
- package/dist/resources/extensions/coworker-scratchpad/attach-banners.js +53 -0
- package/dist/resources/extensions/coworker-scratchpad/extension-manifest.json +13 -0
- package/dist/resources/extensions/coworker-scratchpad/format-age.js +9 -0
- package/dist/resources/extensions/coworker-scratchpad/helpers.js +38 -0
- package/dist/resources/extensions/coworker-scratchpad/index.js +199 -0
- package/dist/resources/extensions/coworker-scratchpad/mime-bundle.js +20 -0
- package/dist/resources/extensions/coworker-scratchpad/scratchpad-tool.js +118 -0
- package/dist/resources/extensions/coworker-scratchpad/session-sidecar.js +60 -0
- package/dist/resources/extensions/coworker-scratchpad/sp-command.js +597 -0
- package/dist/resources/extensions/coworker-scratchpad/workspace-pointer.js +41 -0
- package/dist/resources/extensions/coworker-scratchpad/workspace-root.js +17 -0
- package/dist/resources/extensions/coworker-vault/audit-command.js +35 -0
- package/dist/resources/extensions/coworker-vault/connect-command.js +42 -0
- package/dist/resources/extensions/coworker-vault/datasource-command.js +50 -0
- package/dist/resources/extensions/coworker-vault/extension-manifest.json +12 -0
- package/dist/resources/extensions/coworker-vault/index.js +171 -0
- package/dist/resources/extensions/coworker-vault/test-helpers.js +86 -0
- package/dist/resources/extensions/coworker-vault/vault-singleton.js +24 -0
- package/dist/resources/extensions/otto/commands/release-notes/_data.js +82 -0
- package/dist/resources/extensions/otto/commands/release-notes/command.js +15 -4
- package/dist/resources/extensions/subagent/index.js +8 -1
- package/dist/resources/extensions/subagent/launch.js +37 -5
- package/dist/resources/extensions/subagent/run-store.js +1 -0
- package/dist/resources/extensions/workflow/bootstrap/register-extension.js +2 -0
- package/dist/resources/extensions/workflow/bootstrap/register-hooks.js +10 -0
- package/dist/resources/extensions/workflow/persona-status.js +87 -0
- package/dist/update-cmd.d.ts +19 -0
- package/dist/update-cmd.js +177 -6
- package/package.json +25 -10
- package/packages/contracts/package.json +1 -1
- package/packages/coworker-artifacts/dist/artifact-store.d.ts +25 -0
- package/packages/coworker-artifacts/dist/artifact-store.js +187 -0
- package/packages/coworker-artifacts/dist/dir-snapshot.d.ts +7 -0
- package/packages/coworker-artifacts/dist/dir-snapshot.js +54 -0
- package/packages/coworker-artifacts/dist/errors.d.ts +18 -0
- package/packages/coworker-artifacts/dist/errors.js +37 -0
- package/packages/coworker-artifacts/dist/index.d.ts +7 -0
- package/packages/coworker-artifacts/dist/index.js +7 -0
- package/packages/coworker-artifacts/dist/readme-renderer.d.ts +5 -0
- package/packages/coworker-artifacts/dist/readme-renderer.js +47 -0
- package/packages/coworker-artifacts/dist/resolve-uri.d.ts +3 -0
- package/packages/coworker-artifacts/dist/resolve-uri.js +29 -0
- package/packages/coworker-artifacts/dist/slug.d.ts +4 -0
- package/packages/coworker-artifacts/dist/slug.js +32 -0
- package/packages/coworker-artifacts/dist/types.d.ts +52 -0
- package/packages/coworker-artifacts/dist/types.js +1 -0
- package/packages/coworker-artifacts/package.json +20 -0
- package/packages/coworker-artifacts/src/artifact-store.test.ts +188 -0
- package/packages/coworker-artifacts/src/artifact-store.ts +206 -0
- package/packages/coworker-artifacts/src/artifacts-integration.test.ts +109 -0
- package/packages/coworker-artifacts/src/dir-snapshot.test.ts +71 -0
- package/packages/coworker-artifacts/src/dir-snapshot.ts +52 -0
- package/packages/coworker-artifacts/src/errors.test.ts +37 -0
- package/packages/coworker-artifacts/src/errors.ts +28 -0
- package/packages/coworker-artifacts/src/index.test.ts +22 -0
- package/packages/coworker-artifacts/src/index.ts +7 -0
- package/packages/coworker-artifacts/src/readme-renderer.test.ts +72 -0
- package/packages/coworker-artifacts/src/readme-renderer.ts +56 -0
- package/packages/coworker-artifacts/src/resolve-uri.test.ts +46 -0
- package/packages/coworker-artifacts/src/resolve-uri.ts +29 -0
- package/packages/coworker-artifacts/src/slug.test.ts +47 -0
- package/packages/coworker-artifacts/src/slug.ts +31 -0
- package/packages/coworker-artifacts/src/types.ts +61 -0
- package/packages/coworker-artifacts/tsconfig.json +15 -0
- package/packages/coworker-artifacts/tsconfig.publish.json +4 -0
- package/packages/coworker-memory/dist/context-injection.d.ts +9 -0
- package/packages/coworker-memory/dist/context-injection.js +41 -0
- package/packages/coworker-memory/dist/errors.d.ts +25 -0
- package/packages/coworker-memory/dist/errors.js +51 -0
- package/packages/coworker-memory/dist/index.d.ts +12 -0
- package/packages/coworker-memory/dist/index.js +12 -0
- package/packages/coworker-memory/dist/layer-a-store.d.ts +16 -0
- package/packages/coworker-memory/dist/layer-a-store.js +78 -0
- package/packages/coworker-memory/dist/local-sqlite-backend.d.ts +28 -0
- package/packages/coworker-memory/dist/local-sqlite-backend.js +167 -0
- package/packages/coworker-memory/dist/memory-backend.d.ts +14 -0
- package/packages/coworker-memory/dist/memory-backend.js +1 -0
- package/packages/coworker-memory/dist/memory-recorder.d.ts +50 -0
- package/packages/coworker-memory/dist/memory-recorder.js +69 -0
- package/packages/coworker-memory/dist/migrations/001-init.sql +38 -0
- package/packages/coworker-memory/dist/migrations/002-artifact-kind.sql +50 -0
- package/packages/coworker-memory/dist/paste-detector.d.ts +5 -0
- package/packages/coworker-memory/dist/paste-detector.js +14 -0
- package/packages/coworker-memory/dist/persona-seed.d.ts +10 -0
- package/packages/coworker-memory/dist/persona-seed.js +38 -0
- package/packages/coworker-memory/dist/recall-formatter.d.ts +2 -0
- package/packages/coworker-memory/dist/recall-formatter.js +14 -0
- package/packages/coworker-memory/dist/scope-resolver.d.ts +9 -0
- package/packages/coworker-memory/dist/scope-resolver.js +10 -0
- package/packages/coworker-memory/dist/types.d.ts +51 -0
- package/packages/coworker-memory/dist/types.js +2 -0
- package/packages/coworker-memory/dist/workspace-id.d.ts +3 -0
- package/packages/coworker-memory/dist/workspace-id.js +54 -0
- package/packages/coworker-memory/package.json +35 -0
- package/packages/coworker-memory/src/activator-integration.test.ts +141 -0
- package/packages/coworker-memory/src/context-injection.test.ts +72 -0
- package/packages/coworker-memory/src/context-injection.ts +57 -0
- package/packages/coworker-memory/src/errors.test.ts +45 -0
- package/packages/coworker-memory/src/errors.ts +42 -0
- package/packages/coworker-memory/src/index.test.ts +21 -0
- package/packages/coworker-memory/src/index.ts +12 -0
- package/packages/coworker-memory/src/layer-a-store.test.ts +85 -0
- package/packages/coworker-memory/src/layer-a-store.ts +88 -0
- package/packages/coworker-memory/src/local-sqlite-backend.test.ts +110 -0
- package/packages/coworker-memory/src/local-sqlite-backend.ts +185 -0
- package/packages/coworker-memory/src/memory-backend.ts +10 -0
- package/packages/coworker-memory/src/memory-integration.test.ts +89 -0
- package/packages/coworker-memory/src/memory-recorder.test.ts +101 -0
- package/packages/coworker-memory/src/memory-recorder.ts +95 -0
- package/packages/coworker-memory/src/migrations/001-init.sql +38 -0
- package/packages/coworker-memory/src/migrations/002-artifact-kind.sql +50 -0
- package/packages/coworker-memory/src/paste-detector.test.ts +23 -0
- package/packages/coworker-memory/src/paste-detector.ts +18 -0
- package/packages/coworker-memory/src/persona-seed.test.ts +57 -0
- package/packages/coworker-memory/src/persona-seed.ts +46 -0
- package/packages/coworker-memory/src/recall-formatter.test.ts +34 -0
- package/packages/coworker-memory/src/recall-formatter.ts +15 -0
- package/packages/coworker-memory/src/scope-resolver.test.ts +23 -0
- package/packages/coworker-memory/src/scope-resolver.ts +18 -0
- package/packages/coworker-memory/src/types.ts +61 -0
- package/packages/coworker-memory/src/workspace-id.test.ts +48 -0
- package/packages/coworker-memory/src/workspace-id.ts +56 -0
- package/packages/coworker-memory/tsconfig.json +15 -0
- package/packages/coworker-memory/tsconfig.publish.json +4 -0
- package/packages/coworker-persona/dist/commands.d.ts +7 -0
- package/packages/coworker-persona/dist/commands.js +35 -0
- package/packages/coworker-persona/dist/defaults/manifest.yaml +12 -0
- package/packages/coworker-persona/dist/defaults/steering/identity.md +3 -0
- package/packages/coworker-persona/dist/index.d.ts +3 -0
- package/packages/coworker-persona/dist/index.js +3 -0
- package/packages/coworker-persona/dist/manifest.d.ts +24 -0
- package/packages/coworker-persona/dist/manifest.js +21 -0
- package/packages/coworker-persona/dist/registry.d.ts +22 -0
- package/packages/coworker-persona/dist/registry.js +142 -0
- package/packages/coworker-persona/package.json +28 -0
- package/packages/coworker-persona/scripts/copy-defaults.cjs +17 -0
- package/packages/coworker-persona/src/commands.ts +47 -0
- package/packages/coworker-persona/src/defaults/manifest.yaml +12 -0
- package/packages/coworker-persona/src/defaults/steering/identity.md +3 -0
- package/packages/coworker-persona/src/index.ts +3 -0
- package/packages/coworker-persona/src/manifest.test.ts +67 -0
- package/packages/coworker-persona/src/manifest.ts +49 -0
- package/packages/coworker-persona/src/registry.test.ts +89 -0
- package/packages/coworker-persona/src/registry.ts +147 -0
- package/packages/coworker-persona/tsconfig.json +15 -0
- package/packages/coworker-persona/tsconfig.publish.json +4 -0
- package/packages/coworker-scratchpad/dist/cell-archive.d.ts +39 -0
- package/packages/coworker-scratchpad/dist/cell-archive.js +77 -0
- package/packages/coworker-scratchpad/dist/cell-tree.d.ts +14 -0
- package/packages/coworker-scratchpad/dist/cell-tree.js +72 -0
- package/packages/coworker-scratchpad/dist/child-process-runtime.d.ts +129 -0
- package/packages/coworker-scratchpad/dist/child-process-runtime.js +427 -0
- package/packages/coworker-scratchpad/dist/collector-registry.d.ts +12 -0
- package/packages/coworker-scratchpad/dist/collector-registry.js +29 -0
- package/packages/coworker-scratchpad/dist/detect-kind.d.ts +3 -0
- package/packages/coworker-scratchpad/dist/detect-kind.js +19 -0
- package/packages/coworker-scratchpad/dist/file-collector.d.ts +15 -0
- package/packages/coworker-scratchpad/dist/file-collector.js +99 -0
- package/packages/coworker-scratchpad/dist/index.d.ts +13 -0
- package/packages/coworker-scratchpad/dist/index.js +13 -0
- package/packages/coworker-scratchpad/dist/kernel-bindings.d.ts +49 -0
- package/packages/coworker-scratchpad/dist/kernel-bindings.js +220 -0
- package/packages/coworker-scratchpad/dist/kernel-entry.d.ts +1 -0
- package/packages/coworker-scratchpad/dist/kernel-entry.js +355 -0
- package/packages/coworker-scratchpad/dist/kernel-protocol.d.ts +171 -0
- package/packages/coworker-scratchpad/dist/kernel-protocol.js +48 -0
- package/packages/coworker-scratchpad/dist/kernel-spawn.d.ts +3 -0
- package/packages/coworker-scratchpad/dist/kernel-spawn.js +54 -0
- package/packages/coworker-scratchpad/dist/namespace-codec.d.ts +22 -0
- package/packages/coworker-scratchpad/dist/namespace-codec.js +61 -0
- package/packages/coworker-scratchpad/dist/scratchpad-lock.d.ts +24 -0
- package/packages/coworker-scratchpad/dist/scratchpad-lock.js +86 -0
- package/packages/coworker-scratchpad/dist/scratchpad-manager.d.ts +193 -0
- package/packages/coworker-scratchpad/dist/scratchpad-manager.js +866 -0
- package/packages/coworker-scratchpad/dist/staleness-banner.d.ts +12 -0
- package/packages/coworker-scratchpad/dist/staleness-banner.js +27 -0
- package/packages/coworker-scratchpad/package.json +31 -0
- package/packages/coworker-scratchpad/src/cell-archive.test.ts +150 -0
- package/packages/coworker-scratchpad/src/cell-archive.ts +97 -0
- package/packages/coworker-scratchpad/src/cell-tree.test.ts +105 -0
- package/packages/coworker-scratchpad/src/cell-tree.ts +90 -0
- package/packages/coworker-scratchpad/src/child-process-runtime.test.ts +413 -0
- package/packages/coworker-scratchpad/src/child-process-runtime.ts +493 -0
- package/packages/coworker-scratchpad/src/collector-registry.test.ts +69 -0
- package/packages/coworker-scratchpad/src/collector-registry.ts +33 -0
- package/packages/coworker-scratchpad/src/detect-kind.test.ts +33 -0
- package/packages/coworker-scratchpad/src/detect-kind.ts +22 -0
- package/packages/coworker-scratchpad/src/file-collector.test.ts +109 -0
- package/packages/coworker-scratchpad/src/file-collector.ts +114 -0
- package/packages/coworker-scratchpad/src/index.ts +74 -0
- package/packages/coworker-scratchpad/src/kernel-bindings.test.ts +188 -0
- package/packages/coworker-scratchpad/src/kernel-bindings.ts +279 -0
- package/packages/coworker-scratchpad/src/kernel-entry.test.ts +123 -0
- package/packages/coworker-scratchpad/src/kernel-entry.ts +390 -0
- package/packages/coworker-scratchpad/src/kernel-protocol.test.ts +105 -0
- package/packages/coworker-scratchpad/src/kernel-protocol.ts +230 -0
- package/packages/coworker-scratchpad/src/kernel-spawn.test.ts +60 -0
- package/packages/coworker-scratchpad/src/kernel-spawn.ts +54 -0
- package/packages/coworker-scratchpad/src/namespace-codec.test.ts +102 -0
- package/packages/coworker-scratchpad/src/namespace-codec.ts +90 -0
- package/packages/coworker-scratchpad/src/scratchpad-lock.test.ts +98 -0
- package/packages/coworker-scratchpad/src/scratchpad-lock.ts +102 -0
- package/packages/coworker-scratchpad/src/scratchpad-manager.test.ts +1343 -0
- package/packages/coworker-scratchpad/src/scratchpad-manager.ts +891 -0
- package/packages/coworker-scratchpad/src/staleness-banner.test.ts +53 -0
- package/packages/coworker-scratchpad/src/staleness-banner.ts +33 -0
- package/packages/coworker-scratchpad/src/vault-integration.test.ts +221 -0
- package/packages/coworker-scratchpad/tsconfig.json +15 -0
- package/packages/coworker-scratchpad/tsconfig.publish.json +4 -0
- package/packages/coworker-types/dist/artifacts.d.ts +31 -0
- package/packages/coworker-types/dist/artifacts.js +2 -0
- package/packages/coworker-types/dist/contracts.d.ts +32 -0
- package/packages/coworker-types/dist/contracts.js +1 -0
- package/packages/coworker-types/dist/index.d.ts +5 -0
- package/packages/coworker-types/dist/index.js +5 -0
- package/packages/coworker-types/dist/memory.d.ts +61 -0
- package/packages/coworker-types/dist/memory.js +3 -0
- package/packages/coworker-types/dist/scratchpad.d.ts +43 -0
- package/packages/coworker-types/dist/scratchpad.js +2 -0
- package/packages/coworker-types/dist/vault.d.ts +34 -0
- package/packages/coworker-types/dist/vault.js +2 -0
- package/packages/coworker-types/package.json +24 -0
- package/packages/coworker-types/src/artifacts.test.ts +52 -0
- package/packages/coworker-types/src/artifacts.ts +35 -0
- package/packages/coworker-types/src/contracts.test.ts +43 -0
- package/packages/coworker-types/src/contracts.ts +36 -0
- package/packages/coworker-types/src/index.ts +5 -0
- package/packages/coworker-types/src/memory.test.ts +50 -0
- package/packages/coworker-types/src/memory.ts +79 -0
- package/packages/coworker-types/src/scratchpad.test.ts +46 -0
- package/packages/coworker-types/src/scratchpad.ts +51 -0
- package/packages/coworker-types/src/smoke.test.ts +34 -0
- package/packages/coworker-types/src/vault.test.ts +49 -0
- package/packages/coworker-types/src/vault.ts +40 -0
- package/packages/coworker-types/tsconfig.json +15 -0
- package/packages/coworker-types/tsconfig.publish.json +4 -0
- package/packages/coworker-utils/dist/audit-log.d.ts +34 -0
- package/packages/coworker-utils/dist/audit-log.js +88 -0
- package/packages/coworker-utils/dist/index.d.ts +6 -0
- package/packages/coworker-utils/dist/index.js +6 -0
- package/packages/coworker-utils/dist/lease.d.ts +7 -0
- package/packages/coworker-utils/dist/lease.js +67 -0
- package/packages/coworker-utils/dist/logger.d.ts +13 -0
- package/packages/coworker-utils/dist/logger.js +26 -0
- package/packages/coworker-utils/dist/migration-runner.d.ts +7 -0
- package/packages/coworker-utils/dist/migration-runner.js +36 -0
- package/packages/coworker-utils/dist/ndjson-channel.d.ts +3 -0
- package/packages/coworker-utils/dist/ndjson-channel.js +38 -0
- package/packages/coworker-utils/dist/secret-scanner.d.ts +10 -0
- package/packages/coworker-utils/dist/secret-scanner.js +42 -0
- package/packages/coworker-utils/package.json +24 -0
- package/packages/coworker-utils/src/audit-log.test.ts +140 -0
- package/packages/coworker-utils/src/audit-log.ts +107 -0
- package/packages/coworker-utils/src/index.ts +6 -0
- package/packages/coworker-utils/src/lease.test.ts +64 -0
- package/packages/coworker-utils/src/lease.ts +76 -0
- package/packages/coworker-utils/src/logger.test.ts +50 -0
- package/packages/coworker-utils/src/logger.ts +45 -0
- package/packages/coworker-utils/src/migration-runner.test.ts +65 -0
- package/packages/coworker-utils/src/migration-runner.ts +50 -0
- package/packages/coworker-utils/src/ndjson-channel.test.ts +76 -0
- package/packages/coworker-utils/src/ndjson-channel.ts +41 -0
- package/packages/coworker-utils/src/secret-scanner.test.ts +61 -0
- package/packages/coworker-utils/src/secret-scanner.ts +56 -0
- package/packages/coworker-utils/tsconfig.json +15 -0
- package/packages/coworker-utils/tsconfig.publish.json +4 -0
- package/packages/coworker-vault/dist/data-vault.d.ts +41 -0
- package/packages/coworker-vault/dist/data-vault.js +223 -0
- package/packages/coworker-vault/dist/engine-registry.d.ts +34 -0
- package/packages/coworker-vault/dist/engine-registry.js +90 -0
- package/packages/coworker-vault/dist/engines/jira.yaml +17 -0
- package/packages/coworker-vault/dist/errors.d.ts +28 -0
- package/packages/coworker-vault/dist/errors.js +57 -0
- package/packages/coworker-vault/dist/index.d.ts +6 -0
- package/packages/coworker-vault/dist/index.js +6 -0
- package/packages/coworker-vault/dist/injector.d.ts +19 -0
- package/packages/coworker-vault/dist/injector.js +77 -0
- package/packages/coworker-vault/dist/types.d.ts +28 -0
- package/packages/coworker-vault/dist/types.js +1 -0
- package/packages/coworker-vault/dist/vault-keep.d.ts +4 -0
- package/packages/coworker-vault/dist/vault-keep.js +21 -0
- package/packages/coworker-vault/package.json +29 -0
- package/packages/coworker-vault/src/data-vault.test.ts +199 -0
- package/packages/coworker-vault/src/data-vault.ts +257 -0
- package/packages/coworker-vault/src/engine-registry.test.ts +120 -0
- package/packages/coworker-vault/src/engine-registry.ts +107 -0
- package/packages/coworker-vault/src/engines/jira.yaml +17 -0
- package/packages/coworker-vault/src/errors.test.ts +58 -0
- package/packages/coworker-vault/src/errors.ts +50 -0
- package/packages/coworker-vault/src/index.test.ts +24 -0
- package/packages/coworker-vault/src/index.ts +6 -0
- package/packages/coworker-vault/src/injector.test.ts +109 -0
- package/packages/coworker-vault/src/injector.ts +98 -0
- package/packages/coworker-vault/src/types.ts +33 -0
- package/packages/coworker-vault/src/vault-keep.test.ts +49 -0
- package/packages/coworker-vault/src/vault-keep.ts +31 -0
- package/packages/coworker-vault/tsconfig.json +15 -0
- package/packages/coworker-vault/tsconfig.publish.json +4 -0
- package/packages/daemon/package.json +3 -3
- package/packages/mcp-server/package.json +3 -3
- package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
- package/packages/native/package.json +1 -1
- package/packages/native/tsconfig.tsbuildinfo +1 -1
- package/packages/pi-agent-core/package.json +1 -1
- package/packages/pi-agent-core/tsconfig.tsbuildinfo +1 -1
- package/packages/pi-ai/package.json +1 -1
- package/packages/pi-ai/tsconfig.tsbuildinfo +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts +6 -1
- package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/runner.js +22 -3
- package/packages/pi-coding-agent/dist/core/extensions/runner.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/resolve-config-value.test.js +11 -0
- package/packages/pi-coding-agent/dist/core/resolve-config-value.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/rpc/raw-stdout.d.ts +47 -0
- package/packages/pi-coding-agent/dist/modes/rpc/raw-stdout.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/modes/rpc/raw-stdout.js +107 -0
- package/packages/pi-coding-agent/dist/modes/rpc/raw-stdout.js.map +1 -0
- package/packages/pi-coding-agent/dist/modes/rpc/raw-stdout.regression.test.d.ts +19 -0
- package/packages/pi-coding-agent/dist/modes/rpc/raw-stdout.regression.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/modes/rpc/raw-stdout.regression.test.js +121 -0
- package/packages/pi-coding-agent/dist/modes/rpc/raw-stdout.regression.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.js +17 -1
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/packages/pi-coding-agent/package.json +2 -2
- package/packages/pi-coding-agent/src/core/extensions/runner.ts +22 -3
- package/packages/pi-coding-agent/src/core/resolve-config-value.test.ts +11 -0
- package/packages/pi-coding-agent/src/modes/rpc/raw-stdout.regression.test.ts +129 -0
- package/packages/pi-coding-agent/src/modes/rpc/raw-stdout.ts +117 -0
- package/packages/pi-coding-agent/src/modes/rpc/rpc-mode.ts +18 -1
- package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
- package/packages/pi-tui/package.json +1 -1
- package/packages/pi-tui/tsconfig.tsbuildinfo +1 -1
- package/packages/rpc-client/package.json +2 -2
- package/packages/rpc-client/tsconfig.tsbuildinfo +1 -1
- package/pkg/package.json +1 -1
- package/scripts/install.js +6 -5
- package/src/resources/extensions/_coworker-paths.test.ts +40 -0
- package/src/resources/extensions/_coworker-paths.ts +10 -0
- package/src/resources/extensions/coworker-artifacts/artifacts-command.test.ts +54 -0
- package/src/resources/extensions/coworker-artifacts/artifacts-command.ts +43 -0
- package/src/resources/extensions/coworker-artifacts/artifacts-singleton.test.ts +25 -0
- package/src/resources/extensions/coworker-artifacts/artifacts-singleton.ts +29 -0
- package/src/resources/extensions/coworker-artifacts/extension-manifest.json +13 -0
- package/src/resources/extensions/coworker-artifacts/index.test.ts +46 -0
- package/src/resources/extensions/coworker-artifacts/index.ts +154 -0
- package/src/resources/extensions/coworker-artifacts/list-tool.test.ts +29 -0
- package/src/resources/extensions/coworker-artifacts/list-tool.ts +53 -0
- package/src/resources/extensions/coworker-artifacts/open-tool.test.ts +30 -0
- package/src/resources/extensions/coworker-artifacts/open-tool.ts +43 -0
- package/src/resources/extensions/coworker-memory/extension-manifest.json +13 -0
- package/src/resources/extensions/coworker-memory/index.test.ts +137 -0
- package/src/resources/extensions/coworker-memory/index.ts +257 -0
- package/src/resources/extensions/coworker-memory/memorize-tool.test.ts +41 -0
- package/src/resources/extensions/coworker-memory/memorize-tool.ts +20 -0
- package/src/resources/extensions/coworker-memory/memory-command.test.ts +134 -0
- package/src/resources/extensions/coworker-memory/memory-command.ts +131 -0
- package/src/resources/extensions/coworker-memory/memory-singleton.test.ts +41 -0
- package/src/resources/extensions/coworker-memory/memory-singleton.ts +89 -0
- package/src/resources/extensions/coworker-memory/recall-tool.test.ts +50 -0
- package/src/resources/extensions/coworker-memory/recall-tool.ts +35 -0
- package/src/resources/extensions/coworker-memory/session-hooks.test.ts +77 -0
- package/src/resources/extensions/coworker-memory/session-hooks.ts +61 -0
- package/src/resources/extensions/coworker-scratchpad/attach-banners.test.ts +124 -0
- package/src/resources/extensions/coworker-scratchpad/attach-banners.ts +67 -0
- package/src/resources/extensions/coworker-scratchpad/extension-manifest.json +13 -0
- package/src/resources/extensions/coworker-scratchpad/format-age.test.ts +30 -0
- package/src/resources/extensions/coworker-scratchpad/format-age.ts +6 -0
- package/src/resources/extensions/coworker-scratchpad/helpers.test.ts +93 -0
- package/src/resources/extensions/coworker-scratchpad/helpers.ts +42 -0
- package/src/resources/extensions/coworker-scratchpad/index.test.ts +514 -0
- package/src/resources/extensions/coworker-scratchpad/index.ts +207 -0
- package/src/resources/extensions/coworker-scratchpad/mime-bundle.test.ts +61 -0
- package/src/resources/extensions/coworker-scratchpad/mime-bundle.ts +23 -0
- package/src/resources/extensions/coworker-scratchpad/scratchpad-tool.test.ts +137 -0
- package/src/resources/extensions/coworker-scratchpad/scratchpad-tool.ts +165 -0
- package/src/resources/extensions/coworker-scratchpad/session-sidecar.test.ts +133 -0
- package/src/resources/extensions/coworker-scratchpad/session-sidecar.ts +68 -0
- package/src/resources/extensions/coworker-scratchpad/sp-command.test.ts +836 -0
- package/src/resources/extensions/coworker-scratchpad/sp-command.ts +602 -0
- package/src/resources/extensions/coworker-scratchpad/workspace-pointer.test.ts +74 -0
- package/src/resources/extensions/coworker-scratchpad/workspace-pointer.ts +55 -0
- package/src/resources/extensions/coworker-scratchpad/workspace-root.test.ts +51 -0
- package/src/resources/extensions/coworker-scratchpad/workspace-root.ts +16 -0
- package/src/resources/extensions/coworker-vault/audit-command.test.ts +109 -0
- package/src/resources/extensions/coworker-vault/audit-command.ts +56 -0
- package/src/resources/extensions/coworker-vault/connect-command.test.ts +103 -0
- package/src/resources/extensions/coworker-vault/connect-command.ts +69 -0
- package/src/resources/extensions/coworker-vault/datasource-command.test.ts +80 -0
- package/src/resources/extensions/coworker-vault/datasource-command.ts +81 -0
- package/src/resources/extensions/coworker-vault/extension-manifest.json +12 -0
- package/src/resources/extensions/coworker-vault/index.test.ts +82 -0
- package/src/resources/extensions/coworker-vault/index.ts +181 -0
- package/src/resources/extensions/coworker-vault/test-helpers.ts +120 -0
- package/src/resources/extensions/coworker-vault/vault-singleton.test.ts +27 -0
- package/src/resources/extensions/coworker-vault/vault-singleton.ts +40 -0
- package/src/resources/extensions/otto/commands/release-notes/_data.ts +96 -0
- package/src/resources/extensions/otto/commands/release-notes/command.ts +16 -3
- package/src/resources/extensions/subagent/index.ts +9 -0
- package/src/resources/extensions/subagent/launch.test.ts +97 -0
- package/src/resources/extensions/subagent/launch.ts +42 -5
- package/src/resources/extensions/subagent/run-store.ts +3 -1
- package/src/resources/extensions/workflow/bootstrap/register-extension.ts +2 -0
- package/src/resources/extensions/workflow/bootstrap/register-hooks.ts +10 -0
- package/src/resources/extensions/workflow/persona-status.ts +109 -0
- package/src/resources/extensions/workflow/tests/auto-recovery.test.ts +34 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// Lease helper for global background tasks.
|
|
2
|
+
// Spec §6.4.
|
|
3
|
+
import * as fs from 'node:fs/promises';
|
|
4
|
+
import * as os from 'node:os';
|
|
5
|
+
function isExpired(data) {
|
|
6
|
+
const acquired = Date.parse(data.acquired_at);
|
|
7
|
+
if (Number.isNaN(acquired))
|
|
8
|
+
return true;
|
|
9
|
+
return Date.now() > acquired + data.ttl_ms;
|
|
10
|
+
}
|
|
11
|
+
function pidAlive(pid, host) {
|
|
12
|
+
// We can only check pids on the same host.
|
|
13
|
+
if (host !== os.hostname())
|
|
14
|
+
return true; // assume alive on other hosts
|
|
15
|
+
try {
|
|
16
|
+
process.kill(pid, 0);
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
async function readLease(path) {
|
|
24
|
+
try {
|
|
25
|
+
const raw = await fs.readFile(path, 'utf8');
|
|
26
|
+
return JSON.parse(raw);
|
|
27
|
+
}
|
|
28
|
+
catch (err) {
|
|
29
|
+
if (err.code === 'ENOENT')
|
|
30
|
+
return null;
|
|
31
|
+
throw err;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export async function acquireLease(path, opts) {
|
|
35
|
+
const existing = await readLease(path);
|
|
36
|
+
if (existing && !isExpired(existing) && pidAlive(existing.pid, existing.host)) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
const data = {
|
|
40
|
+
pid: process.pid,
|
|
41
|
+
host: os.hostname(),
|
|
42
|
+
acquired_at: new Date().toISOString(),
|
|
43
|
+
ttl_ms: opts.ttlMs,
|
|
44
|
+
holder: opts.holder,
|
|
45
|
+
};
|
|
46
|
+
await fs.writeFile(path, JSON.stringify(data, null, 2), { mode: 0o600 });
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
export async function releaseLease(path) {
|
|
50
|
+
try {
|
|
51
|
+
await fs.unlink(path);
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
if (err.code !== 'ENOENT')
|
|
55
|
+
throw err;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
export async function isLeaseHeld(path) {
|
|
59
|
+
const data = await readLease(path);
|
|
60
|
+
if (!data)
|
|
61
|
+
return false;
|
|
62
|
+
if (isExpired(data))
|
|
63
|
+
return false;
|
|
64
|
+
if (!pidAlive(data.pid, data.host))
|
|
65
|
+
return false;
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
|
|
2
|
+
export interface LoggerOptions {
|
|
3
|
+
level?: LogLevel;
|
|
4
|
+
sink?: (line: string) => void;
|
|
5
|
+
}
|
|
6
|
+
export interface Logger {
|
|
7
|
+
debug(msg: string, ctx?: Record<string, unknown>): void;
|
|
8
|
+
info(msg: string, ctx?: Record<string, unknown>): void;
|
|
9
|
+
warn(msg: string, ctx?: Record<string, unknown>): void;
|
|
10
|
+
error(msg: string, ctx?: Record<string, unknown>): void;
|
|
11
|
+
child(suffix: string): Logger;
|
|
12
|
+
}
|
|
13
|
+
export declare function createLogger(namespace: string, opts?: LoggerOptions): Logger;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Lightweight logger wrapper. Spec §6.7.
|
|
2
|
+
// Defers to a sink callable so otto-cli can wire it to the existing logger
|
|
3
|
+
// without coworker-utils taking a direct dependency on otto-cli.
|
|
4
|
+
const LEVEL_ORDER = { debug: 0, info: 1, warn: 2, error: 3 };
|
|
5
|
+
const DEFAULT_SINK = (line) => {
|
|
6
|
+
// Phase 0: write to stderr; otto-cli will replace this sink at wire time.
|
|
7
|
+
process.stderr.write(line + '\n');
|
|
8
|
+
};
|
|
9
|
+
export function createLogger(namespace, opts = {}) {
|
|
10
|
+
const level = opts.level ?? 'info';
|
|
11
|
+
const sink = opts.sink ?? DEFAULT_SINK;
|
|
12
|
+
const threshold = LEVEL_ORDER[level];
|
|
13
|
+
function emit(at, msg, ctx) {
|
|
14
|
+
if (LEVEL_ORDER[at] < threshold)
|
|
15
|
+
return;
|
|
16
|
+
const ctxPart = ctx ? ' ' + JSON.stringify(ctx) : '';
|
|
17
|
+
sink(`${new Date().toISOString()} ${at} ${namespace} ${msg}${ctxPart}`);
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
debug: (msg, ctx) => emit('debug', msg, ctx),
|
|
21
|
+
info: (msg, ctx) => emit('info', msg, ctx),
|
|
22
|
+
warn: (msg, ctx) => emit('warn', msg, ctx),
|
|
23
|
+
error: (msg, ctx) => emit('error', msg, ctx),
|
|
24
|
+
child: (suffix) => createLogger(`${namespace}.${suffix}`, { level, sink }),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export type MigrationFn<TIn = unknown, TOut = unknown> = (data: TIn) => Promise<TOut>;
|
|
2
|
+
export declare class MigrationRunner {
|
|
3
|
+
#private;
|
|
4
|
+
register<TIn = unknown, TOut = unknown>(kind: string, from: number, to: number, fn: MigrationFn<TIn, TOut>): void;
|
|
5
|
+
latestVersion(kind: string): number | null;
|
|
6
|
+
migrate(kind: string, fromVersion: number, data: unknown): Promise<unknown>;
|
|
7
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Migration framework — forward-only schema migrations per file kind.
|
|
2
|
+
// Spec §6.2, §3.4c.
|
|
3
|
+
export class MigrationRunner {
|
|
4
|
+
#byKind = new Map();
|
|
5
|
+
register(kind, from, to, fn) {
|
|
6
|
+
if (to <= from) {
|
|
7
|
+
throw new Error(`migration target version (${to}) must be greater than source (${from})`);
|
|
8
|
+
}
|
|
9
|
+
const list = this.#byKind.get(kind) ?? [];
|
|
10
|
+
list.push({ from, to, fn: fn });
|
|
11
|
+
list.sort((a, b) => a.from - b.from);
|
|
12
|
+
this.#byKind.set(kind, list);
|
|
13
|
+
}
|
|
14
|
+
latestVersion(kind) {
|
|
15
|
+
const list = this.#byKind.get(kind);
|
|
16
|
+
if (!list || list.length === 0)
|
|
17
|
+
return null;
|
|
18
|
+
return Math.max(...list.map(m => m.to));
|
|
19
|
+
}
|
|
20
|
+
async migrate(kind, fromVersion, data) {
|
|
21
|
+
const target = this.latestVersion(kind);
|
|
22
|
+
if (target == null || fromVersion >= target)
|
|
23
|
+
return data;
|
|
24
|
+
const list = this.#byKind.get(kind);
|
|
25
|
+
let current = fromVersion;
|
|
26
|
+
let value = data;
|
|
27
|
+
while (current < target) {
|
|
28
|
+
const next = list.find(m => m.from === current);
|
|
29
|
+
if (!next)
|
|
30
|
+
throw new Error(`no migration from version ${current} to ${current + 1} for kind ${kind}`);
|
|
31
|
+
value = await next.fn(value);
|
|
32
|
+
current = next.to;
|
|
33
|
+
}
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export async function writeNdjson(stream, message) {
|
|
2
|
+
const line = JSON.stringify(message) + '\n';
|
|
3
|
+
await new Promise((resolve, reject) => {
|
|
4
|
+
stream.write(line, 'utf8', (err) => (err ? reject(err) : resolve()));
|
|
5
|
+
});
|
|
6
|
+
}
|
|
7
|
+
export async function* readNdjson(stream) {
|
|
8
|
+
stream.setEncoding('utf8');
|
|
9
|
+
let buffer = '';
|
|
10
|
+
let lineNumber = 0;
|
|
11
|
+
for await (const chunk of stream) {
|
|
12
|
+
buffer += chunk;
|
|
13
|
+
let idx;
|
|
14
|
+
while ((idx = buffer.indexOf('\n')) >= 0) {
|
|
15
|
+
const line = buffer.slice(0, idx);
|
|
16
|
+
buffer = buffer.slice(idx + 1);
|
|
17
|
+
lineNumber++;
|
|
18
|
+
if (line.length === 0)
|
|
19
|
+
continue;
|
|
20
|
+
try {
|
|
21
|
+
yield JSON.parse(line);
|
|
22
|
+
}
|
|
23
|
+
catch (err) {
|
|
24
|
+
throw new Error(`NDJSON parse error at line ${lineNumber}: ${err.message}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
// Tail (no trailing newline)
|
|
29
|
+
if (buffer.length > 0) {
|
|
30
|
+
lineNumber++;
|
|
31
|
+
try {
|
|
32
|
+
yield JSON.parse(buffer);
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
throw new Error(`NDJSON parse error at line ${lineNumber}: ${err.message}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// SecretScanner — stub gate before any disk write to memory layers A/B.
|
|
2
|
+
// Spec §6.5.
|
|
3
|
+
// Patterns are intentionally conservative — false negatives are OK in v1; false positives in
|
|
4
|
+
// memory are a real cost (user lessons get over-redacted). Tighten or extend as Phase 3 reveals
|
|
5
|
+
// real-world content.
|
|
6
|
+
const PATTERNS = [
|
|
7
|
+
{ kind: 'anthropic_api_key', regex: /sk-ant-api03-[A-Za-z0-9_-]{40,}/g },
|
|
8
|
+
{ kind: 'openai_api_key', regex: /sk-(?:proj-)?[A-Za-z0-9]{40,}/g },
|
|
9
|
+
{ kind: 'aws_access_key_id', regex: /AKIA[0-9A-Z]{16}/g },
|
|
10
|
+
{ kind: 'github_pat', regex: /gh[pous]_[A-Za-z0-9]{36,}/g },
|
|
11
|
+
];
|
|
12
|
+
export class SecretScanner {
|
|
13
|
+
scan(text) {
|
|
14
|
+
const hits = [];
|
|
15
|
+
for (const { kind, regex } of PATTERNS) {
|
|
16
|
+
regex.lastIndex = 0;
|
|
17
|
+
let m;
|
|
18
|
+
while ((m = regex.exec(text)) != null) {
|
|
19
|
+
hits.push({
|
|
20
|
+
kind,
|
|
21
|
+
start: m.index,
|
|
22
|
+
end: m.index + m[0].length,
|
|
23
|
+
preview: m[0].slice(0, 8) + '...',
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return hits.sort((a, b) => a.start - b.start);
|
|
28
|
+
}
|
|
29
|
+
redact(text) {
|
|
30
|
+
const hits = this.scan(text);
|
|
31
|
+
if (hits.length === 0)
|
|
32
|
+
return text;
|
|
33
|
+
let out = '';
|
|
34
|
+
let cursor = 0;
|
|
35
|
+
for (const h of hits) {
|
|
36
|
+
out += text.slice(cursor, h.start) + `[REDACTED:${h.kind}]`;
|
|
37
|
+
cursor = h.end;
|
|
38
|
+
}
|
|
39
|
+
out += text.slice(cursor);
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@otto/coworker-utils",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Otto co-worker package: coworker-utils",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"otto": {
|
|
7
|
+
"linkable": true,
|
|
8
|
+
"scope": "@otto",
|
|
9
|
+
"name": "coworker-utils"
|
|
10
|
+
},
|
|
11
|
+
"main": "./dist/index.js",
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"import": "./dist/index.js"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsc -p tsconfig.json",
|
|
21
|
+
"build:publish": "tsc -p tsconfig.publish.json"
|
|
22
|
+
},
|
|
23
|
+
"files": ["dist"]
|
|
24
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { appendFileSync, mkdtempSync, readFileSync, statSync, existsSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { AuditLog, type AuditRecord } from './audit-log.js';
|
|
7
|
+
|
|
8
|
+
function tmp(): string { return mkdtempSync(join(tmpdir(), 'audit-')); }
|
|
9
|
+
|
|
10
|
+
describe('AuditLog', () => {
|
|
11
|
+
it('appends a record as one JSONL line', () => {
|
|
12
|
+
const dir = tmp();
|
|
13
|
+
const log = new AuditLog({ path: join(dir, 'audit.jsonl') });
|
|
14
|
+
const rec: AuditRecord = {
|
|
15
|
+
_schema: 1, ts: '2026-06-01T00:00:00.000Z',
|
|
16
|
+
producer: 'vault', action: 'set', detail: { engine: 'jira', name: 'prod' },
|
|
17
|
+
};
|
|
18
|
+
log.append(rec);
|
|
19
|
+
const text = readFileSync(join(dir, 'audit.jsonl'), 'utf8');
|
|
20
|
+
const lines = text.split('\n').filter(Boolean);
|
|
21
|
+
assert.equal(lines.length, 1);
|
|
22
|
+
const parsed = JSON.parse(text.trim());
|
|
23
|
+
assert.deepEqual(parsed, rec);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('reads records back, newest first, with filters', async () => {
|
|
27
|
+
const dir = tmp();
|
|
28
|
+
const log = new AuditLog({ path: join(dir, 'audit.jsonl') });
|
|
29
|
+
log.append({ _schema: 1, ts: '2026-06-01T00:00:00.000Z', producer: 'vault', action: 'set', detail: {} });
|
|
30
|
+
log.append({ _schema: 1, ts: '2026-06-01T00:00:01.000Z', producer: 'secret-scanner', action: 'redact', severity: 'warn', detail: {} });
|
|
31
|
+
log.append({ _schema: 1, ts: '2026-06-01T00:00:02.000Z', producer: 'vault', action: 'get', detail: {} });
|
|
32
|
+
const all: AuditRecord[] = [];
|
|
33
|
+
for await (const r of log.read({})) all.push(r);
|
|
34
|
+
assert.deepEqual(all.map(r => r.action), ['get', 'redact', 'set']);
|
|
35
|
+
const vaultOnly: AuditRecord[] = [];
|
|
36
|
+
for await (const r of log.read({ producer: 'vault' })) vaultOnly.push(r);
|
|
37
|
+
assert.deepEqual(vaultOnly.map(r => r.action), ['get', 'set']);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('rotates at maxBytes threshold', () => {
|
|
41
|
+
const dir = tmp();
|
|
42
|
+
const log = new AuditLog({ path: join(dir, 'audit.jsonl'), maxBytes: 200 });
|
|
43
|
+
const big = 'x'.repeat(60);
|
|
44
|
+
for (let i = 0; i < 5; i++) {
|
|
45
|
+
log.append({ _schema: 1, ts: `2026-06-01T00:00:0${i}.000Z`, producer: 'vault', action: 'set', detail: { pad: big } });
|
|
46
|
+
}
|
|
47
|
+
assert.equal(existsSync(join(dir, 'audit.1.jsonl')), true);
|
|
48
|
+
assert.ok(statSync(join(dir, 'audit.jsonl')).size < 200);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('keeps at most 5 rotated tails', () => {
|
|
52
|
+
const dir = tmp();
|
|
53
|
+
const log = new AuditLog({ path: join(dir, 'audit.jsonl'), maxBytes: 100, maxTails: 5 });
|
|
54
|
+
const pad = 'x'.repeat(40);
|
|
55
|
+
for (let i = 0; i < 50; i++) {
|
|
56
|
+
log.append({ _schema: 1, ts: `2026-06-01T00:00:${String(i).padStart(2,'0')}.000Z`, producer: 'vault', action: 'set', detail: { pad } });
|
|
57
|
+
}
|
|
58
|
+
for (let n = 1; n <= 5; n++) assert.equal(existsSync(join(dir, `audit.${n}.jsonl`)), true);
|
|
59
|
+
assert.equal(existsSync(join(dir, 'audit.6.jsonl')), false);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('filters by since (inclusive lower bound)', async () => {
|
|
63
|
+
const dir = tmp();
|
|
64
|
+
const log = new AuditLog({ path: join(dir, 'audit.jsonl') });
|
|
65
|
+
const t0 = '2026-06-01T00:00:00.000Z';
|
|
66
|
+
const t1 = '2026-06-01T00:00:01.000Z';
|
|
67
|
+
const t2 = '2026-06-01T00:00:02.000Z';
|
|
68
|
+
log.append({ _schema: 1, ts: t0, producer: 'vault', action: 'set', detail: {} });
|
|
69
|
+
log.append({ _schema: 1, ts: t1, producer: 'vault', action: 'set', detail: {} });
|
|
70
|
+
log.append({ _schema: 1, ts: t2, producer: 'vault', action: 'set', detail: {} });
|
|
71
|
+
const got: AuditRecord[] = [];
|
|
72
|
+
for await (const r of log.read({ since: t1 })) got.push(r);
|
|
73
|
+
assert.deepEqual(got.map(r => r.ts), [t2, t1]);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('filters by severity', async () => {
|
|
77
|
+
const dir = tmp();
|
|
78
|
+
const log = new AuditLog({ path: join(dir, 'audit.jsonl') });
|
|
79
|
+
log.append({ _schema: 1, ts: '2026-06-01T00:00:00.000Z', producer: 'vault', action: 'set', severity: 'info', detail: {} });
|
|
80
|
+
log.append({ _schema: 1, ts: '2026-06-01T00:00:01.000Z', producer: 'vault', action: 'set', severity: 'warn', detail: {} });
|
|
81
|
+
const got: AuditRecord[] = [];
|
|
82
|
+
for await (const r of log.read({ severity: 'warn' })) got.push(r);
|
|
83
|
+
assert.equal(got.length, 1);
|
|
84
|
+
assert.equal(got[0].severity, 'warn');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('filters by engineId via detail.engine', async () => {
|
|
88
|
+
const dir = tmp();
|
|
89
|
+
const log = new AuditLog({ path: join(dir, 'audit.jsonl') });
|
|
90
|
+
log.append({ _schema: 1, ts: '2026-06-01T00:00:00.000Z', producer: 'vault', action: 'set', detail: { engine: 'jira' } });
|
|
91
|
+
log.append({ _schema: 1, ts: '2026-06-01T00:00:01.000Z', producer: 'vault', action: 'set', detail: { engine: 'datadog' } });
|
|
92
|
+
const got: AuditRecord[] = [];
|
|
93
|
+
for await (const r of log.read({ engineId: 'jira' })) got.push(r);
|
|
94
|
+
assert.equal(got.length, 1);
|
|
95
|
+
assert.equal((got[0].detail as { engine?: string }).engine, 'jira');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('silently skips malformed lines', async () => {
|
|
99
|
+
const dir = tmp();
|
|
100
|
+
const path = join(dir, 'audit.jsonl');
|
|
101
|
+
const log = new AuditLog({ path });
|
|
102
|
+
log.append({ _schema: 1, ts: '2026-06-01T00:00:00.000Z', producer: 'vault', action: 'set', detail: { tag: 'first' } });
|
|
103
|
+
appendFileSync(path, '{bad json\n', { mode: 0o600 });
|
|
104
|
+
log.append({ _schema: 1, ts: '2026-06-01T00:00:01.000Z', producer: 'vault', action: 'set', detail: { tag: 'second' } });
|
|
105
|
+
const got: AuditRecord[] = [];
|
|
106
|
+
for await (const r of log.read({})) got.push(r);
|
|
107
|
+
assert.equal(got.length, 2);
|
|
108
|
+
assert.deepEqual(got.map(r => (r.detail as { tag: string }).tag), ['second', 'first']);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('reads records from rotated tail files alongside current file', async () => {
|
|
112
|
+
const dir = tmp();
|
|
113
|
+
const path = join(dir, 'audit.jsonl');
|
|
114
|
+
const log = new AuditLog({ path, maxBytes: 200 });
|
|
115
|
+
const pad = 'x'.repeat(60);
|
|
116
|
+
// Write enough records to force at least one rotation.
|
|
117
|
+
for (let i = 0; i < 8; i++) {
|
|
118
|
+
log.append({
|
|
119
|
+
_schema: 1,
|
|
120
|
+
ts: `2026-06-01T00:00:0${i}.000Z`,
|
|
121
|
+
producer: 'vault',
|
|
122
|
+
action: 'set',
|
|
123
|
+
detail: { idx: i, pad },
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
// Confirm rotation actually happened (records exist in both files).
|
|
127
|
+
assert.equal(existsSync(join(dir, 'audit.1.jsonl')), true);
|
|
128
|
+
const currentLines = readFileSync(path, 'utf8').split('\n').filter(Boolean);
|
|
129
|
+
const tailLines = readFileSync(join(dir, 'audit.1.jsonl'), 'utf8').split('\n').filter(Boolean);
|
|
130
|
+
assert.ok(currentLines.length > 0, 'current file should have records');
|
|
131
|
+
assert.ok(tailLines.length > 0, 'rotated tail should have records');
|
|
132
|
+
|
|
133
|
+
const got: AuditRecord[] = [];
|
|
134
|
+
for await (const r of log.read({})) got.push(r);
|
|
135
|
+
// All 8 appended records must come back, newest first.
|
|
136
|
+
assert.equal(got.length, 8);
|
|
137
|
+
const indices = got.map(r => (r.detail as { idx: number }).idx);
|
|
138
|
+
assert.deepEqual(indices, [7, 6, 5, 4, 3, 2, 1, 0]);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { appendFileSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, renameSync, statSync, unlinkSync } from 'node:fs';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
|
|
4
|
+
export interface AuditRecord {
|
|
5
|
+
_schema: 1;
|
|
6
|
+
ts: string;
|
|
7
|
+
producer: string;
|
|
8
|
+
action: string;
|
|
9
|
+
severity?: 'info' | 'warn';
|
|
10
|
+
sessionId?: string;
|
|
11
|
+
scratchpadName?: string;
|
|
12
|
+
pid?: number;
|
|
13
|
+
detail: Record<string, unknown>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface AuditLogOptions {
|
|
17
|
+
path: string;
|
|
18
|
+
maxBytes?: number;
|
|
19
|
+
maxTails?: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface AuditFilter {
|
|
23
|
+
since?: string; // ISO-8601 lower bound (inclusive)
|
|
24
|
+
producer?: string;
|
|
25
|
+
action?: string;
|
|
26
|
+
severity?: 'info' | 'warn';
|
|
27
|
+
engineId?: string; // matches detail.engine if present
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const DEFAULT_MAX_BYTES = 10 * 1024 * 1024;
|
|
31
|
+
const DEFAULT_MAX_TAILS = 5;
|
|
32
|
+
|
|
33
|
+
export class AuditLog {
|
|
34
|
+
private readonly path: string;
|
|
35
|
+
private readonly maxBytes: number;
|
|
36
|
+
private readonly maxTails: number;
|
|
37
|
+
|
|
38
|
+
constructor(opts: AuditLogOptions) {
|
|
39
|
+
this.path = opts.path;
|
|
40
|
+
this.maxBytes = opts.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
41
|
+
this.maxTails = opts.maxTails ?? DEFAULT_MAX_TAILS;
|
|
42
|
+
mkdirSync(dirname(this.path), { recursive: true, mode: 0o700 });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
append(record: AuditRecord): void {
|
|
46
|
+
this.rotateIfNeeded();
|
|
47
|
+
const line = JSON.stringify(record) + '\n';
|
|
48
|
+
try {
|
|
49
|
+
appendFileSync(this.path, line, { mode: 0o600 });
|
|
50
|
+
} catch (err) {
|
|
51
|
+
process.stderr.write(`audit: write failed (${(err as Error).message}); continuing\n`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async *read(filter: AuditFilter): AsyncIterable<AuditRecord> {
|
|
56
|
+
const files = this.listLogFiles();
|
|
57
|
+
const records: AuditRecord[] = [];
|
|
58
|
+
for (const f of files) {
|
|
59
|
+
if (!existsSync(f)) continue;
|
|
60
|
+
const text = readFileSync(f, 'utf8');
|
|
61
|
+
for (const line of text.split('\n')) {
|
|
62
|
+
if (!line) continue;
|
|
63
|
+
try {
|
|
64
|
+
const rec = JSON.parse(line) as AuditRecord;
|
|
65
|
+
if (!this.matches(rec, filter)) continue;
|
|
66
|
+
records.push(rec);
|
|
67
|
+
} catch { /* skip malformed line */ }
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
records.sort((a, b) => (a.ts === b.ts ? 0 : a.ts < b.ts ? 1 : -1));
|
|
71
|
+
for (const r of records) yield r;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
private listLogFiles(): string[] {
|
|
75
|
+
const out = [this.path];
|
|
76
|
+
for (let n = 1; n <= this.maxTails; n++) {
|
|
77
|
+
out.push(`${this.path.replace(/\.jsonl$/, '')}.${n}.jsonl`);
|
|
78
|
+
}
|
|
79
|
+
return out;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private matches(rec: AuditRecord, f: AuditFilter): boolean {
|
|
83
|
+
if (f.producer && rec.producer !== f.producer) return false;
|
|
84
|
+
if (f.action && rec.action !== f.action) return false;
|
|
85
|
+
if (f.severity && rec.severity !== f.severity) return false;
|
|
86
|
+
if (f.since && rec.ts < f.since) return false;
|
|
87
|
+
if (f.engineId && (rec.detail as { engine?: string }).engine !== f.engineId) return false;
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private rotateIfNeeded(): void {
|
|
92
|
+
if (!existsSync(this.path)) return;
|
|
93
|
+
const size = statSync(this.path).size;
|
|
94
|
+
if (size < this.maxBytes) return;
|
|
95
|
+
// Find next available tail slot; if maxTails full, drop tail N (delete) before shifting.
|
|
96
|
+
const base = this.path.replace(/\.jsonl$/, '');
|
|
97
|
+
const tailN = `${base}.${this.maxTails}.jsonl`;
|
|
98
|
+
if (existsSync(tailN)) unlinkSync(tailN);
|
|
99
|
+
for (let n = this.maxTails - 1; n >= 1; n--) {
|
|
100
|
+
const src = `${base}.${n}.jsonl`;
|
|
101
|
+
const dst = `${base}.${n + 1}.jsonl`;
|
|
102
|
+
if (existsSync(src)) renameSync(src, dst);
|
|
103
|
+
}
|
|
104
|
+
renameSync(this.path, `${base}.1.jsonl`);
|
|
105
|
+
closeSync(openSync(this.path, 'w', 0o600));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { describe, it, beforeEach, afterEach } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import * as fs from 'node:fs/promises';
|
|
4
|
+
import * as path from 'node:path';
|
|
5
|
+
import * as os from 'node:os';
|
|
6
|
+
import { acquireLease, releaseLease, isLeaseHeld } from './lease.js';
|
|
7
|
+
|
|
8
|
+
let tmpdir: string;
|
|
9
|
+
|
|
10
|
+
describe('lease helper', () => {
|
|
11
|
+
beforeEach(async () => {
|
|
12
|
+
tmpdir = await fs.mkdtemp(path.join(os.tmpdir(), 'lease-test-'));
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
afterEach(async () => {
|
|
16
|
+
await fs.rm(tmpdir, { recursive: true, force: true });
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('acquires a free lease and writes PID + acquired_at + ttl_ms', async () => {
|
|
20
|
+
const lockPath = path.join(tmpdir, 'task.lock');
|
|
21
|
+
const ok = await acquireLease(lockPath, { ttlMs: 60_000 });
|
|
22
|
+
assert.equal(ok, true);
|
|
23
|
+
const raw = await fs.readFile(lockPath, 'utf8');
|
|
24
|
+
const data = JSON.parse(raw);
|
|
25
|
+
assert.equal(data.pid, process.pid);
|
|
26
|
+
assert.equal(typeof data.acquired_at, 'string');
|
|
27
|
+
assert.equal(data.ttl_ms, 60_000);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('blocks a second acquire while the first is held', async () => {
|
|
31
|
+
const lockPath = path.join(tmpdir, 'task.lock');
|
|
32
|
+
await acquireLease(lockPath, { ttlMs: 60_000 });
|
|
33
|
+
const ok = await acquireLease(lockPath, { ttlMs: 60_000 });
|
|
34
|
+
assert.equal(ok, false);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('release allows re-acquire', async () => {
|
|
38
|
+
const lockPath = path.join(tmpdir, 'task.lock');
|
|
39
|
+
await acquireLease(lockPath, { ttlMs: 60_000 });
|
|
40
|
+
await releaseLease(lockPath);
|
|
41
|
+
const ok = await acquireLease(lockPath, { ttlMs: 60_000 });
|
|
42
|
+
assert.equal(ok, true);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('expired lease (past ttl) is auto-cleared on next acquire', async () => {
|
|
46
|
+
const lockPath = path.join(tmpdir, 'task.lock');
|
|
47
|
+
await acquireLease(lockPath, { ttlMs: 50 });
|
|
48
|
+
await new Promise(r => setTimeout(r, 100));
|
|
49
|
+
const ok = await acquireLease(lockPath, { ttlMs: 60_000 });
|
|
50
|
+
assert.equal(ok, true, 'expected expired lease to be reclaimable');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('isLeaseHeld returns false for missing file', async () => {
|
|
54
|
+
const lockPath = path.join(tmpdir, 'missing.lock');
|
|
55
|
+
assert.equal(await isLeaseHeld(lockPath), false);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('isLeaseHeld returns false for expired lease without clearing it', async () => {
|
|
59
|
+
const lockPath = path.join(tmpdir, 'task.lock');
|
|
60
|
+
await acquireLease(lockPath, { ttlMs: 50 });
|
|
61
|
+
await new Promise(r => setTimeout(r, 100));
|
|
62
|
+
assert.equal(await isLeaseHeld(lockPath), false);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// Lease helper for global background tasks.
|
|
2
|
+
// Spec §6.4.
|
|
3
|
+
import * as fs from 'node:fs/promises';
|
|
4
|
+
import * as os from 'node:os';
|
|
5
|
+
|
|
6
|
+
export interface LeaseOptions {
|
|
7
|
+
ttlMs: number;
|
|
8
|
+
holder?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface LeaseData {
|
|
12
|
+
pid: number;
|
|
13
|
+
host: string;
|
|
14
|
+
acquired_at: string;
|
|
15
|
+
ttl_ms: number;
|
|
16
|
+
holder?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function isExpired(data: LeaseData): boolean {
|
|
20
|
+
const acquired = Date.parse(data.acquired_at);
|
|
21
|
+
if (Number.isNaN(acquired)) return true;
|
|
22
|
+
return Date.now() > acquired + data.ttl_ms;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function pidAlive(pid: number, host: string): boolean {
|
|
26
|
+
// We can only check pids on the same host.
|
|
27
|
+
if (host !== os.hostname()) return true; // assume alive on other hosts
|
|
28
|
+
try {
|
|
29
|
+
process.kill(pid, 0);
|
|
30
|
+
return true;
|
|
31
|
+
} catch {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function readLease(path: string): Promise<LeaseData | null> {
|
|
37
|
+
try {
|
|
38
|
+
const raw = await fs.readFile(path, 'utf8');
|
|
39
|
+
return JSON.parse(raw) as LeaseData;
|
|
40
|
+
} catch (err: unknown) {
|
|
41
|
+
if ((err as NodeJS.ErrnoException).code === 'ENOENT') return null;
|
|
42
|
+
throw err;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function acquireLease(path: string, opts: LeaseOptions): Promise<boolean> {
|
|
47
|
+
const existing = await readLease(path);
|
|
48
|
+
if (existing && !isExpired(existing) && pidAlive(existing.pid, existing.host)) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
const data: LeaseData = {
|
|
52
|
+
pid: process.pid,
|
|
53
|
+
host: os.hostname(),
|
|
54
|
+
acquired_at: new Date().toISOString(),
|
|
55
|
+
ttl_ms: opts.ttlMs,
|
|
56
|
+
holder: opts.holder,
|
|
57
|
+
};
|
|
58
|
+
await fs.writeFile(path, JSON.stringify(data, null, 2), { mode: 0o600 });
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function releaseLease(path: string): Promise<void> {
|
|
63
|
+
try {
|
|
64
|
+
await fs.unlink(path);
|
|
65
|
+
} catch (err: unknown) {
|
|
66
|
+
if ((err as NodeJS.ErrnoException).code !== 'ENOENT') throw err;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function isLeaseHeld(path: string): Promise<boolean> {
|
|
71
|
+
const data = await readLease(path);
|
|
72
|
+
if (!data) return false;
|
|
73
|
+
if (isExpired(data)) return false;
|
|
74
|
+
if (!pidAlive(data.pid, data.host)) return false;
|
|
75
|
+
return true;
|
|
76
|
+
}
|