@quantiya/codevibe-claude-plugin 1.0.11 → 1.0.13
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/.claude-plugin/plugin.json +1 -1
- package/dist/server.js +16 -1162
- package/package.json +5 -5
- package/dist/appsync-client.js +0 -858
- package/dist/auth-cli.js +0 -472
- package/dist/command-executor.js +0 -127
- package/dist/config.js +0 -106
- package/dist/crypto-service.js +0 -278
- package/dist/http-api.js +0 -334
- package/dist/key-manager.js +0 -287
- package/dist/logger.js +0 -18
- package/dist/prompt-responder.js +0 -132
- package/dist/token-storage.js +0 -169
- package/dist/types.js +0 -17
- package/node_modules/@quantiya/codevibe-core/README.md +0 -170
- package/node_modules/@quantiya/codevibe-core/bin/codevibe.js +0 -7
- package/node_modules/@quantiya/codevibe-core/dist/appsync/appsync-client.d.ts +0 -132
- package/node_modules/@quantiya/codevibe-core/dist/appsync/appsync-client.js +0 -576
- package/node_modules/@quantiya/codevibe-core/dist/appsync/index.d.ts +0 -2
- package/node_modules/@quantiya/codevibe-core/dist/appsync/index.js +0 -10
- package/node_modules/@quantiya/codevibe-core/dist/appsync/queries.d.ts +0 -16
- package/node_modules/@quantiya/codevibe-core/dist/appsync/queries.js +0 -189
- package/node_modules/@quantiya/codevibe-core/dist/auth/auth-cli.d.ts +0 -5
- package/node_modules/@quantiya/codevibe-core/dist/auth/auth-cli.js +0 -217
- package/node_modules/@quantiya/codevibe-core/dist/auth/auth-service.d.ts +0 -87
- package/node_modules/@quantiya/codevibe-core/dist/auth/auth-service.js +0 -464
- package/node_modules/@quantiya/codevibe-core/dist/auth/fetch-helpers.d.ts +0 -11
- package/node_modules/@quantiya/codevibe-core/dist/auth/fetch-helpers.js +0 -165
- package/node_modules/@quantiya/codevibe-core/dist/auth/index.d.ts +0 -2
- package/node_modules/@quantiya/codevibe-core/dist/auth/index.js +0 -9
- package/node_modules/@quantiya/codevibe-core/dist/config/config.d.ts +0 -53
- package/node_modules/@quantiya/codevibe-core/dist/config/config.js +0 -123
- package/node_modules/@quantiya/codevibe-core/dist/config/index.d.ts +0 -2
- package/node_modules/@quantiya/codevibe-core/dist/config/index.js +0 -8
- package/node_modules/@quantiya/codevibe-core/dist/crypto/crypto-service.d.ts +0 -118
- package/node_modules/@quantiya/codevibe-core/dist/crypto/crypto-service.js +0 -284
- package/node_modules/@quantiya/codevibe-core/dist/crypto/index.d.ts +0 -1
- package/node_modules/@quantiya/codevibe-core/dist/crypto/index.js +0 -9
- package/node_modules/@quantiya/codevibe-core/dist/index.d.ts +0 -14
- package/node_modules/@quantiya/codevibe-core/dist/index.js +0 -68
- package/node_modules/@quantiya/codevibe-core/dist/keychain/index.d.ts +0 -1
- package/node_modules/@quantiya/codevibe-core/dist/keychain/index.js +0 -8
- package/node_modules/@quantiya/codevibe-core/dist/keychain/keychain-manager.d.ts +0 -125
- package/node_modules/@quantiya/codevibe-core/dist/keychain/keychain-manager.js +0 -375
- package/node_modules/@quantiya/codevibe-core/dist/logger/index.d.ts +0 -1
- package/node_modules/@quantiya/codevibe-core/dist/logger/index.js +0 -8
- package/node_modules/@quantiya/codevibe-core/dist/logger/logger.d.ts +0 -35
- package/node_modules/@quantiya/codevibe-core/dist/logger/logger.js +0 -142
- package/node_modules/@quantiya/codevibe-core/dist/prompt-parser.d.ts +0 -39
- package/node_modules/@quantiya/codevibe-core/dist/prompt-parser.js +0 -236
- package/node_modules/@quantiya/codevibe-core/dist/session/index.d.ts +0 -2
- package/node_modules/@quantiya/codevibe-core/dist/session/index.js +0 -7
- package/node_modules/@quantiya/codevibe-core/dist/session/session-resume.d.ts +0 -55
- package/node_modules/@quantiya/codevibe-core/dist/session/session-resume.js +0 -151
- package/node_modules/@quantiya/codevibe-core/dist/types/auth.d.ts +0 -15
- package/node_modules/@quantiya/codevibe-core/dist/types/auth.js +0 -3
- package/node_modules/@quantiya/codevibe-core/dist/types/encryption.d.ts +0 -54
- package/node_modules/@quantiya/codevibe-core/dist/types/encryption.js +0 -3
- package/node_modules/@quantiya/codevibe-core/dist/types/events.d.ts +0 -74
- package/node_modules/@quantiya/codevibe-core/dist/types/events.js +0 -28
- package/node_modules/@quantiya/codevibe-core/dist/types/index.d.ts +0 -4
- package/node_modules/@quantiya/codevibe-core/dist/types/index.js +0 -22
- package/node_modules/@quantiya/codevibe-core/dist/types/session.d.ts +0 -59
- package/node_modules/@quantiya/codevibe-core/dist/types/session.js +0 -22
- package/node_modules/@quantiya/codevibe-core/node_modules/uuid/CHANGELOG.md +0 -274
- package/node_modules/@quantiya/codevibe-core/node_modules/uuid/CONTRIBUTING.md +0 -18
- package/node_modules/@quantiya/codevibe-core/node_modules/uuid/LICENSE.md +0 -9
- package/node_modules/@quantiya/codevibe-core/node_modules/uuid/README.md +0 -466
- package/node_modules/@quantiya/codevibe-core/node_modules/uuid/dist/bin/uuid +0 -2
- package/node_modules/@quantiya/codevibe-core/node_modules/uuid/dist/commonjs-browser/index.js +0 -79
- package/node_modules/@quantiya/codevibe-core/node_modules/uuid/dist/commonjs-browser/md5.js +0 -223
- package/node_modules/@quantiya/codevibe-core/node_modules/uuid/dist/commonjs-browser/native.js +0 -11
- package/node_modules/@quantiya/codevibe-core/node_modules/uuid/dist/commonjs-browser/nil.js +0 -8
- package/node_modules/@quantiya/codevibe-core/node_modules/uuid/dist/commonjs-browser/parse.js +0 -45
- package/node_modules/@quantiya/codevibe-core/node_modules/uuid/dist/commonjs-browser/regex.js +0 -8
- package/node_modules/@quantiya/codevibe-core/node_modules/uuid/dist/commonjs-browser/rng.js +0 -25
- package/node_modules/@quantiya/codevibe-core/node_modules/uuid/dist/commonjs-browser/sha1.js +0 -104
- package/node_modules/@quantiya/codevibe-core/node_modules/uuid/dist/commonjs-browser/stringify.js +0 -44
- package/node_modules/@quantiya/codevibe-core/node_modules/uuid/dist/commonjs-browser/v1.js +0 -107
- package/node_modules/@quantiya/codevibe-core/node_modules/uuid/dist/commonjs-browser/v3.js +0 -16
- package/node_modules/@quantiya/codevibe-core/node_modules/uuid/dist/commonjs-browser/v35.js +0 -80
- package/node_modules/@quantiya/codevibe-core/node_modules/uuid/dist/commonjs-browser/v4.js +0 -43
- package/node_modules/@quantiya/codevibe-core/node_modules/uuid/dist/commonjs-browser/v5.js +0 -16
- package/node_modules/@quantiya/codevibe-core/node_modules/uuid/dist/commonjs-browser/validate.js +0 -17
- package/node_modules/@quantiya/codevibe-core/node_modules/uuid/dist/commonjs-browser/version.js +0 -21
- package/node_modules/@quantiya/codevibe-core/node_modules/uuid/dist/esm-browser/index.js +0 -9
- package/node_modules/@quantiya/codevibe-core/node_modules/uuid/dist/esm-browser/md5.js +0 -215
- package/node_modules/@quantiya/codevibe-core/node_modules/uuid/dist/esm-browser/native.js +0 -4
- package/node_modules/@quantiya/codevibe-core/node_modules/uuid/dist/esm-browser/nil.js +0 -1
- package/node_modules/@quantiya/codevibe-core/node_modules/uuid/dist/esm-browser/parse.js +0 -35
- package/node_modules/@quantiya/codevibe-core/node_modules/uuid/dist/esm-browser/regex.js +0 -1
- package/node_modules/@quantiya/codevibe-core/node_modules/uuid/dist/esm-browser/rng.js +0 -18
- package/node_modules/@quantiya/codevibe-core/node_modules/uuid/dist/esm-browser/sha1.js +0 -96
- package/node_modules/@quantiya/codevibe-core/node_modules/uuid/dist/esm-browser/stringify.js +0 -33
- package/node_modules/@quantiya/codevibe-core/node_modules/uuid/dist/esm-browser/v1.js +0 -95
- package/node_modules/@quantiya/codevibe-core/node_modules/uuid/dist/esm-browser/v3.js +0 -4
- package/node_modules/@quantiya/codevibe-core/node_modules/uuid/dist/esm-browser/v35.js +0 -66
- package/node_modules/@quantiya/codevibe-core/node_modules/uuid/dist/esm-browser/v4.js +0 -29
- package/node_modules/@quantiya/codevibe-core/node_modules/uuid/dist/esm-browser/v5.js +0 -4
- package/node_modules/@quantiya/codevibe-core/node_modules/uuid/dist/esm-browser/validate.js +0 -7
- package/node_modules/@quantiya/codevibe-core/node_modules/uuid/dist/esm-browser/version.js +0 -11
- package/node_modules/@quantiya/codevibe-core/node_modules/uuid/dist/esm-node/index.js +0 -9
- package/node_modules/@quantiya/codevibe-core/node_modules/uuid/dist/esm-node/md5.js +0 -13
- package/node_modules/@quantiya/codevibe-core/node_modules/uuid/dist/esm-node/native.js +0 -4
- package/node_modules/@quantiya/codevibe-core/node_modules/uuid/dist/esm-node/nil.js +0 -1
- package/node_modules/@quantiya/codevibe-core/node_modules/uuid/dist/esm-node/parse.js +0 -35
- package/node_modules/@quantiya/codevibe-core/node_modules/uuid/dist/esm-node/regex.js +0 -1
- package/node_modules/@quantiya/codevibe-core/node_modules/uuid/dist/esm-node/rng.js +0 -12
- package/node_modules/@quantiya/codevibe-core/node_modules/uuid/dist/esm-node/sha1.js +0 -13
- package/node_modules/@quantiya/codevibe-core/node_modules/uuid/dist/esm-node/stringify.js +0 -33
- package/node_modules/@quantiya/codevibe-core/node_modules/uuid/dist/esm-node/v1.js +0 -95
- package/node_modules/@quantiya/codevibe-core/node_modules/uuid/dist/esm-node/v3.js +0 -4
- package/node_modules/@quantiya/codevibe-core/node_modules/uuid/dist/esm-node/v35.js +0 -66
- package/node_modules/@quantiya/codevibe-core/node_modules/uuid/dist/esm-node/v4.js +0 -29
- package/node_modules/@quantiya/codevibe-core/node_modules/uuid/dist/esm-node/v5.js +0 -4
- package/node_modules/@quantiya/codevibe-core/node_modules/uuid/dist/esm-node/validate.js +0 -7
- package/node_modules/@quantiya/codevibe-core/node_modules/uuid/dist/esm-node/version.js +0 -11
- package/node_modules/@quantiya/codevibe-core/node_modules/uuid/dist/index.js +0 -79
- package/node_modules/@quantiya/codevibe-core/node_modules/uuid/dist/md5-browser.js +0 -223
- package/node_modules/@quantiya/codevibe-core/node_modules/uuid/dist/md5.js +0 -23
- package/node_modules/@quantiya/codevibe-core/node_modules/uuid/dist/native-browser.js +0 -11
- package/node_modules/@quantiya/codevibe-core/node_modules/uuid/dist/native.js +0 -15
- package/node_modules/@quantiya/codevibe-core/node_modules/uuid/dist/nil.js +0 -8
- package/node_modules/@quantiya/codevibe-core/node_modules/uuid/dist/parse.js +0 -45
- package/node_modules/@quantiya/codevibe-core/node_modules/uuid/dist/regex.js +0 -8
- package/node_modules/@quantiya/codevibe-core/node_modules/uuid/dist/rng-browser.js +0 -25
- package/node_modules/@quantiya/codevibe-core/node_modules/uuid/dist/rng.js +0 -24
- package/node_modules/@quantiya/codevibe-core/node_modules/uuid/dist/sha1-browser.js +0 -104
- package/node_modules/@quantiya/codevibe-core/node_modules/uuid/dist/sha1.js +0 -23
- package/node_modules/@quantiya/codevibe-core/node_modules/uuid/dist/stringify.js +0 -44
- package/node_modules/@quantiya/codevibe-core/node_modules/uuid/dist/uuid-bin.js +0 -85
- package/node_modules/@quantiya/codevibe-core/node_modules/uuid/dist/v1.js +0 -107
- package/node_modules/@quantiya/codevibe-core/node_modules/uuid/dist/v3.js +0 -16
- package/node_modules/@quantiya/codevibe-core/node_modules/uuid/dist/v35.js +0 -80
- package/node_modules/@quantiya/codevibe-core/node_modules/uuid/dist/v4.js +0 -43
- package/node_modules/@quantiya/codevibe-core/node_modules/uuid/dist/v5.js +0 -16
- package/node_modules/@quantiya/codevibe-core/node_modules/uuid/dist/validate.js +0 -17
- package/node_modules/@quantiya/codevibe-core/node_modules/uuid/dist/version.js +0 -21
- package/node_modules/@quantiya/codevibe-core/node_modules/uuid/package.json +0 -135
- package/node_modules/@quantiya/codevibe-core/node_modules/uuid/wrapper.mjs +0 -10
- package/node_modules/@quantiya/codevibe-core/package.json +0 -51
- package/node_modules/base64-js/LICENSE +0 -21
- package/node_modules/base64-js/README.md +0 -34
- package/node_modules/base64-js/base64js.min.js +0 -1
- package/node_modules/base64-js/index.d.ts +0 -3
- package/node_modules/base64-js/index.js +0 -150
- package/node_modules/base64-js/package.json +0 -47
- package/node_modules/bl/.travis.yml +0 -17
- package/node_modules/bl/BufferList.js +0 -396
- package/node_modules/bl/LICENSE.md +0 -13
- package/node_modules/bl/README.md +0 -247
- package/node_modules/bl/bl.js +0 -84
- package/node_modules/bl/package.json +0 -37
- package/node_modules/bl/test/convert.js +0 -21
- package/node_modules/bl/test/indexOf.js +0 -492
- package/node_modules/bl/test/isBufferList.js +0 -32
- package/node_modules/bl/test/test.js +0 -869
- package/node_modules/buffer/AUTHORS.md +0 -70
- package/node_modules/buffer/LICENSE +0 -21
- package/node_modules/buffer/README.md +0 -410
- package/node_modules/buffer/index.d.ts +0 -186
- package/node_modules/buffer/index.js +0 -1817
- package/node_modules/buffer/package.json +0 -96
- package/node_modules/chownr/LICENSE +0 -15
- package/node_modules/chownr/README.md +0 -3
- package/node_modules/chownr/chownr.js +0 -167
- package/node_modules/chownr/package.json +0 -29
- package/node_modules/decompress-response/index.d.ts +0 -22
- package/node_modules/decompress-response/index.js +0 -58
- package/node_modules/decompress-response/license +0 -9
- package/node_modules/decompress-response/package.json +0 -56
- package/node_modules/decompress-response/readme.md +0 -48
- package/node_modules/deep-extend/CHANGELOG.md +0 -46
- package/node_modules/deep-extend/LICENSE +0 -20
- package/node_modules/deep-extend/README.md +0 -91
- package/node_modules/deep-extend/index.js +0 -1
- package/node_modules/deep-extend/lib/deep-extend.js +0 -150
- package/node_modules/deep-extend/package.json +0 -62
- package/node_modules/detect-libc/LICENSE +0 -201
- package/node_modules/detect-libc/README.md +0 -163
- package/node_modules/detect-libc/index.d.ts +0 -14
- package/node_modules/detect-libc/lib/detect-libc.js +0 -313
- package/node_modules/detect-libc/lib/elf.js +0 -39
- package/node_modules/detect-libc/lib/filesystem.js +0 -51
- package/node_modules/detect-libc/lib/process.js +0 -24
- package/node_modules/detect-libc/package.json +0 -44
- package/node_modules/end-of-stream/LICENSE +0 -21
- package/node_modules/end-of-stream/README.md +0 -54
- package/node_modules/end-of-stream/index.js +0 -96
- package/node_modules/end-of-stream/package.json +0 -37
- package/node_modules/expand-template/.travis.yml +0 -6
- package/node_modules/expand-template/LICENSE +0 -21
- package/node_modules/expand-template/README.md +0 -43
- package/node_modules/expand-template/index.js +0 -26
- package/node_modules/expand-template/package.json +0 -29
- package/node_modules/expand-template/test.js +0 -67
- package/node_modules/fs-constants/LICENSE +0 -21
- package/node_modules/fs-constants/README.md +0 -26
- package/node_modules/fs-constants/browser.js +0 -1
- package/node_modules/fs-constants/index.js +0 -1
- package/node_modules/fs-constants/package.json +0 -19
- package/node_modules/github-from-package/.travis.yml +0 -4
- package/node_modules/github-from-package/LICENSE +0 -18
- package/node_modules/github-from-package/example/package.json +0 -8
- package/node_modules/github-from-package/example/url.js +0 -3
- package/node_modules/github-from-package/index.js +0 -17
- package/node_modules/github-from-package/package.json +0 -30
- package/node_modules/github-from-package/readme.markdown +0 -53
- package/node_modules/github-from-package/test/a.json +0 -8
- package/node_modules/github-from-package/test/b.json +0 -5
- package/node_modules/github-from-package/test/c.json +0 -5
- package/node_modules/github-from-package/test/d.json +0 -7
- package/node_modules/github-from-package/test/e.json +0 -5
- package/node_modules/github-from-package/test/url.js +0 -19
- package/node_modules/ieee754/LICENSE +0 -11
- package/node_modules/ieee754/README.md +0 -51
- package/node_modules/ieee754/index.d.ts +0 -10
- package/node_modules/ieee754/index.js +0 -85
- package/node_modules/ieee754/package.json +0 -52
- package/node_modules/ini/LICENSE +0 -15
- package/node_modules/ini/README.md +0 -102
- package/node_modules/ini/ini.js +0 -206
- package/node_modules/ini/package.json +0 -33
- package/node_modules/keytar/LICENSE.md +0 -20
- package/node_modules/keytar/README.md +0 -94
- package/node_modules/keytar/binding.gyp +0 -66
- package/node_modules/keytar/build/Release/keytar.node +0 -0
- package/node_modules/keytar/keytar.d.ts +0 -51
- package/node_modules/keytar/lib/keytar.js +0 -43
- package/node_modules/keytar/package.json +0 -66
- package/node_modules/keytar/src/async.cc +0 -242
- package/node_modules/keytar/src/async.h +0 -103
- package/node_modules/keytar/src/credentials.h +0 -13
- package/node_modules/keytar/src/keytar.h +0 -41
- package/node_modules/keytar/src/keytar_mac.cc +0 -296
- package/node_modules/keytar/src/keytar_posix.cc +0 -184
- package/node_modules/keytar/src/keytar_win.cc +0 -272
- package/node_modules/keytar/src/main.cc +0 -139
- package/node_modules/mimic-response/index.d.ts +0 -17
- package/node_modules/mimic-response/index.js +0 -77
- package/node_modules/mimic-response/license +0 -9
- package/node_modules/mimic-response/package.json +0 -42
- package/node_modules/mimic-response/readme.md +0 -78
- package/node_modules/minimist/.eslintrc +0 -29
- package/node_modules/minimist/.github/FUNDING.yml +0 -12
- package/node_modules/minimist/.nycrc +0 -14
- package/node_modules/minimist/CHANGELOG.md +0 -298
- package/node_modules/minimist/LICENSE +0 -18
- package/node_modules/minimist/README.md +0 -121
- package/node_modules/minimist/example/parse.js +0 -4
- package/node_modules/minimist/index.js +0 -263
- package/node_modules/minimist/package.json +0 -75
- package/node_modules/minimist/test/all_bool.js +0 -34
- package/node_modules/minimist/test/bool.js +0 -177
- package/node_modules/minimist/test/dash.js +0 -43
- package/node_modules/minimist/test/default_bool.js +0 -37
- package/node_modules/minimist/test/dotted.js +0 -24
- package/node_modules/minimist/test/kv_short.js +0 -32
- package/node_modules/minimist/test/long.js +0 -33
- package/node_modules/minimist/test/num.js +0 -38
- package/node_modules/minimist/test/parse.js +0 -209
- package/node_modules/minimist/test/parse_modified.js +0 -11
- package/node_modules/minimist/test/proto.js +0 -64
- package/node_modules/minimist/test/short.js +0 -69
- package/node_modules/minimist/test/stop_early.js +0 -17
- package/node_modules/minimist/test/unknown.js +0 -104
- package/node_modules/minimist/test/whitespace.js +0 -10
- package/node_modules/mkdirp-classic/LICENSE +0 -21
- package/node_modules/mkdirp-classic/README.md +0 -18
- package/node_modules/mkdirp-classic/index.js +0 -98
- package/node_modules/mkdirp-classic/package.json +0 -18
- package/node_modules/napi-build-utils/.github/workflows/run-npm-tests.yml +0 -31
- package/node_modules/napi-build-utils/LICENSE +0 -21
- package/node_modules/napi-build-utils/README.md +0 -52
- package/node_modules/napi-build-utils/index.js +0 -214
- package/node_modules/napi-build-utils/index.md +0 -0
- package/node_modules/napi-build-utils/package.json +0 -42
- package/node_modules/node-abi/LICENSE +0 -21
- package/node_modules/node-abi/README.md +0 -54
- package/node_modules/node-abi/abi_registry.json +0 -432
- package/node_modules/node-abi/index.js +0 -179
- package/node_modules/node-abi/package.json +0 -45
- package/node_modules/node-addon-api/LICENSE.md +0 -13
- package/node_modules/node-addon-api/README.md +0 -293
- package/node_modules/node-addon-api/common.gypi +0 -21
- package/node_modules/node-addon-api/except.gypi +0 -25
- package/node_modules/node-addon-api/index.js +0 -11
- package/node_modules/node-addon-api/napi-inl.deprecated.h +0 -192
- package/node_modules/node-addon-api/napi-inl.h +0 -6209
- package/node_modules/node-addon-api/napi.h +0 -2983
- package/node_modules/node-addon-api/node_api.gyp +0 -9
- package/node_modules/node-addon-api/noexcept.gypi +0 -26
- package/node_modules/node-addon-api/nothing.c +0 -0
- package/node_modules/node-addon-api/package-support.json +0 -21
- package/node_modules/node-addon-api/package.json +0 -399
- package/node_modules/node-addon-api/tools/README.md +0 -73
- package/node_modules/node-addon-api/tools/check-napi.js +0 -100
- package/node_modules/node-addon-api/tools/clang-format.js +0 -68
- package/node_modules/node-addon-api/tools/conversion.js +0 -309
- package/node_modules/node-addon-api/tools/eslint-format.js +0 -71
- package/node_modules/prebuild-install/CHANGELOG.md +0 -131
- package/node_modules/prebuild-install/CONTRIBUTING.md +0 -6
- package/node_modules/prebuild-install/LICENSE +0 -21
- package/node_modules/prebuild-install/README.md +0 -163
- package/node_modules/prebuild-install/asset.js +0 -44
- package/node_modules/prebuild-install/bin.js +0 -78
- package/node_modules/prebuild-install/download.js +0 -142
- package/node_modules/prebuild-install/error.js +0 -14
- package/node_modules/prebuild-install/help.txt +0 -16
- package/node_modules/prebuild-install/index.js +0 -1
- package/node_modules/prebuild-install/log.js +0 -33
- package/node_modules/prebuild-install/package.json +0 -67
- package/node_modules/prebuild-install/proxy.js +0 -35
- package/node_modules/prebuild-install/rc.js +0 -64
- package/node_modules/prebuild-install/util.js +0 -143
- package/node_modules/pump/.github/FUNDING.yml +0 -2
- package/node_modules/pump/.travis.yml +0 -5
- package/node_modules/pump/LICENSE +0 -21
- package/node_modules/pump/README.md +0 -74
- package/node_modules/pump/SECURITY.md +0 -5
- package/node_modules/pump/empty.js +0 -1
- package/node_modules/pump/index.js +0 -86
- package/node_modules/pump/package.json +0 -30
- package/node_modules/pump/test-browser.js +0 -66
- package/node_modules/pump/test-node.js +0 -53
- package/node_modules/rc/LICENSE.APACHE2 +0 -15
- package/node_modules/rc/LICENSE.BSD +0 -26
- package/node_modules/rc/LICENSE.MIT +0 -24
- package/node_modules/rc/README.md +0 -227
- package/node_modules/rc/browser.js +0 -7
- package/node_modules/rc/cli.js +0 -4
- package/node_modules/rc/index.js +0 -53
- package/node_modules/rc/lib/utils.js +0 -104
- package/node_modules/rc/package.json +0 -29
- package/node_modules/rc/test/ini.js +0 -16
- package/node_modules/rc/test/nested-env-vars.js +0 -50
- package/node_modules/rc/test/test.js +0 -59
- package/node_modules/readable-stream/CONTRIBUTING.md +0 -38
- package/node_modules/readable-stream/GOVERNANCE.md +0 -136
- package/node_modules/readable-stream/LICENSE +0 -47
- package/node_modules/readable-stream/README.md +0 -106
- package/node_modules/readable-stream/errors-browser.js +0 -127
- package/node_modules/readable-stream/errors.js +0 -116
- package/node_modules/readable-stream/experimentalWarning.js +0 -17
- package/node_modules/readable-stream/lib/_stream_duplex.js +0 -126
- package/node_modules/readable-stream/lib/_stream_passthrough.js +0 -37
- package/node_modules/readable-stream/lib/_stream_readable.js +0 -1027
- package/node_modules/readable-stream/lib/_stream_transform.js +0 -190
- package/node_modules/readable-stream/lib/_stream_writable.js +0 -641
- package/node_modules/readable-stream/lib/internal/streams/async_iterator.js +0 -180
- package/node_modules/readable-stream/lib/internal/streams/buffer_list.js +0 -183
- package/node_modules/readable-stream/lib/internal/streams/destroy.js +0 -96
- package/node_modules/readable-stream/lib/internal/streams/end-of-stream.js +0 -86
- package/node_modules/readable-stream/lib/internal/streams/from-browser.js +0 -3
- package/node_modules/readable-stream/lib/internal/streams/from.js +0 -52
- package/node_modules/readable-stream/lib/internal/streams/pipeline.js +0 -86
- package/node_modules/readable-stream/lib/internal/streams/state.js +0 -22
- package/node_modules/readable-stream/lib/internal/streams/stream-browser.js +0 -1
- package/node_modules/readable-stream/lib/internal/streams/stream.js +0 -1
- package/node_modules/readable-stream/package.json +0 -68
- package/node_modules/readable-stream/readable-browser.js +0 -9
- package/node_modules/readable-stream/readable.js +0 -16
- package/node_modules/safe-buffer/LICENSE +0 -21
- package/node_modules/safe-buffer/README.md +0 -584
- package/node_modules/safe-buffer/index.d.ts +0 -187
- package/node_modules/safe-buffer/index.js +0 -65
- package/node_modules/safe-buffer/package.json +0 -51
- package/node_modules/semver/LICENSE +0 -15
- package/node_modules/semver/README.md +0 -665
- package/node_modules/semver/bin/semver.js +0 -191
- package/node_modules/semver/classes/comparator.js +0 -143
- package/node_modules/semver/classes/index.js +0 -7
- package/node_modules/semver/classes/range.js +0 -557
- package/node_modules/semver/classes/semver.js +0 -333
- package/node_modules/semver/functions/clean.js +0 -8
- package/node_modules/semver/functions/cmp.js +0 -54
- package/node_modules/semver/functions/coerce.js +0 -62
- package/node_modules/semver/functions/compare-build.js +0 -9
- package/node_modules/semver/functions/compare-loose.js +0 -5
- package/node_modules/semver/functions/compare.js +0 -7
- package/node_modules/semver/functions/diff.js +0 -60
- package/node_modules/semver/functions/eq.js +0 -5
- package/node_modules/semver/functions/gt.js +0 -5
- package/node_modules/semver/functions/gte.js +0 -5
- package/node_modules/semver/functions/inc.js +0 -21
- package/node_modules/semver/functions/lt.js +0 -5
- package/node_modules/semver/functions/lte.js +0 -5
- package/node_modules/semver/functions/major.js +0 -5
- package/node_modules/semver/functions/minor.js +0 -5
- package/node_modules/semver/functions/neq.js +0 -5
- package/node_modules/semver/functions/parse.js +0 -18
- package/node_modules/semver/functions/patch.js +0 -5
- package/node_modules/semver/functions/prerelease.js +0 -8
- package/node_modules/semver/functions/rcompare.js +0 -5
- package/node_modules/semver/functions/rsort.js +0 -5
- package/node_modules/semver/functions/satisfies.js +0 -12
- package/node_modules/semver/functions/sort.js +0 -5
- package/node_modules/semver/functions/valid.js +0 -8
- package/node_modules/semver/index.js +0 -91
- package/node_modules/semver/internal/constants.js +0 -37
- package/node_modules/semver/internal/debug.js +0 -11
- package/node_modules/semver/internal/identifiers.js +0 -29
- package/node_modules/semver/internal/lrucache.js +0 -42
- package/node_modules/semver/internal/parse-options.js +0 -17
- package/node_modules/semver/internal/re.js +0 -223
- package/node_modules/semver/package.json +0 -78
- package/node_modules/semver/preload.js +0 -4
- package/node_modules/semver/range.bnf +0 -16
- package/node_modules/semver/ranges/gtr.js +0 -6
- package/node_modules/semver/ranges/intersects.js +0 -9
- package/node_modules/semver/ranges/ltr.js +0 -6
- package/node_modules/semver/ranges/max-satisfying.js +0 -27
- package/node_modules/semver/ranges/min-satisfying.js +0 -26
- package/node_modules/semver/ranges/min-version.js +0 -63
- package/node_modules/semver/ranges/outside.js +0 -82
- package/node_modules/semver/ranges/simplify.js +0 -49
- package/node_modules/semver/ranges/subset.js +0 -249
- package/node_modules/semver/ranges/to-comparators.js +0 -10
- package/node_modules/semver/ranges/valid.js +0 -13
- package/node_modules/simple-concat/.travis.yml +0 -3
- package/node_modules/simple-concat/LICENSE +0 -20
- package/node_modules/simple-concat/README.md +0 -44
- package/node_modules/simple-concat/index.js +0 -15
- package/node_modules/simple-concat/package.json +0 -47
- package/node_modules/simple-concat/test/basic.js +0 -41
- package/node_modules/simple-get/.github/dependabot.yml +0 -15
- package/node_modules/simple-get/.github/workflows/ci.yml +0 -23
- package/node_modules/simple-get/LICENSE +0 -20
- package/node_modules/simple-get/README.md +0 -333
- package/node_modules/simple-get/index.js +0 -108
- package/node_modules/simple-get/package.json +0 -67
- package/node_modules/string_decoder/LICENSE +0 -48
- package/node_modules/string_decoder/README.md +0 -47
- package/node_modules/string_decoder/lib/string_decoder.js +0 -296
- package/node_modules/string_decoder/package.json +0 -34
- package/node_modules/strip-json-comments/index.js +0 -70
- package/node_modules/strip-json-comments/license +0 -21
- package/node_modules/strip-json-comments/package.json +0 -42
- package/node_modules/strip-json-comments/readme.md +0 -64
- package/node_modules/tar-fs/.travis.yml +0 -6
- package/node_modules/tar-fs/LICENSE +0 -21
- package/node_modules/tar-fs/README.md +0 -165
- package/node_modules/tar-fs/index.js +0 -363
- package/node_modules/tar-fs/package.json +0 -41
- package/node_modules/tar-fs/test/fixtures/a/hello.txt +0 -1
- package/node_modules/tar-fs/test/fixtures/b/a/test.txt +0 -1
- package/node_modules/tar-fs/test/fixtures/d/file1 +0 -0
- package/node_modules/tar-fs/test/fixtures/d/file2 +0 -0
- package/node_modules/tar-fs/test/fixtures/d/sub-dir/file5 +0 -0
- package/node_modules/tar-fs/test/fixtures/d/sub-files/file3 +0 -0
- package/node_modules/tar-fs/test/fixtures/d/sub-files/file4 +0 -0
- package/node_modules/tar-fs/test/fixtures/e/directory/.ignore +0 -0
- package/node_modules/tar-fs/test/fixtures/e/file +0 -0
- package/node_modules/tar-fs/test/fixtures/invalid.tar +0 -0
- package/node_modules/tar-fs/test/index.js +0 -346
- package/node_modules/tar-stream/LICENSE +0 -21
- package/node_modules/tar-stream/README.md +0 -168
- package/node_modules/tar-stream/extract.js +0 -257
- package/node_modules/tar-stream/headers.js +0 -295
- package/node_modules/tar-stream/index.js +0 -2
- package/node_modules/tar-stream/pack.js +0 -255
- package/node_modules/tar-stream/package.json +0 -58
- package/node_modules/tar-stream/sandbox.js +0 -11
- package/node_modules/tunnel-agent/LICENSE +0 -55
- package/node_modules/tunnel-agent/README.md +0 -4
- package/node_modules/tunnel-agent/index.js +0 -244
- package/node_modules/tunnel-agent/package.json +0 -22
- package/node_modules/util-deprecate/History.md +0 -16
- package/node_modules/util-deprecate/LICENSE +0 -24
- package/node_modules/util-deprecate/README.md +0 -53
- package/node_modules/util-deprecate/browser.js +0 -67
- package/node_modules/util-deprecate/node.js +0 -6
- package/node_modules/util-deprecate/package.json +0 -27
package/dist/server.js
CHANGED
|
@@ -1,1162 +1,16 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
-
var ownKeys = function(o) {
|
|
20
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
-
var ar = [];
|
|
22
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
-
return ar;
|
|
24
|
-
};
|
|
25
|
-
return ownKeys(o);
|
|
26
|
-
};
|
|
27
|
-
return function (mod) {
|
|
28
|
-
if (mod && mod.__esModule) return mod;
|
|
29
|
-
var result = {};
|
|
30
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
-
__setModuleDefault(result, mod);
|
|
32
|
-
return result;
|
|
33
|
-
};
|
|
34
|
-
})();
|
|
35
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.parseInteractivePromptInput = parseInteractivePromptInput;
|
|
37
|
-
const fs = __importStar(require("fs"));
|
|
38
|
-
const path = __importStar(require("path"));
|
|
39
|
-
const os = __importStar(require("os"));
|
|
40
|
-
const logger_1 = require("./logger");
|
|
41
|
-
// Import shared modules from codevibe-core
|
|
42
|
-
const codevibe_core_1 = require("@quantiya/codevibe-core");
|
|
43
|
-
// Import plugin-specific modules
|
|
44
|
-
const http_api_1 = require("./http-api");
|
|
45
|
-
const command_executor_1 = require("./command-executor");
|
|
46
|
-
const prompt_responder_1 = require("./prompt-responder");
|
|
47
|
-
class McpServer {
|
|
48
|
-
constructor(sessionId) {
|
|
49
|
-
this.activeSessions = new Map();
|
|
50
|
-
this.assignedPort = 0;
|
|
51
|
-
this.sessionKey = null; // E2E encryption session key
|
|
52
|
-
// Map Claude Code session IDs to backend session IDs (claude-{uuid} format)
|
|
53
|
-
this.claudeToBackendSessionId = new Map();
|
|
54
|
-
// Track prompts sent from mobile to avoid duplicate USER_PROMPT events
|
|
55
|
-
// When mobile sends a prompt, it gets typed into terminal which triggers UserPromptSubmit hook
|
|
56
|
-
// We track with timestamp and expire after 3 seconds to avoid false positives
|
|
57
|
-
this.pendingMobilePrompts = new Map();
|
|
58
|
-
this.httpApi = new http_api_1.HttpApi();
|
|
59
|
-
// AppSyncClient is created in start() after config is loaded with correct environment
|
|
60
|
-
this.commandExecutor = new command_executor_1.CommandExecutor();
|
|
61
|
-
this.promptResponder = new prompt_responder_1.PromptResponder();
|
|
62
|
-
this.initialSessionId = sessionId;
|
|
63
|
-
}
|
|
64
|
-
/**
|
|
65
|
-
* Get the port the server is listening on
|
|
66
|
-
*/
|
|
67
|
-
getPort() {
|
|
68
|
-
return this.assignedPort;
|
|
69
|
-
}
|
|
70
|
-
/**
|
|
71
|
-
* Generate backend session ID from Claude Code session ID.
|
|
72
|
-
* Format: claude-{claudeSessionId}
|
|
73
|
-
*/
|
|
74
|
-
generateBackendSessionId(claudeSessionId) {
|
|
75
|
-
return `claude-${claudeSessionId}`;
|
|
76
|
-
}
|
|
77
|
-
/**
|
|
78
|
-
* Track a mobile prompt to filter out the duplicate USER_PROMPT from desktop hook
|
|
79
|
-
*/
|
|
80
|
-
trackMobilePrompt(sessionId, prompt) {
|
|
81
|
-
if (!this.pendingMobilePrompts.has(sessionId)) {
|
|
82
|
-
this.pendingMobilePrompts.set(sessionId, []);
|
|
83
|
-
}
|
|
84
|
-
this.pendingMobilePrompts.get(sessionId).push({
|
|
85
|
-
prompt: prompt.trim(),
|
|
86
|
-
timestamp: Date.now(),
|
|
87
|
-
});
|
|
88
|
-
logger_1.logger.debug('Tracking mobile prompt for deduplication', { sessionId, promptLength: prompt.length });
|
|
89
|
-
}
|
|
90
|
-
/**
|
|
91
|
-
* Check if a prompt was recently sent from mobile (within expiry window)
|
|
92
|
-
* Returns true and removes the entry if found
|
|
93
|
-
*/
|
|
94
|
-
isRecentMobilePrompt(sessionId, prompt) {
|
|
95
|
-
const prompts = this.pendingMobilePrompts.get(sessionId);
|
|
96
|
-
if (!prompts)
|
|
97
|
-
return false;
|
|
98
|
-
const now = Date.now();
|
|
99
|
-
const trimmedPrompt = prompt.trim();
|
|
100
|
-
// Clean up expired entries and check for match
|
|
101
|
-
const validPrompts = [];
|
|
102
|
-
let found = false;
|
|
103
|
-
for (const entry of prompts) {
|
|
104
|
-
if (now - entry.timestamp > McpServer.MOBILE_PROMPT_EXPIRY_MS) {
|
|
105
|
-
// Expired, skip
|
|
106
|
-
continue;
|
|
107
|
-
}
|
|
108
|
-
if (!found && entry.prompt === trimmedPrompt) {
|
|
109
|
-
// Found match, mark as found but don't add to validPrompts (consume it)
|
|
110
|
-
found = true;
|
|
111
|
-
logger_1.logger.debug('Found matching mobile prompt, filtering duplicate', { sessionId });
|
|
112
|
-
continue;
|
|
113
|
-
}
|
|
114
|
-
validPrompts.push(entry);
|
|
115
|
-
}
|
|
116
|
-
// Update the list with remaining valid prompts
|
|
117
|
-
if (validPrompts.length > 0) {
|
|
118
|
-
this.pendingMobilePrompts.set(sessionId, validPrompts);
|
|
119
|
-
}
|
|
120
|
-
else {
|
|
121
|
-
this.pendingMobilePrompts.delete(sessionId);
|
|
122
|
-
}
|
|
123
|
-
return found;
|
|
124
|
-
}
|
|
125
|
-
/**
|
|
126
|
-
* Write port file for a session so hooks can discover the server port
|
|
127
|
-
*/
|
|
128
|
-
writePortFile(sessionId) {
|
|
129
|
-
const portFilePath = path.join(os.tmpdir(), `codevibe-claude-${sessionId}.port`);
|
|
130
|
-
try {
|
|
131
|
-
fs.writeFileSync(portFilePath, this.assignedPort.toString());
|
|
132
|
-
logger_1.logger.info(`Port file written: ${portFilePath} -> ${this.assignedPort}`);
|
|
133
|
-
}
|
|
134
|
-
catch (error) {
|
|
135
|
-
logger_1.logger.error(`Failed to write port file: ${portFilePath}`, error);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
/**
|
|
139
|
-
* Remove port file for a session
|
|
140
|
-
*/
|
|
141
|
-
removePortFile(sessionId) {
|
|
142
|
-
const portFilePath = path.join(os.tmpdir(), `codevibe-claude-${sessionId}.port`);
|
|
143
|
-
try {
|
|
144
|
-
if (fs.existsSync(portFilePath)) {
|
|
145
|
-
fs.unlinkSync(portFilePath);
|
|
146
|
-
logger_1.logger.info(`Port file removed: ${portFilePath}`);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
catch (error) {
|
|
150
|
-
logger_1.logger.warn(`Failed to remove port file: ${portFilePath}`, error);
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
async start() {
|
|
154
|
-
try {
|
|
155
|
-
logger_1.logger.info('Starting CodeVibe MCP Server...', {
|
|
156
|
-
environment: (0, codevibe_core_1.getEnvironment)(),
|
|
157
|
-
});
|
|
158
|
-
// Create AppSyncClient (auto-configures from ENVIRONMENT env var)
|
|
159
|
-
this.appSyncClient = new codevibe_core_1.AppSyncClient();
|
|
160
|
-
// Authenticate using stored OAuth tokens (from 'codevibe-claude login')
|
|
161
|
-
const storedTokensAuth = await this.appSyncClient.authenticateWithStoredTokens();
|
|
162
|
-
if (storedTokensAuth) {
|
|
163
|
-
logger_1.logger.info('Authenticated with stored OAuth tokens', {
|
|
164
|
-
userId: this.appSyncClient.getCurrentUserId(),
|
|
165
|
-
email: this.appSyncClient.getCurrentUserEmail(),
|
|
166
|
-
});
|
|
167
|
-
// Register device encryption key for E2E encryption
|
|
168
|
-
await this.registerDeviceEncryptionKey();
|
|
169
|
-
}
|
|
170
|
-
else {
|
|
171
|
-
logger_1.logger.error('Authentication failed. Run "codevibe-claude login" first.');
|
|
172
|
-
console.error('Not authenticated. Run "codevibe-claude login" to sign in.');
|
|
173
|
-
process.exit(1);
|
|
174
|
-
}
|
|
175
|
-
// Register event handlers
|
|
176
|
-
this.httpApi.onEvent(this.handleEventFromHook.bind(this));
|
|
177
|
-
// Start HTTP API with dynamic port allocation
|
|
178
|
-
// Pass session ID if provided at startup
|
|
179
|
-
this.assignedPort = await this.httpApi.start(this.initialSessionId);
|
|
180
|
-
logger_1.logger.info('MCP Server started successfully', {
|
|
181
|
-
port: this.assignedPort,
|
|
182
|
-
host: (0, codevibe_core_1.getConfig)().server.host,
|
|
183
|
-
dynamicPort: (0, codevibe_core_1.getConfig)().server.dynamicPort,
|
|
184
|
-
sessionId: this.initialSessionId,
|
|
185
|
-
authenticated: this.appSyncClient.isAuthenticated(),
|
|
186
|
-
userId: this.appSyncClient.getCurrentUserId(),
|
|
187
|
-
});
|
|
188
|
-
}
|
|
189
|
-
catch (error) {
|
|
190
|
-
logger_1.logger.error('Failed to start MCP Server:', error);
|
|
191
|
-
throw error;
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
async stop() {
|
|
195
|
-
logger_1.logger.info('Stopping MCP Server...');
|
|
196
|
-
// Mark all active sessions as INACTIVE before shutting down
|
|
197
|
-
const sessionIds = Array.from(this.activeSessions.keys());
|
|
198
|
-
logger_1.logger.info(`Marking ${sessionIds.length} active session(s) as INACTIVE...`);
|
|
199
|
-
for (const sessionId of sessionIds) {
|
|
200
|
-
try {
|
|
201
|
-
await this.appSyncClient.updateSession({
|
|
202
|
-
sessionId,
|
|
203
|
-
status: codevibe_core_1.SessionStatus.INACTIVE,
|
|
204
|
-
});
|
|
205
|
-
logger_1.logger.info('Session marked as INACTIVE during shutdown', { sessionId });
|
|
206
|
-
// Remove port file using raw Claude session ID
|
|
207
|
-
const state = this.activeSessions.get(sessionId);
|
|
208
|
-
if (state) {
|
|
209
|
-
this.removePortFile(state.claudeSessionId);
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
catch (error) {
|
|
213
|
-
logger_1.logger.warn('Failed to mark session as INACTIVE during shutdown', { sessionId, error });
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
// Cleanup subscriptions
|
|
217
|
-
this.appSyncClient.cleanupSubscriptions();
|
|
218
|
-
// Clear active sessions
|
|
219
|
-
this.activeSessions.clear();
|
|
220
|
-
// Stop HTTP API
|
|
221
|
-
await this.httpApi.stop();
|
|
222
|
-
logger_1.logger.info('MCP Server stopped');
|
|
223
|
-
}
|
|
224
|
-
/**
|
|
225
|
-
* Handle events received from hook scripts via HTTP API
|
|
226
|
-
*/
|
|
227
|
-
async handleEventFromHook(payload) {
|
|
228
|
-
const { session_id, hook_event_name, type, content } = payload;
|
|
229
|
-
logger_1.logger.info('Processing hook event', {
|
|
230
|
-
sessionId: session_id,
|
|
231
|
-
hookEvent: hook_event_name,
|
|
232
|
-
type,
|
|
233
|
-
});
|
|
234
|
-
try {
|
|
235
|
-
// Handle session lifecycle events
|
|
236
|
-
if (hook_event_name === 'SessionStart') {
|
|
237
|
-
await this.handleSessionStart(payload);
|
|
238
|
-
}
|
|
239
|
-
else if (hook_event_name === 'SessionEnd') {
|
|
240
|
-
await this.handleSessionEnd(payload);
|
|
241
|
-
}
|
|
242
|
-
// Resolve backend session ID (claude-{uuid} format)
|
|
243
|
-
const backendSessionId = this.claudeToBackendSessionId.get(session_id)
|
|
244
|
-
|| this.generateBackendSessionId(session_id);
|
|
245
|
-
// Skip USER_PROMPT events that originated from mobile
|
|
246
|
-
// When mobile sends a prompt via tmux, it triggers UserPromptSubmit hook
|
|
247
|
-
// This creates a duplicate since mobile already created the event
|
|
248
|
-
if (type === codevibe_core_1.EventType.USER_PROMPT &&
|
|
249
|
-
payload.source === codevibe_core_1.EventSource.DESKTOP &&
|
|
250
|
-
hook_event_name === 'UserPromptSubmit' &&
|
|
251
|
-
content &&
|
|
252
|
-
this.isRecentMobilePrompt(backendSessionId, content)) {
|
|
253
|
-
logger_1.logger.info('Skipping duplicate USER_PROMPT from mobile-originated prompt', {
|
|
254
|
-
sessionId: backendSessionId,
|
|
255
|
-
contentLength: content.length,
|
|
256
|
-
});
|
|
257
|
-
return; // Don't send to AppSync
|
|
258
|
-
}
|
|
259
|
-
// Intercept INTERACTIVE_PROMPT — handle async with tmux snapshot parsing
|
|
260
|
-
if (type === codevibe_core_1.EventType.INTERACTIVE_PROMPT) {
|
|
261
|
-
const sessionState = this.activeSessions.get(backendSessionId);
|
|
262
|
-
if (sessionState) {
|
|
263
|
-
sessionState.waitingForPromptResponse = true;
|
|
264
|
-
sessionState.pendingPromptId = payload.prompt_id;
|
|
265
|
-
logger_1.logger.info('Interactive prompt detected - will parse options from tmux', {
|
|
266
|
-
sessionId: backendSessionId,
|
|
267
|
-
promptId: payload.prompt_id,
|
|
268
|
-
});
|
|
269
|
-
}
|
|
270
|
-
// Fire async — capture tmux after prompt renders, then send to AppSync
|
|
271
|
-
this.sendInteractivePromptAsync(backendSessionId, payload, content).catch(e => {
|
|
272
|
-
logger_1.logger.error('Failed to send interactive prompt with dynamic options', { error: e });
|
|
273
|
-
});
|
|
274
|
-
return; // Don't create AppSync event here — async handler will do it
|
|
275
|
-
}
|
|
276
|
-
// Encrypt event content if we have a session key
|
|
277
|
-
let eventContent = content;
|
|
278
|
-
let eventMetadata = payload.metadata;
|
|
279
|
-
let isEncrypted = false;
|
|
280
|
-
// DEBUG: Log session key state for hook events
|
|
281
|
-
logger_1.logger.info('Hook event encryption state', {
|
|
282
|
-
type,
|
|
283
|
-
sessionId: backendSessionId,
|
|
284
|
-
hasSessionKey: !!this.sessionKey,
|
|
285
|
-
sessionKeyLength: this.sessionKey?.length || 0,
|
|
286
|
-
});
|
|
287
|
-
if (this.sessionKey) {
|
|
288
|
-
eventContent = codevibe_core_1.cryptoService.encryptContent(content, this.sessionKey);
|
|
289
|
-
if (eventMetadata) {
|
|
290
|
-
const encryptedMeta = codevibe_core_1.cryptoService.encryptMetadata(eventMetadata, this.sessionKey);
|
|
291
|
-
eventMetadata = { encrypted: encryptedMeta };
|
|
292
|
-
}
|
|
293
|
-
isEncrypted = true;
|
|
294
|
-
logger_1.logger.info('Event encrypted for hook', { type, sessionId: backendSessionId, isEncrypted: true });
|
|
295
|
-
}
|
|
296
|
-
else {
|
|
297
|
-
logger_1.logger.warn('No session key - event will NOT be encrypted', { type, sessionId: backendSessionId });
|
|
298
|
-
}
|
|
299
|
-
// Send event to AppSync
|
|
300
|
-
const event = await this.appSyncClient.createEvent({
|
|
301
|
-
sessionId: backendSessionId,
|
|
302
|
-
type,
|
|
303
|
-
source: payload.source,
|
|
304
|
-
content: eventContent,
|
|
305
|
-
metadata: eventMetadata,
|
|
306
|
-
promptId: payload.prompt_id,
|
|
307
|
-
isEncrypted: isEncrypted ? true : undefined,
|
|
308
|
-
});
|
|
309
|
-
// Clear waiting state when user sends new prompt from desktop
|
|
310
|
-
// (means they answered the prompt locally or moved on)
|
|
311
|
-
if (type === codevibe_core_1.EventType.USER_PROMPT && payload.source === codevibe_core_1.EventSource.DESKTOP) {
|
|
312
|
-
const sessionState = this.activeSessions.get(backendSessionId);
|
|
313
|
-
if (sessionState?.waitingForPromptResponse) {
|
|
314
|
-
sessionState.waitingForPromptResponse = false;
|
|
315
|
-
sessionState.pendingPromptId = undefined;
|
|
316
|
-
sessionState.pendingSubmitMap = undefined;
|
|
317
|
-
logger_1.logger.info('Clearing prompt wait state - new desktop prompt received', {
|
|
318
|
-
sessionId: backendSessionId,
|
|
319
|
-
});
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
logger_1.logger.debug('Event sent to AppSync successfully');
|
|
323
|
-
}
|
|
324
|
-
catch (error) {
|
|
325
|
-
logger_1.logger.error('Failed to process hook event:', error);
|
|
326
|
-
throw error;
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
/**
|
|
330
|
-
* Handle SessionStart hook event
|
|
331
|
-
*
|
|
332
|
-
* Handles both new sessions and /resume:
|
|
333
|
-
* - If session exists in backend → reactivate it (same session ID)
|
|
334
|
-
* - If session doesn't exist → create new one with that session ID
|
|
335
|
-
*/
|
|
336
|
-
async handleSessionStart(payload) {
|
|
337
|
-
const claudeSessionId = payload.session_id;
|
|
338
|
-
const sessionId = this.generateBackendSessionId(claudeSessionId);
|
|
339
|
-
const cwd = payload.metadata?.cwd || process.cwd();
|
|
340
|
-
// Cache the Claude → backend session ID mapping
|
|
341
|
-
this.claudeToBackendSessionId.set(claudeSessionId, sessionId);
|
|
342
|
-
logger_1.logger.info('Session started', { claudeSessionId, sessionId, cwd });
|
|
343
|
-
// If there are other sessions in memory, mark them as INACTIVE
|
|
344
|
-
// This happens when user does /resume - the first session should be marked INACTIVE
|
|
345
|
-
const previousSessionIds = Array.from(this.activeSessions.keys()).filter(id => id !== sessionId);
|
|
346
|
-
if (previousSessionIds.length > 0) {
|
|
347
|
-
logger_1.logger.info(`Marking ${previousSessionIds.length} previous session(s) as INACTIVE`);
|
|
348
|
-
for (const prevId of previousSessionIds) {
|
|
349
|
-
try {
|
|
350
|
-
await this.appSyncClient.updateSession({
|
|
351
|
-
sessionId: prevId,
|
|
352
|
-
status: codevibe_core_1.SessionStatus.INACTIVE,
|
|
353
|
-
});
|
|
354
|
-
logger_1.logger.info('Previous session marked INACTIVE', { prevId, newSessionId: sessionId });
|
|
355
|
-
}
|
|
356
|
-
catch (error) {
|
|
357
|
-
logger_1.logger.warn('Failed to mark previous session as INACTIVE', { prevId, error });
|
|
358
|
-
}
|
|
359
|
-
// Use raw Claude session ID for port file (hooks use raw IDs)
|
|
360
|
-
const prevState = this.activeSessions.get(prevId);
|
|
361
|
-
if (prevState) {
|
|
362
|
-
this.removePortFile(prevState.claudeSessionId);
|
|
363
|
-
}
|
|
364
|
-
this.activeSessions.delete(prevId);
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
// Write port file using raw Claude session ID (hooks use raw IDs to discover port)
|
|
368
|
-
this.writePortFile(claudeSessionId);
|
|
369
|
-
// Create session state with authenticated user ID
|
|
370
|
-
const userId = this.appSyncClient.getCurrentUserId();
|
|
371
|
-
const sessionState = {
|
|
372
|
-
sessionId,
|
|
373
|
-
claudeSessionId,
|
|
374
|
-
userId,
|
|
375
|
-
projectPath: cwd,
|
|
376
|
-
cwd,
|
|
377
|
-
createdAt: new Date(),
|
|
378
|
-
subscriptionActive: false,
|
|
379
|
-
waitingForPromptResponse: false,
|
|
380
|
-
metadata: payload.metadata || {},
|
|
381
|
-
};
|
|
382
|
-
this.activeSessions.set(sessionId, sessionState);
|
|
383
|
-
// Use centralized resume/create utility from codevibe-core
|
|
384
|
-
try {
|
|
385
|
-
const result = await (0, codevibe_core_1.resumeOrCreateSession)({
|
|
386
|
-
sessionId,
|
|
387
|
-
userId: sessionState.userId,
|
|
388
|
-
agentType: codevibe_core_1.AgentType.CLAUDE,
|
|
389
|
-
projectPath: cwd,
|
|
390
|
-
metadata: payload.metadata || {},
|
|
391
|
-
}, this.appSyncClient, logger_1.logger);
|
|
392
|
-
this.sessionKey = result.sessionKey;
|
|
393
|
-
// Claude-specific: warn if resumed encrypted session but no key found
|
|
394
|
-
// (device key was regenerated after session was created)
|
|
395
|
-
if (result.resumed && !result.sessionKey) {
|
|
396
|
-
const pluginDeviceId = await codevibe_core_1.keychainManager.getDeviceId();
|
|
397
|
-
logger_1.logger.error('Device key not found in session encryptedKeys', { sessionId, pluginDeviceId });
|
|
398
|
-
console.error('\n⚠️ E2E ENCRYPTION WARNING: Cannot decrypt this session!');
|
|
399
|
-
console.error(` Your device ID (${pluginDeviceId.substring(0, 8)}...) is not in session's encryption keys.`);
|
|
400
|
-
console.error(' This happens if your device key was regenerated after the session was created.');
|
|
401
|
-
console.error(' SOLUTION: Start a new Claude Code session instead of resuming this one.\n');
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
catch (error) {
|
|
405
|
-
// createSession errors propagate from resumeOrCreateSession
|
|
406
|
-
if (this.isSessionLimitExceeded(error)) {
|
|
407
|
-
this.displaySubscriptionLimitError(error, 'session');
|
|
408
|
-
this.activeSessions.delete(sessionId);
|
|
409
|
-
this.removePortFile(claudeSessionId);
|
|
410
|
-
return;
|
|
411
|
-
}
|
|
412
|
-
logger_1.logger.error('Failed to create/resume session:', error);
|
|
413
|
-
}
|
|
414
|
-
// Subscribe to mobile events for this session
|
|
415
|
-
this.subscribeToMobileEvents(sessionId);
|
|
416
|
-
// Start heartbeat so iOS can detect if desktop is still connected
|
|
417
|
-
this.appSyncClient.startHeartbeat(sessionId);
|
|
418
|
-
}
|
|
419
|
-
/**
|
|
420
|
-
* Handle SessionEnd hook event
|
|
421
|
-
*/
|
|
422
|
-
async handleSessionEnd(payload) {
|
|
423
|
-
const claudeSessionId = payload.session_id;
|
|
424
|
-
const sessionId = this.claudeToBackendSessionId.get(claudeSessionId)
|
|
425
|
-
|| this.generateBackendSessionId(claudeSessionId);
|
|
426
|
-
logger_1.logger.info('Session ended', {
|
|
427
|
-
claudeSessionId,
|
|
428
|
-
sessionId,
|
|
429
|
-
reason: payload.metadata?.reason,
|
|
430
|
-
});
|
|
431
|
-
// Remove port file using raw Claude session ID (hooks use raw IDs)
|
|
432
|
-
this.removePortFile(claudeSessionId);
|
|
433
|
-
// Clear any waiting prompt state before ending session
|
|
434
|
-
const sessionState = this.activeSessions.get(sessionId);
|
|
435
|
-
if (sessionState?.waitingForPromptResponse) {
|
|
436
|
-
logger_1.logger.info('Clearing prompt wait state - session ending', { sessionId });
|
|
437
|
-
sessionState.waitingForPromptResponse = false;
|
|
438
|
-
sessionState.pendingPromptId = undefined;
|
|
439
|
-
}
|
|
440
|
-
// Stop heartbeat
|
|
441
|
-
this.appSyncClient.stopHeartbeat(sessionId);
|
|
442
|
-
// Update session status in AppSync
|
|
443
|
-
if (sessionState) {
|
|
444
|
-
try {
|
|
445
|
-
await this.appSyncClient.updateSession({
|
|
446
|
-
sessionId,
|
|
447
|
-
status: codevibe_core_1.SessionStatus.INACTIVE,
|
|
448
|
-
});
|
|
449
|
-
logger_1.logger.info('Session marked as INACTIVE in AppSync', { sessionId });
|
|
450
|
-
}
|
|
451
|
-
catch (error) {
|
|
452
|
-
logger_1.logger.warn('Failed to update session in AppSync:', error);
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
else {
|
|
456
|
-
logger_1.logger.warn('Cannot update session - session state not found', { sessionId });
|
|
457
|
-
}
|
|
458
|
-
// Remove from active sessions and ID mapping
|
|
459
|
-
this.activeSessions.delete(sessionId);
|
|
460
|
-
this.claudeToBackendSessionId.delete(claudeSessionId);
|
|
461
|
-
logger_1.logger.debug('Session cleanup completed', { sessionId });
|
|
462
|
-
}
|
|
463
|
-
/**
|
|
464
|
-
* Register device encryption key with backend for E2E encryption
|
|
465
|
-
* This ensures the current device's public key is known to the backend
|
|
466
|
-
* so session keys can be encrypted for this device
|
|
467
|
-
*/
|
|
468
|
-
async registerDeviceEncryptionKey() {
|
|
469
|
-
try {
|
|
470
|
-
// Get or generate device key pair (async keychain access)
|
|
471
|
-
const deviceId = await codevibe_core_1.keychainManager.getDeviceId();
|
|
472
|
-
const publicKey = await codevibe_core_1.keychainManager.getDevicePublicKey();
|
|
473
|
-
const platform = codevibe_core_1.keychainManager.getDevicePlatform();
|
|
474
|
-
const deviceName = codevibe_core_1.keychainManager.getDeviceName();
|
|
475
|
-
logger_1.logger.info('Registering device encryption key', { deviceId, platform, deviceName });
|
|
476
|
-
// Register with backend (will update if deviceId already exists)
|
|
477
|
-
await this.appSyncClient.registerDeviceKey(deviceId, publicKey, platform, deviceName);
|
|
478
|
-
codevibe_core_1.keychainManager.setIsRegistered(true);
|
|
479
|
-
logger_1.logger.info('Device encryption key registered successfully', { deviceId });
|
|
480
|
-
}
|
|
481
|
-
catch (error) {
|
|
482
|
-
// Don't fail startup if registration fails - encryption is optional
|
|
483
|
-
logger_1.logger.warn('Failed to register device encryption key (E2E encryption may not work):', error);
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
/**
|
|
487
|
-
* Subscribe to mobile events for a session
|
|
488
|
-
*/
|
|
489
|
-
subscribeToMobileEvents(sessionId) {
|
|
490
|
-
logger_1.logger.info('Subscribing to mobile events', { sessionId });
|
|
491
|
-
const sessionState = this.activeSessions.get(sessionId);
|
|
492
|
-
if (!sessionState) {
|
|
493
|
-
logger_1.logger.error('Session not found', { sessionId });
|
|
494
|
-
return;
|
|
495
|
-
}
|
|
496
|
-
this.appSyncClient.subscribeToEvents(sessionId, async (event) => {
|
|
497
|
-
logger_1.logger.info('Received mobile event', {
|
|
498
|
-
eventId: event.eventId,
|
|
499
|
-
type: event.type,
|
|
500
|
-
sessionId: event.sessionId,
|
|
501
|
-
isEncrypted: event.isEncrypted,
|
|
502
|
-
});
|
|
503
|
-
// Decrypt event content if encrypted
|
|
504
|
-
let decryptedContent = event.content || '';
|
|
505
|
-
if (event.isEncrypted && this.sessionKey) {
|
|
506
|
-
try {
|
|
507
|
-
decryptedContent = codevibe_core_1.cryptoService.decryptContent(event.content, this.sessionKey);
|
|
508
|
-
logger_1.logger.debug('Event decrypted successfully', { eventId: event.eventId });
|
|
509
|
-
}
|
|
510
|
-
catch (error) {
|
|
511
|
-
logger_1.logger.error('Failed to decrypt event:', { eventId: event.eventId, error });
|
|
512
|
-
// Fall back to original content (might not work, but better than nothing)
|
|
513
|
-
decryptedContent = event.content;
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
// Create a modified event with decrypted content for processing
|
|
517
|
-
const processedEvent = { ...event, content: decryptedContent };
|
|
518
|
-
// Mark event as DELIVERED (MCP server received it) - double gray checkmark
|
|
519
|
-
try {
|
|
520
|
-
await this.appSyncClient.updateEventStatus({
|
|
521
|
-
eventId: event.eventId,
|
|
522
|
-
sessionId: event.sessionId,
|
|
523
|
-
timestamp: event.timestamp,
|
|
524
|
-
deliveryStatus: codevibe_core_1.DeliveryStatus.DELIVERED,
|
|
525
|
-
});
|
|
526
|
-
logger_1.logger.info('Event marked as DELIVERED', { eventId: event.eventId });
|
|
527
|
-
}
|
|
528
|
-
catch (error) {
|
|
529
|
-
logger_1.logger.warn('Failed to mark event as DELIVERED', { eventId: event.eventId, error });
|
|
530
|
-
}
|
|
531
|
-
// Handle different event types from mobile
|
|
532
|
-
if (event.type === codevibe_core_1.EventType.USER_PROMPT) {
|
|
533
|
-
const sessionState = this.activeSessions.get(sessionId);
|
|
534
|
-
// Check if session is waiting for prompt response
|
|
535
|
-
if (sessionState?.waitingForPromptResponse) {
|
|
536
|
-
// Parse the input to determine action
|
|
537
|
-
const content = decryptedContent.trim();
|
|
538
|
-
const optionCount = sessionState.pendingSubmitMap
|
|
539
|
-
? Object.keys(sessionState.pendingSubmitMap).length
|
|
540
|
-
: 3;
|
|
541
|
-
const parsed = this.parseInteractivePromptInput(content, optionCount);
|
|
542
|
-
logger_1.logger.info('Parsed interactive prompt input', {
|
|
543
|
-
sessionId,
|
|
544
|
-
content,
|
|
545
|
-
parsed,
|
|
546
|
-
hasSubmitMap: !!sessionState.pendingSubmitMap,
|
|
547
|
-
});
|
|
548
|
-
if (parsed.action === 'select_option') {
|
|
549
|
-
// Translate via submitMap before sending to terminal
|
|
550
|
-
const terminalInput = sessionState.pendingSubmitMap?.[parsed.option] || parsed.option;
|
|
551
|
-
logger_1.logger.info('User selected option', { option: parsed.option, terminalInput });
|
|
552
|
-
const success = await this.promptResponder.answerInteractivePrompt(sessionId, terminalInput);
|
|
553
|
-
if (success) {
|
|
554
|
-
await this.markEventExecuted(event);
|
|
555
|
-
sessionState.waitingForPromptResponse = false;
|
|
556
|
-
sessionState.pendingPromptId = undefined;
|
|
557
|
-
sessionState.pendingSubmitMap = undefined;
|
|
558
|
-
await this.appSyncClient.createEvent({
|
|
559
|
-
sessionId,
|
|
560
|
-
type: codevibe_core_1.EventType.NOTIFICATION,
|
|
561
|
-
source: codevibe_core_1.EventSource.DESKTOP,
|
|
562
|
-
content: `Selected option ${parsed.option}`,
|
|
563
|
-
metadata: { promptAnswered: true },
|
|
564
|
-
});
|
|
565
|
-
}
|
|
566
|
-
else {
|
|
567
|
-
await this.sendPromptError(sessionId, 'Failed to select option');
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
else if (parsed.action === 'option_with_followup') {
|
|
571
|
-
// Translate option via submitMap, send it, then send follow-up text
|
|
572
|
-
const terminalInput = sessionState.pendingSubmitMap?.[parsed.option] || parsed.option;
|
|
573
|
-
logger_1.logger.info('User selected option with follow-up', {
|
|
574
|
-
option: parsed.option,
|
|
575
|
-
terminalInput,
|
|
576
|
-
followUpText: parsed.followUpText,
|
|
577
|
-
});
|
|
578
|
-
const optionSuccess = await this.promptResponder.answerInteractivePrompt(sessionId, terminalInput);
|
|
579
|
-
// Clear waiting state
|
|
580
|
-
sessionState.waitingForPromptResponse = false;
|
|
581
|
-
sessionState.pendingPromptId = undefined;
|
|
582
|
-
sessionState.pendingSubmitMap = undefined;
|
|
583
|
-
if (optionSuccess) {
|
|
584
|
-
await this.appSyncClient.createEvent({
|
|
585
|
-
sessionId,
|
|
586
|
-
type: codevibe_core_1.EventType.NOTIFICATION,
|
|
587
|
-
source: codevibe_core_1.EventSource.DESKTOP,
|
|
588
|
-
content: `Selected option ${parsed.option}`,
|
|
589
|
-
metadata: { promptAnswered: true },
|
|
590
|
-
});
|
|
591
|
-
// If there's follow-up text, send it after a short delay
|
|
592
|
-
if (parsed.followUpText) {
|
|
593
|
-
// Wait for Claude to process the option selection
|
|
594
|
-
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
595
|
-
// Send the follow-up text as a new prompt
|
|
596
|
-
const modifiedEvent = { ...event, content: parsed.followUpText };
|
|
597
|
-
await this.executeMobilePrompt(sessionId, modifiedEvent);
|
|
598
|
-
}
|
|
599
|
-
await this.markEventExecuted(event);
|
|
600
|
-
}
|
|
601
|
-
else {
|
|
602
|
-
await this.sendPromptError(sessionId, 'Failed to select option');
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
else {
|
|
606
|
-
// 'send_as_response' - send content as-is to interactive prompt
|
|
607
|
-
logger_1.logger.info('Sending as free-form response to interactive prompt', {
|
|
608
|
-
response: content,
|
|
609
|
-
});
|
|
610
|
-
const success = await this.promptResponder.answerInteractivePrompt(sessionId, content);
|
|
611
|
-
if (success) {
|
|
612
|
-
await this.markEventExecuted(event);
|
|
613
|
-
sessionState.waitingForPromptResponse = false;
|
|
614
|
-
sessionState.pendingPromptId = undefined;
|
|
615
|
-
sessionState.pendingSubmitMap = undefined;
|
|
616
|
-
await this.appSyncClient.createEvent({
|
|
617
|
-
sessionId,
|
|
618
|
-
type: codevibe_core_1.EventType.NOTIFICATION,
|
|
619
|
-
source: codevibe_core_1.EventSource.DESKTOP,
|
|
620
|
-
content: `Response sent to interactive prompt`,
|
|
621
|
-
metadata: { promptAnswered: true },
|
|
622
|
-
});
|
|
623
|
-
}
|
|
624
|
-
else {
|
|
625
|
-
await this.sendPromptError(sessionId, 'Failed to send response');
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
else {
|
|
630
|
-
// No interactive prompt waiting - execute as regular prompt
|
|
631
|
-
await this.executeMobilePrompt(sessionId, processedEvent);
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
}, (error) => {
|
|
635
|
-
logger_1.logger.error('Subscription error', { sessionId, error });
|
|
636
|
-
// TODO: Implement reconnection logic
|
|
637
|
-
});
|
|
638
|
-
sessionState.subscriptionActive = true;
|
|
639
|
-
logger_1.logger.info('Subscription active', { sessionId });
|
|
640
|
-
}
|
|
641
|
-
/**
|
|
642
|
-
* Send INTERACTIVE_PROMPT to AppSync after capturing dynamic options from tmux.
|
|
643
|
-
* Waits 500ms for Claude Code to render the prompt, then captures the terminal.
|
|
644
|
-
*/
|
|
645
|
-
async sendInteractivePromptAsync(backendSessionId, payload, content) {
|
|
646
|
-
// Wait for hook to return and Claude Code to render the prompt
|
|
647
|
-
await new Promise(resolve => setTimeout(resolve, 500));
|
|
648
|
-
const tmuxSession = process.env.CODEVIBE_TMUX_SESSION;
|
|
649
|
-
const metadata = { ...(payload.metadata || {}) };
|
|
650
|
-
if (tmuxSession) {
|
|
651
|
-
try {
|
|
652
|
-
const { exec } = await Promise.resolve().then(() => __importStar(require('child_process')));
|
|
653
|
-
const execAsync = (cmd) => new Promise((resolve, reject) => {
|
|
654
|
-
exec(cmd, { timeout: 5000 }, (err, stdout) => {
|
|
655
|
-
if (err)
|
|
656
|
-
reject(err);
|
|
657
|
-
else
|
|
658
|
-
resolve({ stdout: stdout || '' });
|
|
659
|
-
});
|
|
660
|
-
});
|
|
661
|
-
const { stdout } = await execAsync(`tmux capture-pane -p -e -S -30 -t '${tmuxSession}'`);
|
|
662
|
-
// Log captured snapshot for debugging
|
|
663
|
-
const snapshotLines = stdout.split('\n');
|
|
664
|
-
logger_1.logger.info('tmux capture result', {
|
|
665
|
-
tmuxSession,
|
|
666
|
-
totalLines: snapshotLines.length,
|
|
667
|
-
lastLines: snapshotLines.slice(-15).map(l => l.replace(/\x1B[^m]*m/g, '').trim()).filter(Boolean),
|
|
668
|
-
});
|
|
669
|
-
const parsed = (0, codevibe_core_1.parseInteractivePrompt)(stdout);
|
|
670
|
-
if (parsed && parsed.options.length > 0) {
|
|
671
|
-
metadata.options = parsed.options;
|
|
672
|
-
metadata.submitMap = parsed.submitMap;
|
|
673
|
-
metadata.instructions = this.buildPromptInstructions(parsed);
|
|
674
|
-
logger_1.logger.info('Parsed dynamic options from tmux', {
|
|
675
|
-
optionCount: parsed.options.length,
|
|
676
|
-
kind: parsed.kind,
|
|
677
|
-
options: parsed.options,
|
|
678
|
-
});
|
|
679
|
-
}
|
|
680
|
-
else {
|
|
681
|
-
logger_1.logger.info('No dynamic options parsed from tmux, using fallback', {
|
|
682
|
-
parsedResult: parsed,
|
|
683
|
-
});
|
|
684
|
-
this.addFallbackOptions(metadata);
|
|
685
|
-
}
|
|
686
|
-
}
|
|
687
|
-
catch (e) {
|
|
688
|
-
logger_1.logger.warn('Failed to capture tmux pane for options', { error: e });
|
|
689
|
-
this.addFallbackOptions(metadata);
|
|
690
|
-
}
|
|
691
|
-
}
|
|
692
|
-
else {
|
|
693
|
-
logger_1.logger.warn('No tmux session — using fallback options');
|
|
694
|
-
this.addFallbackOptions(metadata);
|
|
695
|
-
}
|
|
696
|
-
// Store submitMap in session state for response translation
|
|
697
|
-
const sessionState = this.activeSessions.get(backendSessionId);
|
|
698
|
-
if (sessionState && metadata.submitMap) {
|
|
699
|
-
sessionState.pendingSubmitMap = metadata.submitMap;
|
|
700
|
-
}
|
|
701
|
-
// Encrypt and send to AppSync
|
|
702
|
-
let eventContent = content;
|
|
703
|
-
let eventMetadata = metadata;
|
|
704
|
-
let isEncrypted = false;
|
|
705
|
-
if (this.sessionKey) {
|
|
706
|
-
eventContent = codevibe_core_1.cryptoService.encryptContent(content, this.sessionKey);
|
|
707
|
-
const encryptedMeta = codevibe_core_1.cryptoService.encryptMetadata(eventMetadata, this.sessionKey);
|
|
708
|
-
eventMetadata = { encrypted: encryptedMeta };
|
|
709
|
-
isEncrypted = true;
|
|
710
|
-
}
|
|
711
|
-
await this.appSyncClient.createEvent({
|
|
712
|
-
sessionId: backendSessionId,
|
|
713
|
-
type: codevibe_core_1.EventType.INTERACTIVE_PROMPT,
|
|
714
|
-
source: payload.source,
|
|
715
|
-
content: eventContent,
|
|
716
|
-
metadata: eventMetadata,
|
|
717
|
-
promptId: payload.prompt_id,
|
|
718
|
-
isEncrypted: isEncrypted ? true : undefined,
|
|
719
|
-
});
|
|
720
|
-
logger_1.logger.info('Interactive prompt sent to AppSync with dynamic options', {
|
|
721
|
-
sessionId: backendSessionId,
|
|
722
|
-
});
|
|
723
|
-
}
|
|
724
|
-
addFallbackOptions(metadata) {
|
|
725
|
-
metadata.options = [
|
|
726
|
-
{ number: '1', text: 'Yes' },
|
|
727
|
-
{ number: '2', text: 'Yes, and don\'t ask again' },
|
|
728
|
-
{ number: '3', text: 'Reject and tell Claude what to do differently' },
|
|
729
|
-
];
|
|
730
|
-
metadata.submitMap = { '1': '1', '2': '2', '3': '3' };
|
|
731
|
-
metadata.instructions = 'Reply with 1, 2, or 3. Append a message to provide alternative instructions.';
|
|
732
|
-
}
|
|
733
|
-
buildPromptInstructions(parsed) {
|
|
734
|
-
const nums = parsed.options.map(o => o.number).join(', ');
|
|
735
|
-
return `Reply with ${nums}. Append a message to provide alternative instructions.`;
|
|
736
|
-
}
|
|
737
|
-
/**
|
|
738
|
-
* Parse mobile input when an interactive prompt is waiting.
|
|
739
|
-
* Dynamically supports any number of options (not hardcoded to 3).
|
|
740
|
-
*
|
|
741
|
-
* Input patterns:
|
|
742
|
-
* - "N" (exact number) → select that option
|
|
743
|
-
* - "N <text>" → select option N, then send <text> as follow-up prompt
|
|
744
|
-
* - anything else → send as free-form response
|
|
745
|
-
*/
|
|
746
|
-
parseInteractivePromptInput(content, optionCount = 3) {
|
|
747
|
-
return parseInteractivePromptInput(content, optionCount);
|
|
748
|
-
}
|
|
749
|
-
/**
|
|
750
|
-
* Helper to mark an event as EXECUTED
|
|
751
|
-
*/
|
|
752
|
-
async markEventExecuted(event) {
|
|
753
|
-
try {
|
|
754
|
-
await this.appSyncClient.updateEventStatus({
|
|
755
|
-
eventId: event.eventId,
|
|
756
|
-
sessionId: event.sessionId,
|
|
757
|
-
timestamp: event.timestamp,
|
|
758
|
-
deliveryStatus: codevibe_core_1.DeliveryStatus.EXECUTED,
|
|
759
|
-
});
|
|
760
|
-
logger_1.logger.info('Event marked as EXECUTED', { eventId: event.eventId });
|
|
761
|
-
}
|
|
762
|
-
catch (error) {
|
|
763
|
-
logger_1.logger.warn('Failed to mark event as EXECUTED', { eventId: event.eventId, error });
|
|
764
|
-
}
|
|
765
|
-
}
|
|
766
|
-
/**
|
|
767
|
-
* Helper to send error notification to mobile
|
|
768
|
-
*/
|
|
769
|
-
async sendPromptError(sessionId, message) {
|
|
770
|
-
await this.appSyncClient.createEvent({
|
|
771
|
-
sessionId,
|
|
772
|
-
type: codevibe_core_1.EventType.NOTIFICATION,
|
|
773
|
-
source: codevibe_core_1.EventSource.DESKTOP,
|
|
774
|
-
content: message,
|
|
775
|
-
metadata: { error: true },
|
|
776
|
-
});
|
|
777
|
-
}
|
|
778
|
-
/**
|
|
779
|
-
* Check if error is a subscription limit exceeded error
|
|
780
|
-
*/
|
|
781
|
-
isSessionLimitExceeded(error) {
|
|
782
|
-
const errorMessage = this.getErrorMessage(error);
|
|
783
|
-
return errorMessage.includes('SESSION_LIMIT_EXCEEDED');
|
|
784
|
-
}
|
|
785
|
-
/**
|
|
786
|
-
* Check if error is a usage limit exceeded error (message or image)
|
|
787
|
-
*/
|
|
788
|
-
isUsageLimitExceeded(error) {
|
|
789
|
-
const errorMessage = this.getErrorMessage(error);
|
|
790
|
-
return errorMessage.includes('MESSAGE_LIMIT_EXCEEDED') || errorMessage.includes('IMAGE_LIMIT_EXCEEDED');
|
|
791
|
-
}
|
|
792
|
-
/**
|
|
793
|
-
* Extract error message from various error types
|
|
794
|
-
*/
|
|
795
|
-
getErrorMessage(error) {
|
|
796
|
-
if (error instanceof Error) {
|
|
797
|
-
return error.message;
|
|
798
|
-
}
|
|
799
|
-
if (typeof error === 'object' && error !== null) {
|
|
800
|
-
const errorObj = error;
|
|
801
|
-
// Check for GraphQL error format
|
|
802
|
-
if (errorObj.errors && Array.isArray(errorObj.errors)) {
|
|
803
|
-
return errorObj.errors.map((e) => e.message || '').join(' ');
|
|
804
|
-
}
|
|
805
|
-
// Check for message property
|
|
806
|
-
if (typeof errorObj.message === 'string') {
|
|
807
|
-
return errorObj.message;
|
|
808
|
-
}
|
|
809
|
-
}
|
|
810
|
-
return String(error);
|
|
811
|
-
}
|
|
812
|
-
/**
|
|
813
|
-
* Display subscription limit error to user
|
|
814
|
-
*/
|
|
815
|
-
displaySubscriptionLimitError(error, limitType) {
|
|
816
|
-
const errorMessage = this.getErrorMessage(error);
|
|
817
|
-
// Extract tier and limit info from error message if available
|
|
818
|
-
let tierInfo = '';
|
|
819
|
-
const tierMatch = errorMessage.match(/for your (\w+) plan/i);
|
|
820
|
-
if (tierMatch) {
|
|
821
|
-
tierInfo = ` (${tierMatch[1]} tier)`;
|
|
822
|
-
}
|
|
823
|
-
let limitInfo = '';
|
|
824
|
-
const limitMatch = errorMessage.match(/of (\d+)/);
|
|
825
|
-
if (limitMatch) {
|
|
826
|
-
limitInfo = ` [Limit: ${limitMatch[1]}]`;
|
|
827
|
-
}
|
|
828
|
-
// Display user-friendly console message with formatting
|
|
829
|
-
console.log('\n' + '='.repeat(60));
|
|
830
|
-
console.log('⚠️ SUBSCRIPTION LIMIT REACHED');
|
|
831
|
-
console.log('='.repeat(60));
|
|
832
|
-
switch (limitType) {
|
|
833
|
-
case 'session':
|
|
834
|
-
console.log(`You have reached the maximum number of active sessions${tierInfo}.`);
|
|
835
|
-
console.log(`${limitInfo}`);
|
|
836
|
-
console.log('\nTo continue, please:');
|
|
837
|
-
console.log(' • Close an existing Claude Code session, or');
|
|
838
|
-
console.log(' • Upgrade your subscription in the CodeVibe iOS app');
|
|
839
|
-
break;
|
|
840
|
-
case 'message':
|
|
841
|
-
console.log(`You have reached your monthly message limit${tierInfo}.`);
|
|
842
|
-
console.log(`${limitInfo}`);
|
|
843
|
-
console.log('\nTo continue, please:');
|
|
844
|
-
console.log(' • Wait until your usage resets next month, or');
|
|
845
|
-
console.log(' • Upgrade your subscription in the CodeVibe iOS app');
|
|
846
|
-
break;
|
|
847
|
-
case 'image':
|
|
848
|
-
console.log(`You have reached your monthly image attachment limit${tierInfo}.`);
|
|
849
|
-
console.log(`${limitInfo}`);
|
|
850
|
-
console.log('\nTo continue, please:');
|
|
851
|
-
console.log(' • Wait until your usage resets next month, or');
|
|
852
|
-
console.log(' • Upgrade your subscription in the CodeVibe iOS app');
|
|
853
|
-
break;
|
|
854
|
-
}
|
|
855
|
-
console.log('\nNote: You can still use Claude Code normally from your desktop.');
|
|
856
|
-
console.log('This limit only affects syncing with the mobile app.');
|
|
857
|
-
console.log('='.repeat(60) + '\n');
|
|
858
|
-
logger_1.logger.error('Subscription limit exceeded', { limitType, errorMessage });
|
|
859
|
-
}
|
|
860
|
-
/**
|
|
861
|
-
* Download an attachment from S3 and save it to a temp file
|
|
862
|
-
* If the attachment is encrypted, decrypts it using the session key
|
|
863
|
-
* Returns the local file path
|
|
864
|
-
* @param attachment The attachment to download
|
|
865
|
-
* @param sessionId The session ID for organizing temp files
|
|
866
|
-
* @param eventIsEncrypted Whether the parent event is encrypted (fallback for attachment.isEncrypted)
|
|
867
|
-
*/
|
|
868
|
-
async downloadAttachment(attachment, sessionId, eventIsEncrypted) {
|
|
869
|
-
try {
|
|
870
|
-
// Use attachment.isEncrypted if available, otherwise fall back to event.isEncrypted
|
|
871
|
-
// This handles AppSync subscription limitation where nested object fields may be null
|
|
872
|
-
const shouldDecrypt = attachment.isEncrypted ?? eventIsEncrypted ?? false;
|
|
873
|
-
logger_1.logger.info('Downloading attachment - START', {
|
|
874
|
-
id: attachment.id,
|
|
875
|
-
type: attachment.type,
|
|
876
|
-
filename: attachment.filename,
|
|
877
|
-
s3Key: attachment.s3Key,
|
|
878
|
-
attachmentIsEncrypted: attachment.isEncrypted,
|
|
879
|
-
eventIsEncrypted,
|
|
880
|
-
shouldDecrypt,
|
|
881
|
-
hasSessionKey: !!this.sessionKey,
|
|
882
|
-
});
|
|
883
|
-
// Get pre-signed download URL
|
|
884
|
-
const { downloadUrl } = await this.appSyncClient.getAttachmentDownloadUrl(attachment.s3Key);
|
|
885
|
-
// Download the file
|
|
886
|
-
const response = await fetch(downloadUrl);
|
|
887
|
-
if (!response.ok) {
|
|
888
|
-
throw new Error(`Failed to download attachment: ${response.status} ${response.statusText}`);
|
|
889
|
-
}
|
|
890
|
-
let buffer = Buffer.from(await response.arrayBuffer());
|
|
891
|
-
logger_1.logger.info('Attachment downloaded', {
|
|
892
|
-
id: attachment.id,
|
|
893
|
-
downloadedSize: buffer.length,
|
|
894
|
-
first20Bytes: buffer.slice(0, 20).toString('hex'),
|
|
895
|
-
});
|
|
896
|
-
// Decrypt if encrypted
|
|
897
|
-
logger_1.logger.info('Checking decryption conditions', {
|
|
898
|
-
id: attachment.id,
|
|
899
|
-
shouldDecrypt,
|
|
900
|
-
hasSessionKey: !!this.sessionKey,
|
|
901
|
-
willDecrypt: !!(shouldDecrypt && this.sessionKey),
|
|
902
|
-
});
|
|
903
|
-
if (shouldDecrypt && this.sessionKey) {
|
|
904
|
-
try {
|
|
905
|
-
logger_1.logger.info('Decrypting attachment', { id: attachment.id, encryptedSize: buffer.length });
|
|
906
|
-
buffer = codevibe_core_1.cryptoService.decryptData(buffer, this.sessionKey);
|
|
907
|
-
logger_1.logger.info('Attachment decrypted successfully', {
|
|
908
|
-
id: attachment.id,
|
|
909
|
-
decryptedSize: buffer.length,
|
|
910
|
-
first20Bytes: buffer.slice(0, 20).toString('hex'),
|
|
911
|
-
});
|
|
912
|
-
}
|
|
913
|
-
catch (decryptError) {
|
|
914
|
-
logger_1.logger.error('Failed to decrypt attachment:', { id: attachment.id, error: decryptError });
|
|
915
|
-
throw new Error('Failed to decrypt attachment');
|
|
916
|
-
}
|
|
917
|
-
}
|
|
918
|
-
else if (shouldDecrypt && !this.sessionKey) {
|
|
919
|
-
logger_1.logger.warn('Cannot decrypt attachment - no session key available', { id: attachment.id });
|
|
920
|
-
// Continue with encrypted data - Claude Code won't be able to view it properly
|
|
921
|
-
}
|
|
922
|
-
else {
|
|
923
|
-
logger_1.logger.info('Skipping decryption - attachment not encrypted or no session key', {
|
|
924
|
-
id: attachment.id,
|
|
925
|
-
shouldDecrypt,
|
|
926
|
-
hasSessionKey: !!this.sessionKey,
|
|
927
|
-
});
|
|
928
|
-
}
|
|
929
|
-
// Create temp directory for this session if it doesn't exist
|
|
930
|
-
const tempDir = path.join(os.tmpdir(), 'codevibe-claude', sessionId);
|
|
931
|
-
if (!fs.existsSync(tempDir)) {
|
|
932
|
-
fs.mkdirSync(tempDir, { recursive: true });
|
|
933
|
-
}
|
|
934
|
-
// Determine file extension from MIME type or filename
|
|
935
|
-
let extension = '';
|
|
936
|
-
// If filename is encrypted, decrypt it
|
|
937
|
-
let actualFilename = attachment.filename;
|
|
938
|
-
if (shouldDecrypt && attachment.filename && this.sessionKey) {
|
|
939
|
-
try {
|
|
940
|
-
actualFilename = codevibe_core_1.cryptoService.decryptContent(attachment.filename, this.sessionKey);
|
|
941
|
-
}
|
|
942
|
-
catch {
|
|
943
|
-
// Filename decryption failed, use as-is (might be plaintext)
|
|
944
|
-
actualFilename = attachment.filename;
|
|
945
|
-
}
|
|
946
|
-
}
|
|
947
|
-
if (actualFilename) {
|
|
948
|
-
const ext = path.extname(actualFilename);
|
|
949
|
-
if (ext)
|
|
950
|
-
extension = ext;
|
|
951
|
-
}
|
|
952
|
-
if (!extension) {
|
|
953
|
-
// Fallback to MIME type
|
|
954
|
-
const mimeToExt = {
|
|
955
|
-
'image/jpeg': '.jpg',
|
|
956
|
-
'image/png': '.png',
|
|
957
|
-
'image/gif': '.gif',
|
|
958
|
-
'image/webp': '.webp',
|
|
959
|
-
'image/heic': '.heic',
|
|
960
|
-
'application/pdf': '.pdf',
|
|
961
|
-
};
|
|
962
|
-
extension = mimeToExt[attachment.type] || '.bin';
|
|
963
|
-
}
|
|
964
|
-
// Save to temp file
|
|
965
|
-
const filename = `attachment-${attachment.id}${extension}`;
|
|
966
|
-
const filePath = path.join(tempDir, filename);
|
|
967
|
-
fs.writeFileSync(filePath, buffer);
|
|
968
|
-
logger_1.logger.info('Attachment saved to temp file', {
|
|
969
|
-
id: attachment.id,
|
|
970
|
-
filePath,
|
|
971
|
-
size: buffer.length,
|
|
972
|
-
wasDecrypted: shouldDecrypt && !!this.sessionKey,
|
|
973
|
-
});
|
|
974
|
-
return filePath;
|
|
975
|
-
}
|
|
976
|
-
catch (error) {
|
|
977
|
-
logger_1.logger.error('Failed to download attachment:', { id: attachment.id, error });
|
|
978
|
-
return null;
|
|
979
|
-
}
|
|
980
|
-
}
|
|
981
|
-
/**
|
|
982
|
-
* Execute a prompt from mobile in the desktop Claude Code session
|
|
983
|
-
* Uses tmux send-keys to send the prompt to the Claude Code session
|
|
984
|
-
* If the event has attachments, downloads them and includes file paths in prompt
|
|
985
|
-
*/
|
|
986
|
-
async executeMobilePrompt(sessionId, event) {
|
|
987
|
-
let prompt = event.content || '';
|
|
988
|
-
const attachments = event.attachments || [];
|
|
989
|
-
logger_1.logger.info('Executing mobile prompt via tmux', {
|
|
990
|
-
sessionId,
|
|
991
|
-
promptLength: prompt.length,
|
|
992
|
-
attachmentCount: attachments.length,
|
|
993
|
-
});
|
|
994
|
-
// Download attachments and build file paths to include in prompt
|
|
995
|
-
const attachmentPaths = [];
|
|
996
|
-
if (attachments.length > 0) {
|
|
997
|
-
logger_1.logger.info('Downloading attachments for prompt', { count: attachments.length });
|
|
998
|
-
for (const attachment of attachments) {
|
|
999
|
-
const filePath = await this.downloadAttachment(attachment, sessionId, event.isEncrypted // Pass event encryption status as fallback
|
|
1000
|
-
);
|
|
1001
|
-
if (filePath) {
|
|
1002
|
-
attachmentPaths.push(filePath);
|
|
1003
|
-
}
|
|
1004
|
-
}
|
|
1005
|
-
// If we have downloaded files, prepend them to the prompt
|
|
1006
|
-
if (attachmentPaths.length > 0) {
|
|
1007
|
-
const fileReferences = attachmentPaths
|
|
1008
|
-
.map(p => `[Attached file: ${p}]`)
|
|
1009
|
-
.join('\n');
|
|
1010
|
-
// Format: file references first, then the user's message
|
|
1011
|
-
if (prompt) {
|
|
1012
|
-
prompt = `${fileReferences}\n\n${prompt}`;
|
|
1013
|
-
}
|
|
1014
|
-
else {
|
|
1015
|
-
// No text content, just file references with instruction
|
|
1016
|
-
prompt = `${fileReferences}\n\nPlease analyze the attached file(s).`;
|
|
1017
|
-
}
|
|
1018
|
-
logger_1.logger.info('Prompt updated with attachment paths', {
|
|
1019
|
-
attachmentCount: attachmentPaths.length,
|
|
1020
|
-
newPromptLength: prompt.length,
|
|
1021
|
-
});
|
|
1022
|
-
}
|
|
1023
|
-
}
|
|
1024
|
-
// Track this prompt to filter out the duplicate USER_PROMPT from the hook
|
|
1025
|
-
this.trackMobilePrompt(sessionId, prompt);
|
|
1026
|
-
try {
|
|
1027
|
-
// Use tmux send-keys to send the prompt (same as answering interactive prompts)
|
|
1028
|
-
const success = await this.promptResponder.answerInteractivePrompt(sessionId, prompt);
|
|
1029
|
-
if (success) {
|
|
1030
|
-
// Mark event as EXECUTED (fed into Claude Code) - double blue checkmark
|
|
1031
|
-
try {
|
|
1032
|
-
await this.appSyncClient.updateEventStatus({
|
|
1033
|
-
eventId: event.eventId,
|
|
1034
|
-
sessionId: event.sessionId,
|
|
1035
|
-
timestamp: event.timestamp,
|
|
1036
|
-
deliveryStatus: codevibe_core_1.DeliveryStatus.EXECUTED,
|
|
1037
|
-
});
|
|
1038
|
-
logger_1.logger.info('Event marked as EXECUTED', { eventId: event.eventId });
|
|
1039
|
-
}
|
|
1040
|
-
catch (error) {
|
|
1041
|
-
logger_1.logger.warn('Failed to mark event as EXECUTED', { eventId: event.eventId, error });
|
|
1042
|
-
}
|
|
1043
|
-
logger_1.logger.info('Mobile prompt sent successfully', { sessionId });
|
|
1044
|
-
// Send confirmation to mobile
|
|
1045
|
-
const confirmationMsg = attachmentPaths.length > 0
|
|
1046
|
-
? `Prompt with ${attachmentPaths.length} attachment(s) sent to Claude Code`
|
|
1047
|
-
: `Prompt "${prompt.substring(0, 50)}${prompt.length > 50 ? '...' : ''}" sent to Claude Code`;
|
|
1048
|
-
await this.appSyncClient.createEvent({
|
|
1049
|
-
sessionId,
|
|
1050
|
-
type: codevibe_core_1.EventType.NOTIFICATION,
|
|
1051
|
-
source: codevibe_core_1.EventSource.DESKTOP,
|
|
1052
|
-
content: confirmationMsg,
|
|
1053
|
-
metadata: {
|
|
1054
|
-
mobilePrompt: true,
|
|
1055
|
-
attachmentCount: attachmentPaths.length,
|
|
1056
|
-
},
|
|
1057
|
-
});
|
|
1058
|
-
}
|
|
1059
|
-
else {
|
|
1060
|
-
logger_1.logger.error('Failed to send mobile prompt', { sessionId });
|
|
1061
|
-
// Send error notification to mobile
|
|
1062
|
-
await this.appSyncClient.createEvent({
|
|
1063
|
-
sessionId,
|
|
1064
|
-
type: codevibe_core_1.EventType.NOTIFICATION,
|
|
1065
|
-
source: codevibe_core_1.EventSource.DESKTOP,
|
|
1066
|
-
content: `Failed to send prompt to Claude Code`,
|
|
1067
|
-
metadata: {
|
|
1068
|
-
error: true,
|
|
1069
|
-
},
|
|
1070
|
-
});
|
|
1071
|
-
}
|
|
1072
|
-
}
|
|
1073
|
-
catch (error) {
|
|
1074
|
-
logger_1.logger.error('Failed to execute mobile prompt:', error);
|
|
1075
|
-
}
|
|
1076
|
-
}
|
|
1077
|
-
}
|
|
1078
|
-
McpServer.MOBILE_PROMPT_EXPIRY_MS = 3000; // 3 seconds
|
|
1079
|
-
// Main entry point
|
|
1080
|
-
async function main() {
|
|
1081
|
-
// Get session ID from command line argument or environment variable
|
|
1082
|
-
const sessionId = process.argv[2] || process.env.CLAUDE_SESSION_ID;
|
|
1083
|
-
if (sessionId) {
|
|
1084
|
-
logger_1.logger.info(`Starting MCP server for session: ${sessionId}`);
|
|
1085
|
-
}
|
|
1086
|
-
else {
|
|
1087
|
-
logger_1.logger.info('Starting MCP server without initial session ID (will be set on SessionStart)');
|
|
1088
|
-
}
|
|
1089
|
-
const server = new McpServer(sessionId);
|
|
1090
|
-
try {
|
|
1091
|
-
await server.start();
|
|
1092
|
-
// Output the assigned port for the session-start hook to capture
|
|
1093
|
-
const port = server.getPort();
|
|
1094
|
-
console.log(`PORT=${port}`);
|
|
1095
|
-
// Handle graceful shutdown
|
|
1096
|
-
let isShuttingDown = false;
|
|
1097
|
-
const shutdown = async (signal) => {
|
|
1098
|
-
if (isShuttingDown) {
|
|
1099
|
-
logger_1.logger.info('Shutdown already in progress, ignoring additional signal');
|
|
1100
|
-
return;
|
|
1101
|
-
}
|
|
1102
|
-
isShuttingDown = true;
|
|
1103
|
-
logger_1.logger.info(`Received ${signal} signal, stopping server...`);
|
|
1104
|
-
try {
|
|
1105
|
-
await server.stop();
|
|
1106
|
-
logger_1.logger.info('Graceful shutdown completed');
|
|
1107
|
-
process.exit(0);
|
|
1108
|
-
}
|
|
1109
|
-
catch (error) {
|
|
1110
|
-
logger_1.logger.error('Error during shutdown:', error);
|
|
1111
|
-
process.exit(1);
|
|
1112
|
-
}
|
|
1113
|
-
};
|
|
1114
|
-
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
1115
|
-
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
1116
|
-
process.on('SIGHUP', () => shutdown('SIGHUP'));
|
|
1117
|
-
// Handle uncaught exceptions - try to cleanup before exit
|
|
1118
|
-
process.on('uncaughtException', async (error) => {
|
|
1119
|
-
logger_1.logger.error('Uncaught exception:', error);
|
|
1120
|
-
await shutdown('uncaughtException');
|
|
1121
|
-
});
|
|
1122
|
-
process.on('unhandledRejection', async (reason) => {
|
|
1123
|
-
logger_1.logger.error('Unhandled rejection:', reason);
|
|
1124
|
-
await shutdown('unhandledRejection');
|
|
1125
|
-
});
|
|
1126
|
-
}
|
|
1127
|
-
catch (error) {
|
|
1128
|
-
logger_1.logger.error('Failed to start MCP Server:', error);
|
|
1129
|
-
process.exit(1);
|
|
1130
|
-
}
|
|
1131
|
-
}
|
|
1132
|
-
/**
|
|
1133
|
-
* Parse interactive prompt input — exported for testing
|
|
1134
|
-
*/
|
|
1135
|
-
function parseInteractivePromptInput(content, optionCount = 3) {
|
|
1136
|
-
const trimmed = content.trim();
|
|
1137
|
-
// Check for exact option number (e.g., "1", "2", "3")
|
|
1138
|
-
const exactMatch = trimmed.match(/^(\d+)$/);
|
|
1139
|
-
if (exactMatch) {
|
|
1140
|
-
const num = parseInt(exactMatch[1]);
|
|
1141
|
-
if (num >= 1 && num <= optionCount) {
|
|
1142
|
-
return { action: 'select_option', option: exactMatch[1] };
|
|
1143
|
-
}
|
|
1144
|
-
}
|
|
1145
|
-
// Check for "N <text>", "N,<text>", "N.<text>", "N\n<text>" pattern (option + follow-up)
|
|
1146
|
-
// Supports separators: whitespace, comma, period, semicolon, colon, dash, or combinations
|
|
1147
|
-
const withTextMatch = trimmed.match(/^(\d+)[,.:;\-\s\n]+(.+)$/s);
|
|
1148
|
-
if (withTextMatch) {
|
|
1149
|
-
const num = parseInt(withTextMatch[1]);
|
|
1150
|
-
if (num >= 1 && num <= optionCount) {
|
|
1151
|
-
return { action: 'option_with_followup', option: withTextMatch[1], followUpText: withTextMatch[2].trim() };
|
|
1152
|
-
}
|
|
1153
|
-
}
|
|
1154
|
-
// Anything else — send as free-form response
|
|
1155
|
-
return { action: 'send_as_response' };
|
|
1156
|
-
}
|
|
1157
|
-
// Start the server
|
|
1158
|
-
main().catch((error) => {
|
|
1159
|
-
logger_1.logger.error('Unhandled error in main:', error);
|
|
1160
|
-
process.exit(1);
|
|
1161
|
-
});
|
|
1162
|
-
//# sourceMappingURL=server.js.map
|
|
1
|
+
"use strict";var G=Object.create;var P=Object.defineProperty;var Y=Object.getOwnPropertyDescriptor;var z=Object.getOwnPropertyNames;var W=Object.getPrototypeOf,J=Object.prototype.hasOwnProperty;var Q=(m,e)=>{for(var s in e)P(m,s,{get:e[s],enumerable:!0})},F=(m,e,s,t)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of z(e))!J.call(m,i)&&i!==s&&P(m,i,{get:()=>e[i],enumerable:!(t=Y(e,i))||t.enumerable});return m};var y=(m,e,s)=>(s=m!=null?G(W(m)):{},F(e||!m||!m.__esModule?P(s,"default",{value:m,enumerable:!0}):s,m)),Z=m=>F(P({},"__esModule",{value:!0}),m);var te={};Q(te,{parseInteractivePromptInput:()=>B});module.exports=Z(te);var v=y(require("fs")),E=y(require("path")),T=y(require("os"));var O=y(require("os")),D=y(require("path")),N=require("@quantiya/codevibe-core"),n=(0,N.createLogger)({name:"codevibe-claude",logFile:D.default.join(O.default.tmpdir(),"codevibe-claude-mcp.log"),level:"info"});var a=require("@quantiya/codevibe-core");var R=y(require("express")),w=y(require("fs")),M=y(require("path")),k=y(require("os")),$=require("@quantiya/codevibe-core");var l=require("@quantiya/codevibe-core");var I=class{constructor(){this.assignedPort=0;this.app=(0,R.default)(),this.setupMiddleware(),this.setupRoutes()}setSessionId(e){this.sessionId=e}getPort(){return this.assignedPort}setupMiddleware(){this.app.use(R.default.json({limit:"1mb"})),this.app.use((e,s,t)=>{n.debug(`${e.method} ${e.path}`,{body:e.body,query:e.query}),t()}),this.app.use((e,s,t,i)=>{n.error("Express error:",e);let r={success:!1,error:e.message||"Internal server error"};t.status(500).json(r)})}setupRoutes(){this.app.get("/health",this.handleHealth.bind(this)),this.app.post("/event",this.handleEvent.bind(this)),process.env.NODE_ENV!=="production"&&this.app.post("/test/execute",this.handleTestExecute.bind(this))}handleHealth(e,s){let t={success:!0,data:{status:"healthy",uptime:process.uptime(),version:"0.1.0",timestamp:new Date().toISOString()}};s.json(t)}async handleEvent(e,s){try{let t=e.body;if(!t.session_id){let o={success:!1,error:"Missing required field: session_id"};s.status(400).json(o);return}if(!t.hook_event_name){let o={success:!1,error:"Missing required field: hook_event_name"};s.status(400).json(o);return}let i=this.transformHookToEvent(t);n.info("Received event from hook",{sessionId:t.session_id,hookEvent:t.hook_event_name,type:i.type}),this.eventHandler?await this.eventHandler(i):n.warn("No event handler registered");let r={success:!0,message:"Event processed successfully"};s.json(r)}catch(t){n.error("Error handling event:",t);let i={success:!1,error:t instanceof Error?t.message:"Unknown error"};s.status(500).json(i)}}async handleTestExecute(e,s){try{let{sessionId:t,prompt:i}=e.body;if(!t||!i){let o={success:!1,error:"Missing required fields: sessionId, prompt"};s.status(400).json(o);return}n.info("Test execute request",{sessionId:t,prompt:i});let r={success:!0,message:"Test execution endpoint - not implemented yet",data:{sessionId:t,prompt:i}};s.json(r)}catch(t){n.error("Error in test execute:",t);let i={success:!1,error:t instanceof Error?t.message:"Unknown error"};s.status(500).json(i)}}transformHookToEvent(e){let s,t,i={cwd:e.cwd,hook_event_name:e.hook_event_name,...e.metadata||{}};if(e.type&&e.content!==void 0)s=e.type,t=e.content;else switch(e.hook_event_name){case"SessionStart":s=l.EventType.NOTIFICATION,t="Session started",i.source=e.source;break;case"SessionEnd":s=l.EventType.NOTIFICATION,t=`Session ended: ${e.reason||"unknown"}`,i.reason=e.reason;break;case"UserPromptSubmit":s=l.EventType.USER_PROMPT,t=e.prompt||"";break;case"PostToolUse":s=l.EventType.TOOL_USE,t=JSON.stringify({tool_name:e.tool_name,tool_input:e.tool_input,tool_response:e.tool_response}),i.tool_name=e.tool_name;break;case"Notification":s=l.EventType.NOTIFICATION,t=e.message||"",i.notification_type=e.notification_type;break;default:s=l.EventType.NOTIFICATION,t=`Hook event: ${e.hook_event_name}`}return{session_id:e.session_id,hook_event_name:e.hook_event_name,type:s,source:l.EventSource.DESKTOP,content:t,metadata:i}}onEvent(e){this.eventHandler=e}async start(e){let s=e||this.sessionId;return s&&(this.sessionId=s),new Promise((t,i)=>{try{let r=(0,$.getConfig)(),o=r.server.dynamicPort?0:r.server.port;this.server=this.app.listen(o,r.server.host,()=>{let p=this.server.address();this.assignedPort=p.port,n.info(`HTTP API listening on http://${r.server.host}:${this.assignedPort}`),this.sessionId&&this.writePortFile(this.sessionId,this.assignedPort),t(this.assignedPort)}),this.server.on("error",p=>{n.error("HTTP server error:",p),i(p)})}catch(r){i(r)}})}writePortFile(e,s){let t=M.join(k.tmpdir(),`codevibe-claude-${e}.port`);try{w.writeFileSync(t,s.toString()),n.info(`Port file written: ${t} -> ${s}`)}catch(i){n.error(`Failed to write port file: ${t}`,i)}}removePortFile(){if(this.sessionId){let e=M.join(k.tmpdir(),`codevibe-claude-${this.sessionId}.port`);try{w.existsSync(e)&&(w.unlinkSync(e),n.info(`Port file removed: ${e}`))}catch(s){n.warn(`Failed to remove port file: ${e}`,s)}}}async stop(){return new Promise((e,s)=>{this.removePortFile(),this.server?this.server.close(t=>{t?(n.error("Error stopping HTTP server:",t),s(t)):(n.info("HTTP API stopped"),e())}):e()})}};var K=require("child_process"),U=require("@quantiya/codevibe-core");var b=class{async executePrompt(e,s){let t=(0,U.getConfig)(),i=t.claude.defaultTimeout;return n.info("Executing prompt from mobile",{sessionId:e,promptLength:s.length,timeout:i}),new Promise(r=>{let o=["--resume",e,"--print","--output-format","stream-json",s];n.debug("Spawning Claude command",{command:t.claude.command,args:o});let p=(0,K.spawn)(t.claude.command,o,{stdio:["pipe","pipe","pipe"],shell:!0}),c="",d="",u=!1,h=setTimeout(()=>{u=!0,n.warn("Command execution timed out",{sessionId:e,timeout:i}),p.kill("SIGTERM")},i);p.stdout?.on("data",g=>{let f=g.toString();c+=f,n.debug("Command stdout",{output:f.slice(0,200)})}),p.stderr?.on("data",g=>{let f=g.toString();d+=f,n.debug("Command stderr",{output:f.slice(0,200)})}),p.on("close",g=>{clearTimeout(h);let f={success:g===0&&!u,output:c,error:d,exitCode:g||void 0,timedOut:u};f.success?n.info("Command executed successfully",{sessionId:e,exitCode:g,outputLength:c.length}):n.error("Command execution failed",{sessionId:e,exitCode:g,timedOut:u,error:d.slice(0,500)}),r(f)}),p.on("error",g=>{clearTimeout(h),n.error("Failed to spawn command",{error:g.message}),r({success:!1,error:g.message,timedOut:!1})})})}detectInteractivePrompt(e){return[/\[Y\/n\]/i,/\[y\/N\]/i,/\(y\/n\)/i,/Continue\?/i,/Proceed\?/i].some(t=>t.test(e))}extractPromptText(e){let s=e.split(`
|
|
2
|
+
`);for(let t=s.length-1;t>=0;t--){let i=s[t].trim();if(this.detectInteractivePrompt(i))return i}return null}};var L=require("child_process"),H=require("util");var j=(0,H.promisify)(L.exec),C=class{async answerInteractivePrompt(e,s){n.info("Attempting to answer interactive prompt",{sessionId:e,response:s});try{let t=process.env.CODEVIBE_TMUX_SESSION;return n.info("Checking tmux session environment",{tmuxSession:t||"(not set)",allEnvKeys:Object.keys(process.env).filter(i=>i.includes("CODEVIBE")||i.includes("TMUX"))}),t?(n.info("Using tmux send-keys",{tmuxSession:t}),await this.sendViaTmux(t,s),n.info("Successfully sent response to interactive prompt",{sessionId:e,response:s}),!0):(n.error("No tmux session found - codevibe-claude wrapper is required",{sessionId:e,hint:"Start Claude Code using the codevibe-claude wrapper script"}),!1)}catch(t){return n.error("Failed to answer interactive prompt",{sessionId:e,error:t instanceof Error?t.message:String(t)}),!1}}async sendViaTmux(e,s){let t=s.replace(/\\/g,"\\\\").replace(/"/g,'\\"').replace(/\$/g,"\\$").replace(/`/g,"\\`");n.info("Sending via tmux",{sessionName:e,inputLength:s.length});try{let i=`tmux send-keys -t "${e}" -l "${t}"`,r=await j(i);n.info("tmux send-keys (text) completed",{stdout:r.stdout||"(empty)",stderr:r.stderr||"(empty)"}),await this.delay(500);let o=`tmux send-keys -t "${e}" Enter`,p=await j(o);n.info("tmux send-keys (Enter) completed",{stdout:p.stdout||"(empty)",stderr:p.stderr||"(empty)"})}catch(i){throw n.error("tmux send-keys failed",{sessionName:e,error:i}),i}}delay(e){return new Promise(s=>setTimeout(s,e))}isPromptResponse(e){let s=e.trim().toLowerCase();return!!(s==="y"||s==="n"||s==="yes"||s==="no"||/^[0-9]+$/.test(s)||/^[a-z]$/.test(s)||["exit","quit","q","continue","skip","abort","retry","cancel"].includes(s))}};var A=class m{constructor(e){this.activeSessions=new Map;this.assignedPort=0;this.sessionKey=null;this.claudeToBackendSessionId=new Map;this.pendingMobilePrompts=new Map;this.httpApi=new I,this.commandExecutor=new b,this.promptResponder=new C,this.initialSessionId=e}static{this.MOBILE_PROMPT_EXPIRY_MS=3e3}getPort(){return this.assignedPort}generateBackendSessionId(e){return`claude-${e}`}trackMobilePrompt(e,s){this.pendingMobilePrompts.has(e)||this.pendingMobilePrompts.set(e,[]),this.pendingMobilePrompts.get(e).push({prompt:s.trim(),timestamp:Date.now()}),n.debug("Tracking mobile prompt for deduplication",{sessionId:e,promptLength:s.length})}isRecentMobilePrompt(e,s){let t=this.pendingMobilePrompts.get(e);if(!t)return!1;let i=Date.now(),r=s.trim(),o=[],p=!1;for(let c of t)if(!(i-c.timestamp>m.MOBILE_PROMPT_EXPIRY_MS)){if(!p&&c.prompt===r){p=!0,n.debug("Found matching mobile prompt, filtering duplicate",{sessionId:e});continue}o.push(c)}return o.length>0?this.pendingMobilePrompts.set(e,o):this.pendingMobilePrompts.delete(e),p}writePortFile(e){let s=E.join(T.tmpdir(),`codevibe-claude-${e}.port`);try{v.writeFileSync(s,this.assignedPort.toString()),n.info(`Port file written: ${s} -> ${this.assignedPort}`)}catch(t){n.error(`Failed to write port file: ${s}`,t)}}removePortFile(e){let s=E.join(T.tmpdir(),`codevibe-claude-${e}.port`);try{v.existsSync(s)&&(v.unlinkSync(s),n.info(`Port file removed: ${s}`))}catch(t){n.warn(`Failed to remove port file: ${s}`,t)}}async start(){try{n.info("Starting CodeVibe MCP Server...",{environment:(0,a.getEnvironment)()}),this.appSyncClient=new a.AppSyncClient,await this.appSyncClient.authenticateWithStoredTokens()?(n.info("Authenticated with stored OAuth tokens",{userId:this.appSyncClient.getCurrentUserId(),email:this.appSyncClient.getCurrentUserEmail()}),await this.registerDeviceEncryptionKey()):(n.error('Authentication failed. Run "codevibe-claude login" first.'),console.error('Not authenticated. Run "codevibe-claude login" to sign in.'),process.exit(1)),this.httpApi.onEvent(this.handleEventFromHook.bind(this)),this.assignedPort=await this.httpApi.start(this.initialSessionId),n.info("MCP Server started successfully",{port:this.assignedPort,host:(0,a.getConfig)().server.host,dynamicPort:(0,a.getConfig)().server.dynamicPort,sessionId:this.initialSessionId,authenticated:this.appSyncClient.isAuthenticated(),userId:this.appSyncClient.getCurrentUserId()})}catch(e){throw n.error("Failed to start MCP Server:",e),e}}async stop(){n.info("Stopping MCP Server...");let e=Array.from(this.activeSessions.keys());n.info(`Marking ${e.length} active session(s) as INACTIVE...`);for(let s of e)try{await this.appSyncClient.updateSession({sessionId:s,status:a.SessionStatus.INACTIVE}),n.info("Session marked as INACTIVE during shutdown",{sessionId:s});let t=this.activeSessions.get(s);t&&this.removePortFile(t.claudeSessionId)}catch(t){n.warn("Failed to mark session as INACTIVE during shutdown",{sessionId:s,error:t})}this.appSyncClient.cleanupSubscriptions(),this.activeSessions.clear(),await this.httpApi.stop(),n.info("MCP Server stopped")}async handleEventFromHook(e){let{session_id:s,hook_event_name:t,type:i,content:r}=e;n.info("Processing hook event",{sessionId:s,hookEvent:t,type:i});try{t==="SessionStart"?await this.handleSessionStart(e):t==="SessionEnd"&&await this.handleSessionEnd(e);let o=this.claudeToBackendSessionId.get(s)||this.generateBackendSessionId(s);if(i===a.EventType.USER_PROMPT&&e.source===a.EventSource.DESKTOP&&t==="UserPromptSubmit"&&r&&this.isRecentMobilePrompt(o,r)){n.info("Skipping duplicate USER_PROMPT from mobile-originated prompt",{sessionId:o,contentLength:r.length});return}if(i===a.EventType.INTERACTIVE_PROMPT){let h=this.activeSessions.get(o);h&&(h.waitingForPromptResponse=!0,h.pendingPromptId=e.prompt_id,n.info("Interactive prompt detected - will parse options from tmux",{sessionId:o,promptId:e.prompt_id})),this.sendInteractivePromptAsync(o,e,r).catch(g=>{n.error("Failed to send interactive prompt with dynamic options",{error:g})});return}let p=r,c=e.metadata,d=!1;n.info("Hook event encryption state",{type:i,sessionId:o,hasSessionKey:!!this.sessionKey,sessionKeyLength:this.sessionKey?.length||0}),this.sessionKey?(p=a.cryptoService.encryptContent(r,this.sessionKey),c&&(c={encrypted:a.cryptoService.encryptMetadata(c,this.sessionKey)}),d=!0,n.info("Event encrypted for hook",{type:i,sessionId:o,isEncrypted:!0})):n.warn("No session key - event will NOT be encrypted",{type:i,sessionId:o});let u=await this.appSyncClient.createEvent({sessionId:o,type:i,source:e.source,content:p,metadata:c,promptId:e.prompt_id,isEncrypted:d?!0:void 0});if(i===a.EventType.USER_PROMPT&&e.source===a.EventSource.DESKTOP){let h=this.activeSessions.get(o);h?.waitingForPromptResponse&&(h.waitingForPromptResponse=!1,h.pendingPromptId=void 0,h.pendingSubmitMap=void 0,n.info("Clearing prompt wait state - new desktop prompt received",{sessionId:o}))}n.debug("Event sent to AppSync successfully")}catch(o){throw n.error("Failed to process hook event:",o),o}}async handleSessionStart(e){let s=e.session_id,t=this.generateBackendSessionId(s),i=e.metadata?.cwd||process.cwd();this.claudeToBackendSessionId.set(s,t),n.info("Session started",{claudeSessionId:s,sessionId:t,cwd:i});let r=Array.from(this.activeSessions.keys()).filter(c=>c!==t);if(r.length>0){n.info(`Marking ${r.length} previous session(s) as INACTIVE`);for(let c of r){try{await this.appSyncClient.updateSession({sessionId:c,status:a.SessionStatus.INACTIVE}),n.info("Previous session marked INACTIVE",{prevId:c,newSessionId:t})}catch(u){n.warn("Failed to mark previous session as INACTIVE",{prevId:c,error:u})}let d=this.activeSessions.get(c);d&&this.removePortFile(d.claudeSessionId),this.activeSessions.delete(c)}}this.writePortFile(s);let o=this.appSyncClient.getCurrentUserId(),p={sessionId:t,claudeSessionId:s,userId:o,projectPath:i,cwd:i,createdAt:new Date,subscriptionActive:!1,waitingForPromptResponse:!1,metadata:e.metadata||{}};this.activeSessions.set(t,p);try{let c=await(0,a.resumeOrCreateSession)({sessionId:t,userId:p.userId,agentType:a.AgentType.CLAUDE,projectPath:i,metadata:e.metadata||{}},this.appSyncClient,n);if(this.sessionKey=c.sessionKey,c.resumed&&!c.sessionKey){let d=await a.keychainManager.getDeviceId();n.error("Device key not found in session encryptedKeys",{sessionId:t,pluginDeviceId:d}),console.error(`
|
|
3
|
+
\u26A0\uFE0F E2E ENCRYPTION WARNING: Cannot decrypt this session!`),console.error(` Your device ID (${d.substring(0,8)}...) is not in session's encryption keys.`),console.error(" This happens if your device key was regenerated after the session was created."),console.error(` SOLUTION: Start a new Claude Code session instead of resuming this one.
|
|
4
|
+
`)}}catch(c){if(this.isSessionLimitExceeded(c)){this.displaySubscriptionLimitError(c,"session"),this.activeSessions.delete(t),this.removePortFile(s);return}n.error("Failed to create/resume session:",c)}this.subscribeToMobileEvents(t),this.appSyncClient.startHeartbeat(t)}async handleSessionEnd(e){let s=e.session_id,t=this.claudeToBackendSessionId.get(s)||this.generateBackendSessionId(s);n.info("Session ended",{claudeSessionId:s,sessionId:t,reason:e.metadata?.reason}),this.removePortFile(s);let i=this.activeSessions.get(t);if(i?.waitingForPromptResponse&&(n.info("Clearing prompt wait state - session ending",{sessionId:t}),i.waitingForPromptResponse=!1,i.pendingPromptId=void 0),this.appSyncClient.stopHeartbeat(t),i)try{await this.appSyncClient.updateSession({sessionId:t,status:a.SessionStatus.INACTIVE}),n.info("Session marked as INACTIVE in AppSync",{sessionId:t})}catch(r){n.warn("Failed to update session in AppSync:",r)}else n.warn("Cannot update session - session state not found",{sessionId:t});this.activeSessions.delete(t),this.claudeToBackendSessionId.delete(s),n.debug("Session cleanup completed",{sessionId:t})}async registerDeviceEncryptionKey(){try{let e=await a.keychainManager.getDeviceId(),s=await a.keychainManager.getDevicePublicKey(),t=a.keychainManager.getDevicePlatform(),i=a.keychainManager.getDeviceName();n.info("Registering device encryption key",{deviceId:e,platform:t,deviceName:i}),await this.appSyncClient.registerDeviceKey(e,s,t,i),a.keychainManager.setIsRegistered(!0),n.info("Device encryption key registered successfully",{deviceId:e})}catch(e){n.warn("Failed to register device encryption key (E2E encryption may not work):",e)}}subscribeToMobileEvents(e){n.info("Subscribing to mobile events",{sessionId:e});let s=this.activeSessions.get(e);if(!s){n.error("Session not found",{sessionId:e});return}this.appSyncClient.subscribeToEvents(e,async t=>{n.info("Received mobile event",{eventId:t.eventId,type:t.type,sessionId:t.sessionId,isEncrypted:t.isEncrypted});let i=t.content||"";if(t.isEncrypted&&this.sessionKey)try{i=a.cryptoService.decryptContent(t.content,this.sessionKey),n.debug("Event decrypted successfully",{eventId:t.eventId})}catch(o){n.error("Failed to decrypt event:",{eventId:t.eventId,error:o}),i=t.content}let r={...t,content:i};try{await this.appSyncClient.updateEventStatus({eventId:t.eventId,sessionId:t.sessionId,timestamp:t.timestamp,deliveryStatus:a.DeliveryStatus.DELIVERED}),n.info("Event marked as DELIVERED",{eventId:t.eventId})}catch(o){n.warn("Failed to mark event as DELIVERED",{eventId:t.eventId,error:o})}if(t.type===a.EventType.USER_PROMPT){let o=this.activeSessions.get(e);if(o?.waitingForPromptResponse){let p=i.trim(),c=o.pendingSubmitMap?Object.keys(o.pendingSubmitMap).length:3,d=this.parseInteractivePromptInput(p,c);if(n.info("Parsed interactive prompt input",{sessionId:e,content:p,parsed:d,hasSubmitMap:!!o.pendingSubmitMap}),d.action==="select_option"){let u=o.pendingSubmitMap?.[d.option]||d.option;n.info("User selected option",{option:d.option,terminalInput:u}),await this.promptResponder.answerInteractivePrompt(e,u)?(await this.markEventExecuted(t),o.waitingForPromptResponse=!1,o.pendingPromptId=void 0,o.pendingSubmitMap=void 0,await this.appSyncClient.createEvent({sessionId:e,type:a.EventType.NOTIFICATION,source:a.EventSource.DESKTOP,content:`Selected option ${d.option}`,metadata:{promptAnswered:!0}})):await this.sendPromptError(e,"Failed to select option")}else if(d.action==="option_with_followup"){let u=o.pendingSubmitMap?.[d.option]||d.option;n.info("User selected option with follow-up",{option:d.option,terminalInput:u,followUpText:d.followUpText});let h=await this.promptResponder.answerInteractivePrompt(e,u);if(o.waitingForPromptResponse=!1,o.pendingPromptId=void 0,o.pendingSubmitMap=void 0,h){if(await this.appSyncClient.createEvent({sessionId:e,type:a.EventType.NOTIFICATION,source:a.EventSource.DESKTOP,content:`Selected option ${d.option}`,metadata:{promptAnswered:!0}}),d.followUpText){await new Promise(f=>setTimeout(f,1e3));let g={...t,content:d.followUpText};await this.executeMobilePrompt(e,g)}await this.markEventExecuted(t)}else await this.sendPromptError(e,"Failed to select option")}else n.info("Sending as free-form response to interactive prompt",{response:p}),await this.promptResponder.answerInteractivePrompt(e,p)?(await this.markEventExecuted(t),o.waitingForPromptResponse=!1,o.pendingPromptId=void 0,o.pendingSubmitMap=void 0,await this.appSyncClient.createEvent({sessionId:e,type:a.EventType.NOTIFICATION,source:a.EventSource.DESKTOP,content:"Response sent to interactive prompt",metadata:{promptAnswered:!0}})):await this.sendPromptError(e,"Failed to send response")}else await this.executeMobilePrompt(e,r)}},t=>{n.error("Subscription error",{sessionId:e,error:t})}),s.subscriptionActive=!0,n.info("Subscription active",{sessionId:e})}async sendInteractivePromptAsync(e,s,t){await new Promise(u=>setTimeout(u,500));let i=process.env.CODEVIBE_TMUX_SESSION,r={...s.metadata||{}};if(i)try{let{exec:u}=await import("child_process"),h=x=>new Promise((V,q)=>{u(x,{timeout:5e3},(_,X)=>{_?q(_):V({stdout:X||""})})}),{stdout:g}=await h(`tmux capture-pane -p -e -S -30 -t '${i}'`),f=g.split(`
|
|
5
|
+
`);n.info("tmux capture result",{tmuxSession:i,totalLines:f.length,lastLines:f.slice(-15).map(x=>x.replace(/\x1B[^m]*m/g,"").trim()).filter(Boolean)});let S=(0,a.parseInteractivePrompt)(g);S&&S.options.length>0?(r.options=S.options,r.submitMap=S.submitMap,r.instructions=this.buildPromptInstructions(S),n.info("Parsed dynamic options from tmux",{optionCount:S.options.length,kind:S.kind,options:S.options})):(n.info("No dynamic options parsed from tmux, using fallback",{parsedResult:S}),this.addFallbackOptions(r))}catch(u){n.warn("Failed to capture tmux pane for options",{error:u}),this.addFallbackOptions(r)}else n.warn("No tmux session \u2014 using fallback options"),this.addFallbackOptions(r);let o=this.activeSessions.get(e);o&&r.submitMap&&(o.pendingSubmitMap=r.submitMap);let p=t,c=r,d=!1;this.sessionKey&&(p=a.cryptoService.encryptContent(t,this.sessionKey),c={encrypted:a.cryptoService.encryptMetadata(c,this.sessionKey)},d=!0),await this.appSyncClient.createEvent({sessionId:e,type:a.EventType.INTERACTIVE_PROMPT,source:s.source,content:p,metadata:c,promptId:s.prompt_id,isEncrypted:d?!0:void 0}),n.info("Interactive prompt sent to AppSync with dynamic options",{sessionId:e})}addFallbackOptions(e){e.options=[{number:"1",text:"Yes"},{number:"2",text:"Yes, and don't ask again"},{number:"3",text:"Reject and tell Claude what to do differently"}],e.submitMap={1:"1",2:"2",3:"3"},e.instructions="Reply with 1, 2, or 3. Append a message to provide alternative instructions."}buildPromptInstructions(e){return`Reply with ${e.options.map(t=>t.number).join(", ")}. Append a message to provide alternative instructions.`}parseInteractivePromptInput(e,s=3){return B(e,s)}async markEventExecuted(e){try{await this.appSyncClient.updateEventStatus({eventId:e.eventId,sessionId:e.sessionId,timestamp:e.timestamp,deliveryStatus:a.DeliveryStatus.EXECUTED}),n.info("Event marked as EXECUTED",{eventId:e.eventId})}catch(s){n.warn("Failed to mark event as EXECUTED",{eventId:e.eventId,error:s})}}async sendPromptError(e,s){await this.appSyncClient.createEvent({sessionId:e,type:a.EventType.NOTIFICATION,source:a.EventSource.DESKTOP,content:s,metadata:{error:!0}})}isSessionLimitExceeded(e){return this.getErrorMessage(e).includes("SESSION_LIMIT_EXCEEDED")}isUsageLimitExceeded(e){let s=this.getErrorMessage(e);return s.includes("MESSAGE_LIMIT_EXCEEDED")||s.includes("IMAGE_LIMIT_EXCEEDED")}getErrorMessage(e){if(e instanceof Error)return e.message;if(typeof e=="object"&&e!==null){let s=e;if(s.errors&&Array.isArray(s.errors))return s.errors.map(t=>t.message||"").join(" ");if(typeof s.message=="string")return s.message}return String(e)}displaySubscriptionLimitError(e,s){let t=this.getErrorMessage(e),i="",r=t.match(/for your (\w+) plan/i);r&&(i=` (${r[1]} tier)`);let o="",p=t.match(/of (\d+)/);switch(p&&(o=` [Limit: ${p[1]}]`),console.log(`
|
|
6
|
+
`+"=".repeat(60)),console.log("\u26A0\uFE0F SUBSCRIPTION LIMIT REACHED"),console.log("=".repeat(60)),s){case"session":console.log(`You have reached the maximum number of active sessions${i}.`),console.log(`${o}`),console.log(`
|
|
7
|
+
To continue, please:`),console.log(" \u2022 Close an existing Claude Code session, or"),console.log(" \u2022 Upgrade your subscription in the CodeVibe iOS app");break;case"message":console.log(`You have reached your monthly message limit${i}.`),console.log(`${o}`),console.log(`
|
|
8
|
+
To continue, please:`),console.log(" \u2022 Wait until your usage resets next month, or"),console.log(" \u2022 Upgrade your subscription in the CodeVibe iOS app");break;case"image":console.log(`You have reached your monthly image attachment limit${i}.`),console.log(`${o}`),console.log(`
|
|
9
|
+
To continue, please:`),console.log(" \u2022 Wait until your usage resets next month, or"),console.log(" \u2022 Upgrade your subscription in the CodeVibe iOS app");break}console.log(`
|
|
10
|
+
Note: You can still use Claude Code normally from your desktop.`),console.log("This limit only affects syncing with the mobile app."),console.log("=".repeat(60)+`
|
|
11
|
+
`),n.error("Subscription limit exceeded",{limitType:s,errorMessage:t})}async downloadAttachment(e,s,t){try{let i=e.isEncrypted??t??!1;n.info("Downloading attachment - START",{id:e.id,type:e.type,filename:e.filename,s3Key:e.s3Key,attachmentIsEncrypted:e.isEncrypted,eventIsEncrypted:t,shouldDecrypt:i,hasSessionKey:!!this.sessionKey});let{downloadUrl:r}=await this.appSyncClient.getAttachmentDownloadUrl(e.s3Key),o=await fetch(r);if(!o.ok)throw new Error(`Failed to download attachment: ${o.status} ${o.statusText}`);let p=Buffer.from(await o.arrayBuffer());if(n.info("Attachment downloaded",{id:e.id,downloadedSize:p.length,first20Bytes:p.slice(0,20).toString("hex")}),n.info("Checking decryption conditions",{id:e.id,shouldDecrypt:i,hasSessionKey:!!this.sessionKey,willDecrypt:!!(i&&this.sessionKey)}),i&&this.sessionKey)try{n.info("Decrypting attachment",{id:e.id,encryptedSize:p.length}),p=a.cryptoService.decryptData(p,this.sessionKey),n.info("Attachment decrypted successfully",{id:e.id,decryptedSize:p.length,first20Bytes:p.slice(0,20).toString("hex")})}catch(f){throw n.error("Failed to decrypt attachment:",{id:e.id,error:f}),new Error("Failed to decrypt attachment")}else i&&!this.sessionKey?n.warn("Cannot decrypt attachment - no session key available",{id:e.id}):n.info("Skipping decryption - attachment not encrypted or no session key",{id:e.id,shouldDecrypt:i,hasSessionKey:!!this.sessionKey});let c=E.join(T.tmpdir(),"codevibe-claude",s);v.existsSync(c)||v.mkdirSync(c,{recursive:!0});let d="",u=e.filename;if(i&&e.filename&&this.sessionKey)try{u=a.cryptoService.decryptContent(e.filename,this.sessionKey)}catch{u=e.filename}if(u){let f=E.extname(u);f&&(d=f)}d||(d={"image/jpeg":".jpg","image/png":".png","image/gif":".gif","image/webp":".webp","image/heic":".heic","application/pdf":".pdf"}[e.type]||".bin");let h=`attachment-${e.id}${d}`,g=E.join(c,h);return v.writeFileSync(g,p),n.info("Attachment saved to temp file",{id:e.id,filePath:g,size:p.length,wasDecrypted:i&&!!this.sessionKey}),g}catch(i){return n.error("Failed to download attachment:",{id:e.id,error:i}),null}}async executeMobilePrompt(e,s){let t=s.content||"",i=s.attachments||[];n.info("Executing mobile prompt via tmux",{sessionId:e,promptLength:t.length,attachmentCount:i.length});let r=[];if(i.length>0){n.info("Downloading attachments for prompt",{count:i.length});for(let o of i){let p=await this.downloadAttachment(o,e,s.isEncrypted);p&&r.push(p)}if(r.length>0){let o=r.map(p=>`[Attached file: ${p}]`).join(`
|
|
12
|
+
`);t?t=`${o}
|
|
13
|
+
|
|
14
|
+
${t}`:t=`${o}
|
|
15
|
+
|
|
16
|
+
Please analyze the attached file(s).`,n.info("Prompt updated with attachment paths",{attachmentCount:r.length,newPromptLength:t.length})}}this.trackMobilePrompt(e,t);try{if(await this.promptResponder.answerInteractivePrompt(e,t)){try{await this.appSyncClient.updateEventStatus({eventId:s.eventId,sessionId:s.sessionId,timestamp:s.timestamp,deliveryStatus:a.DeliveryStatus.EXECUTED}),n.info("Event marked as EXECUTED",{eventId:s.eventId})}catch(c){n.warn("Failed to mark event as EXECUTED",{eventId:s.eventId,error:c})}n.info("Mobile prompt sent successfully",{sessionId:e});let p=r.length>0?`Prompt with ${r.length} attachment(s) sent to Claude Code`:`Prompt "${t.substring(0,50)}${t.length>50?"...":""}" sent to Claude Code`;await this.appSyncClient.createEvent({sessionId:e,type:a.EventType.NOTIFICATION,source:a.EventSource.DESKTOP,content:p,metadata:{mobilePrompt:!0,attachmentCount:r.length}})}else n.error("Failed to send mobile prompt",{sessionId:e}),await this.appSyncClient.createEvent({sessionId:e,type:a.EventType.NOTIFICATION,source:a.EventSource.DESKTOP,content:"Failed to send prompt to Claude Code",metadata:{error:!0}})}catch(o){n.error("Failed to execute mobile prompt:",o)}}};async function ee(){let m=process.argv[2]||process.env.CLAUDE_SESSION_ID;m?n.info(`Starting MCP server for session: ${m}`):n.info("Starting MCP server without initial session ID (will be set on SessionStart)");let e=new A(m);try{await e.start();let s=e.getPort();console.log(`PORT=${s}`);let t=!1,i=async r=>{if(t){n.info("Shutdown already in progress, ignoring additional signal");return}t=!0,n.info(`Received ${r} signal, stopping server...`);try{await e.stop(),n.info("Graceful shutdown completed"),process.exit(0)}catch(o){n.error("Error during shutdown:",o),process.exit(1)}};process.on("SIGINT",()=>i("SIGINT")),process.on("SIGTERM",()=>i("SIGTERM")),process.on("SIGHUP",()=>i("SIGHUP")),process.on("uncaughtException",async r=>{n.error("Uncaught exception:",r),await i("uncaughtException")}),process.on("unhandledRejection",async r=>{n.error("Unhandled rejection:",r),await i("unhandledRejection")})}catch(s){n.error("Failed to start MCP Server:",s),process.exit(1)}}function B(m,e=3){let s=m.trim(),t=s.match(/^(\d+)$/);if(t){let r=parseInt(t[1]);if(r>=1&&r<=e)return{action:"select_option",option:t[1]}}let i=s.match(/^(\d+)[,.:;\-\s\n]+(.+)$/s);if(i){let r=parseInt(i[1]);if(r>=1&&r<=e)return{action:"option_with_followup",option:i[1],followUpText:i[2].trim()}}return{action:"send_as_response"}}ee().catch(m=>{n.error("Unhandled error in main:",m),process.exit(1)});0&&(module.exports={parseInteractivePromptInput});
|