@quantiya/codevibe-claude-plugin 1.0.37 → 1.0.38
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/bin/codevibe-claude +17 -3
- package/dist/server.js +8 -8
- package/hooks/stop.sh +30 -10
- package/node_modules/@quantiya/codevibe-core/dist/appsync/appsync-client.d.ts +1 -139
- package/node_modules/@quantiya/codevibe-core/dist/appsync/queries.d.ts +0 -5
- package/node_modules/@quantiya/codevibe-core/dist/auth/auth-telemetry.d.ts +64 -29
- package/node_modules/@quantiya/codevibe-core/dist/index.d.ts +0 -4
- package/node_modules/@quantiya/codevibe-core/dist/index.js +33 -194
- package/node_modules/@quantiya/codevibe-core/dist/keychain/keychain-manager.d.ts +16 -2
- package/node_modules/@quantiya/codevibe-core/dist/session/session-rekey.d.ts +40 -0
- package/node_modules/@quantiya/codevibe-core/dist/session/session-resume.d.ts +1 -0
- package/node_modules/@quantiya/codevibe-core/dist/types/index.d.ts +0 -2
- package/node_modules/@quantiya/codevibe-core/dist/types/session.d.ts +0 -16
- package/node_modules/@quantiya/codevibe-core/package.json +1 -1
- package/node_modules/body-parser/README.md +18 -18
- package/node_modules/body-parser/index.js +6 -15
- package/node_modules/body-parser/lib/read.js +17 -20
- package/node_modules/body-parser/lib/types/json.js +8 -16
- package/node_modules/body-parser/lib/types/raw.js +3 -4
- package/node_modules/body-parser/lib/types/text.js +3 -4
- package/node_modules/body-parser/lib/types/urlencoded.js +8 -8
- package/node_modules/body-parser/lib/utils.js +11 -9
- package/node_modules/body-parser/package.json +2 -2
- package/node_modules/content-disposition/README.md +7 -8
- package/node_modules/content-disposition/index.js +118 -40
- package/node_modules/content-disposition/package.json +8 -11
- package/node_modules/express/Readme.md +39 -29
- package/node_modules/express/lib/application.js +1 -1
- package/node_modules/express/lib/request.js +5 -6
- package/node_modules/express/lib/response.js +14 -0
- package/node_modules/express/lib/utils.js +3 -1
- package/node_modules/express/package.json +6 -5
- package/node_modules/finalhandler/HISTORY.md +6 -0
- package/node_modules/finalhandler/README.md +26 -23
- package/node_modules/finalhandler/package.json +13 -9
- package/node_modules/graphql/execution/execute.d.ts +14 -1
- package/node_modules/graphql/execution/execute.js +63 -13
- package/node_modules/graphql/execution/execute.mjs +63 -13
- package/node_modules/graphql/execution/subscribe.js +1 -0
- package/node_modules/graphql/execution/subscribe.mjs +2 -0
- package/node_modules/graphql/execution/values.js +4 -4
- package/node_modules/graphql/execution/values.mjs +4 -4
- package/node_modules/graphql/index.d.ts +1 -0
- package/node_modules/graphql/language/ast.d.ts +10 -1
- package/node_modules/graphql/language/ast.js +8 -1
- package/node_modules/graphql/language/ast.mjs +8 -1
- package/node_modules/graphql/language/directiveLocation.d.ts +1 -0
- package/node_modules/graphql/language/directiveLocation.js +1 -0
- package/node_modules/graphql/language/directiveLocation.mjs +1 -0
- package/node_modules/graphql/language/index.d.ts +1 -0
- package/node_modules/graphql/language/kinds.d.ts +1 -0
- package/node_modules/graphql/language/kinds.js +1 -0
- package/node_modules/graphql/language/kinds.mjs +1 -0
- package/node_modules/graphql/language/parser.d.ts +14 -0
- package/node_modules/graphql/language/parser.js +33 -0
- package/node_modules/graphql/language/parser.mjs +33 -0
- package/node_modules/graphql/language/predicates.js +3 -1
- package/node_modules/graphql/language/predicates.mjs +5 -1
- package/node_modules/graphql/language/printer.js +13 -1
- package/node_modules/graphql/language/printer.mjs +13 -1
- package/node_modules/graphql/package.json +1 -1
- package/node_modules/graphql/type/directives.d.ts +9 -1
- package/node_modules/graphql/type/directives.js +10 -1
- package/node_modules/graphql/type/directives.mjs +10 -1
- package/node_modules/graphql/type/introspection.js +24 -1
- package/node_modules/graphql/type/introspection.mjs +24 -1
- package/node_modules/graphql/utilities/buildASTSchema.js +4 -0
- package/node_modules/graphql/utilities/buildASTSchema.mjs +4 -0
- package/node_modules/graphql/utilities/buildClientSchema.js +1 -0
- package/node_modules/graphql/utilities/buildClientSchema.mjs +1 -0
- package/node_modules/graphql/utilities/coerceInputValue.js +2 -2
- package/node_modules/graphql/utilities/coerceInputValue.mjs +2 -2
- package/node_modules/graphql/utilities/extendSchema.js +58 -3
- package/node_modules/graphql/utilities/extendSchema.mjs +58 -3
- package/node_modules/graphql/utilities/getIntrospectionQuery.d.ts +16 -0
- package/node_modules/graphql/utilities/getIntrospectionQuery.js +31 -38
- package/node_modules/graphql/utilities/getIntrospectionQuery.mjs +31 -38
- package/node_modules/graphql/utilities/introspectionFromSchema.js +1 -0
- package/node_modules/graphql/utilities/introspectionFromSchema.mjs +1 -0
- package/node_modules/graphql/utilities/printSchema.js +1 -0
- package/node_modules/graphql/utilities/printSchema.mjs +1 -0
- package/node_modules/graphql/utilities/valueFromAST.js +12 -2
- package/node_modules/graphql/utilities/valueFromAST.mjs +12 -2
- package/node_modules/graphql/validation/rules/KnownDirectivesRule.js +4 -0
- package/node_modules/graphql/validation/rules/KnownDirectivesRule.mjs +4 -0
- package/node_modules/graphql/validation/rules/UniqueDirectivesPerLocationRule.js +12 -0
- package/node_modules/graphql/validation/rules/UniqueDirectivesPerLocationRule.mjs +12 -0
- package/node_modules/graphql/validation/rules/ValuesOfCorrectTypeRule.js +5 -11
- package/node_modules/graphql/validation/rules/ValuesOfCorrectTypeRule.mjs +5 -11
- package/node_modules/graphql/validation/validate.js +12 -0
- package/node_modules/graphql/validation/validate.mjs +13 -2
- package/node_modules/graphql/version.js +2 -2
- package/node_modules/graphql/version.mjs +2 -2
- package/node_modules/hasown/CHANGELOG.md +11 -0
- package/node_modules/hasown/eslint.config.mjs +6 -0
- package/node_modules/hasown/index.d.ts +1 -0
- package/node_modules/hasown/package.json +14 -14
- package/node_modules/iconv-lite/lib/index.d.ts +114 -26
- package/node_modules/iconv-lite/lib/index.js +39 -40
- package/node_modules/iconv-lite/package.json +13 -2
- package/node_modules/iconv-lite/types/encodings.d.ts +423 -0
- package/node_modules/node-abi/abi_registry.json +10 -3
- package/node_modules/{semver → node-abi/node_modules/semver}/README.md +19 -4
- package/node_modules/{semver → node-abi/node_modules/semver}/bin/semver.js +14 -10
- package/node_modules/node-abi/node_modules/semver/functions/truncate.js +48 -0
- package/node_modules/{semver → node-abi/node_modules/semver}/index.js +2 -0
- package/node_modules/{semver → node-abi/node_modules/semver}/internal/re.js +1 -1
- package/node_modules/{semver → node-abi/node_modules/semver}/package.json +3 -3
- package/node_modules/{semver → node-abi/node_modules/semver}/range.bnf +5 -4
- package/node_modules/node-abi/package.json +1 -1
- package/node_modules/path-to-regexp/Readme.md +3 -3
- package/node_modules/path-to-regexp/dist/index.d.ts +3 -0
- package/node_modules/path-to-regexp/dist/index.js +215 -193
- package/node_modules/path-to-regexp/dist/index.js.map +1 -1
- package/node_modules/path-to-regexp/package.json +2 -2
- package/node_modules/qs/.editorconfig +1 -1
- package/node_modules/qs/.github/SECURITY.md +11 -0
- package/node_modules/qs/.github/THREAT_MODEL.md +78 -0
- package/node_modules/qs/CHANGELOG.md +190 -0
- package/node_modules/qs/README.md +29 -4
- package/node_modules/qs/dist/qs.js +21 -21
- package/node_modules/qs/eslint.config.mjs +56 -0
- package/node_modules/qs/lib/parse.js +94 -49
- package/node_modules/qs/lib/utils.js +85 -11
- package/node_modules/qs/package.json +10 -9
- package/node_modules/qs/test/parse.js +391 -13
- package/node_modules/qs/test/stringify.js +16 -3
- package/node_modules/qs/test/utils.js +173 -3
- package/node_modules/send/package.json +11 -8
- package/node_modules/serve-static/README.md +23 -23
- package/node_modules/serve-static/package.json +6 -3
- package/node_modules/side-channel-list/CHANGELOG.md +25 -4
- package/node_modules/side-channel-list/index.js +1 -3
- package/node_modules/side-channel-list/package.json +8 -8
- package/node_modules/side-channel-list/test/index.js +50 -0
- package/node_modules/uuid/dist/v35.js +3 -0
- package/node_modules/uuid/dist/v6.js +3 -0
- package/node_modules/uuid/dist-node/v35.js +3 -0
- package/node_modules/uuid/dist-node/v6.js +3 -0
- package/node_modules/uuid/package.json +1 -1
- package/node_modules/ws/index.js +15 -6
- package/node_modules/ws/lib/constants.js +1 -0
- package/node_modules/ws/lib/permessage-deflate.js +6 -6
- package/node_modules/ws/lib/websocket-server.js +10 -6
- package/node_modules/ws/lib/websocket.js +19 -14
- package/node_modules/ws/package.json +4 -3
- package/node_modules/ws/wrapper.mjs +14 -1
- package/package.json +2 -2
- package/node_modules/@quantiya/codevibe-core/dist/appsync/__tests__/appsync-client.test.d.ts +0 -1
- package/node_modules/@quantiya/codevibe-core/dist/audit-keys/__tests__/audit-keys-parity.test.d.ts +0 -1
- package/node_modules/@quantiya/codevibe-core/dist/audit-keys/index.d.ts +0 -41
- package/node_modules/@quantiya/codevibe-core/dist/auth/__tests__/auth-telemetry.test.d.ts +0 -1
- package/node_modules/@quantiya/codevibe-core/dist/orchestration/__tests__/setup-bootstrap.test.d.ts +0 -1
- package/node_modules/@quantiya/codevibe-core/dist/orchestration/__tests__/setup-failure-recourse.test.d.ts +0 -1
- package/node_modules/@quantiya/codevibe-core/dist/orchestration/__tests__/setup-save.test.d.ts +0 -1
- package/node_modules/@quantiya/codevibe-core/dist/orchestration/__tests__/setup-seat-picker.test.d.ts +0 -1
- package/node_modules/@quantiya/codevibe-core/dist/orchestration/__tests__/setup-telemetry.test.d.ts +0 -1
- package/node_modules/@quantiya/codevibe-core/dist/orchestration/__tests__/setup-test-agents.test.d.ts +0 -1
- package/node_modules/@quantiya/codevibe-core/dist/orchestration/__tests__/setup-types.test.d.ts +0 -1
- package/node_modules/@quantiya/codevibe-core/dist/orchestration/__tests__/setup-wizard.test.d.ts +0 -1
- package/node_modules/@quantiya/codevibe-core/dist/orchestration/__tests__/v1-options.test.d.ts +0 -1
- package/node_modules/@quantiya/codevibe-core/dist/orchestration/detect-agents.d.ts +0 -56
- package/node_modules/@quantiya/codevibe-core/dist/orchestration/index.d.ts +0 -3
- package/node_modules/@quantiya/codevibe-core/dist/orchestration/orchestration-cli.d.ts +0 -12
- package/node_modules/@quantiya/codevibe-core/dist/orchestration/setup-bootstrap.d.ts +0 -146
- package/node_modules/@quantiya/codevibe-core/dist/orchestration/setup-failure-recourse.d.ts +0 -23
- package/node_modules/@quantiya/codevibe-core/dist/orchestration/setup-save.d.ts +0 -47
- package/node_modules/@quantiya/codevibe-core/dist/orchestration/setup-seat-picker.d.ts +0 -72
- package/node_modules/@quantiya/codevibe-core/dist/orchestration/setup-telemetry.d.ts +0 -54
- package/node_modules/@quantiya/codevibe-core/dist/orchestration/setup-test-agents.d.ts +0 -108
- package/node_modules/@quantiya/codevibe-core/dist/orchestration/setup-types.d.ts +0 -140
- package/node_modules/@quantiya/codevibe-core/dist/orchestration/setup-wizard.d.ts +0 -57
- package/node_modules/@quantiya/codevibe-core/dist/orchestration/v1-options.d.ts +0 -108
- package/node_modules/@quantiya/codevibe-core/dist/reviewer/__tests__/integration.test.d.ts +0 -1
- package/node_modules/@quantiya/codevibe-core/dist/reviewer/__tests__/mocks.test.d.ts +0 -1
- package/node_modules/@quantiya/codevibe-core/dist/reviewer/__tests__/output-parser.test.d.ts +0 -1
- package/node_modules/@quantiya/codevibe-core/dist/reviewer/__tests__/registry.test.d.ts +0 -1
- package/node_modules/@quantiya/codevibe-core/dist/reviewer/__tests__/subprocess.test.d.ts +0 -1
- package/node_modules/@quantiya/codevibe-core/dist/reviewer/index.d.ts +0 -15
- package/node_modules/@quantiya/codevibe-core/dist/reviewer/mocks.d.ts +0 -80
- package/node_modules/@quantiya/codevibe-core/dist/reviewer/output-parser.d.ts +0 -95
- package/node_modules/@quantiya/codevibe-core/dist/reviewer/provider.d.ts +0 -153
- package/node_modules/@quantiya/codevibe-core/dist/reviewer/providers/__tests__/claude-live-smoke.test.d.ts +0 -1
- package/node_modules/@quantiya/codevibe-core/dist/reviewer/providers/__tests__/claude.test.d.ts +0 -1
- package/node_modules/@quantiya/codevibe-core/dist/reviewer/providers/__tests__/codex-live-smoke.test.d.ts +0 -1
- package/node_modules/@quantiya/codevibe-core/dist/reviewer/providers/__tests__/codex.test.d.ts +0 -1
- package/node_modules/@quantiya/codevibe-core/dist/reviewer/providers/__tests__/gemini-live-smoke.test.d.ts +0 -1
- package/node_modules/@quantiya/codevibe-core/dist/reviewer/providers/__tests__/gemini.test.d.ts +0 -1
- package/node_modules/@quantiya/codevibe-core/dist/reviewer/providers/claude.d.ts +0 -59
- package/node_modules/@quantiya/codevibe-core/dist/reviewer/providers/codex.d.ts +0 -67
- package/node_modules/@quantiya/codevibe-core/dist/reviewer/providers/common.d.ts +0 -25
- package/node_modules/@quantiya/codevibe-core/dist/reviewer/providers/gemini.d.ts +0 -108
- package/node_modules/@quantiya/codevibe-core/dist/reviewer/registry.d.ts +0 -87
- package/node_modules/@quantiya/codevibe-core/dist/reviewer/subprocess.d.ts +0 -117
- package/node_modules/@quantiya/codevibe-core/dist/reviewer/types.d.ts +0 -101
- package/node_modules/@quantiya/codevibe-core/dist/types/orchestration.d.ts +0 -57
- package/node_modules/@quantiya/codevibe-core/dist/types/reviewer.d.ts +0 -67
- package/node_modules/content-disposition/HISTORY.md +0 -72
- package/node_modules/express/History.md +0 -3858
- package/node_modules/hasown/.eslintrc +0 -5
- package/node_modules/iconv-lite/Changelog.md +0 -236
- package/node_modules/qs/.eslintrc +0 -39
- package/node_modules/send/HISTORY.md +0 -580
- package/node_modules/serve-static/HISTORY.md +0 -516
- /package/node_modules/{semver → node-abi/node_modules/semver}/LICENSE +0 -0
- /package/node_modules/{semver → node-abi/node_modules/semver}/classes/comparator.js +0 -0
- /package/node_modules/{semver → node-abi/node_modules/semver}/classes/index.js +0 -0
- /package/node_modules/{semver → node-abi/node_modules/semver}/classes/range.js +0 -0
- /package/node_modules/{semver → node-abi/node_modules/semver}/classes/semver.js +0 -0
- /package/node_modules/{semver → node-abi/node_modules/semver}/functions/clean.js +0 -0
- /package/node_modules/{semver → node-abi/node_modules/semver}/functions/cmp.js +0 -0
- /package/node_modules/{semver → node-abi/node_modules/semver}/functions/coerce.js +0 -0
- /package/node_modules/{semver → node-abi/node_modules/semver}/functions/compare-build.js +0 -0
- /package/node_modules/{semver → node-abi/node_modules/semver}/functions/compare-loose.js +0 -0
- /package/node_modules/{semver → node-abi/node_modules/semver}/functions/compare.js +0 -0
- /package/node_modules/{semver → node-abi/node_modules/semver}/functions/diff.js +0 -0
- /package/node_modules/{semver → node-abi/node_modules/semver}/functions/eq.js +0 -0
- /package/node_modules/{semver → node-abi/node_modules/semver}/functions/gt.js +0 -0
- /package/node_modules/{semver → node-abi/node_modules/semver}/functions/gte.js +0 -0
- /package/node_modules/{semver → node-abi/node_modules/semver}/functions/inc.js +0 -0
- /package/node_modules/{semver → node-abi/node_modules/semver}/functions/lt.js +0 -0
- /package/node_modules/{semver → node-abi/node_modules/semver}/functions/lte.js +0 -0
- /package/node_modules/{semver → node-abi/node_modules/semver}/functions/major.js +0 -0
- /package/node_modules/{semver → node-abi/node_modules/semver}/functions/minor.js +0 -0
- /package/node_modules/{semver → node-abi/node_modules/semver}/functions/neq.js +0 -0
- /package/node_modules/{semver → node-abi/node_modules/semver}/functions/parse.js +0 -0
- /package/node_modules/{semver → node-abi/node_modules/semver}/functions/patch.js +0 -0
- /package/node_modules/{semver → node-abi/node_modules/semver}/functions/prerelease.js +0 -0
- /package/node_modules/{semver → node-abi/node_modules/semver}/functions/rcompare.js +0 -0
- /package/node_modules/{semver → node-abi/node_modules/semver}/functions/rsort.js +0 -0
- /package/node_modules/{semver → node-abi/node_modules/semver}/functions/satisfies.js +0 -0
- /package/node_modules/{semver → node-abi/node_modules/semver}/functions/sort.js +0 -0
- /package/node_modules/{semver → node-abi/node_modules/semver}/functions/valid.js +0 -0
- /package/node_modules/{semver → node-abi/node_modules/semver}/internal/constants.js +0 -0
- /package/node_modules/{semver → node-abi/node_modules/semver}/internal/debug.js +0 -0
- /package/node_modules/{semver → node-abi/node_modules/semver}/internal/identifiers.js +0 -0
- /package/node_modules/{semver → node-abi/node_modules/semver}/internal/lrucache.js +0 -0
- /package/node_modules/{semver → node-abi/node_modules/semver}/internal/parse-options.js +0 -0
- /package/node_modules/{semver → node-abi/node_modules/semver}/preload.js +0 -0
- /package/node_modules/{semver → node-abi/node_modules/semver}/ranges/gtr.js +0 -0
- /package/node_modules/{semver → node-abi/node_modules/semver}/ranges/intersects.js +0 -0
- /package/node_modules/{semver → node-abi/node_modules/semver}/ranges/ltr.js +0 -0
- /package/node_modules/{semver → node-abi/node_modules/semver}/ranges/max-satisfying.js +0 -0
- /package/node_modules/{semver → node-abi/node_modules/semver}/ranges/min-satisfying.js +0 -0
- /package/node_modules/{semver → node-abi/node_modules/semver}/ranges/min-version.js +0 -0
- /package/node_modules/{semver → node-abi/node_modules/semver}/ranges/outside.js +0 -0
- /package/node_modules/{semver → node-abi/node_modules/semver}/ranges/simplify.js +0 -0
- /package/node_modules/{semver → node-abi/node_modules/semver}/ranges/subset.js +0 -0
- /package/node_modules/{semver → node-abi/node_modules/semver}/ranges/to-comparators.js +0 -0
- /package/node_modules/{semver → node-abi/node_modules/semver}/ranges/valid.js +0 -0
- /package/node_modules/{strip-json-comments → rc/node_modules/strip-json-comments}/index.js +0 -0
- /package/node_modules/{strip-json-comments → rc/node_modules/strip-json-comments}/license +0 -0
- /package/node_modules/{strip-json-comments → rc/node_modules/strip-json-comments}/package.json +0 -0
- /package/node_modules/{strip-json-comments → rc/node_modules/strip-json-comments}/readme.md +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codevibe-claude",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.38",
|
|
4
4
|
"description": "Sync Claude Code sessions with iOS mobile app via AWS backend. Control Claude Code from your phone with real-time bidirectional synchronization.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "CodeVibe Team"
|
package/bin/codevibe-claude
CHANGED
|
@@ -291,6 +291,16 @@ trap cleanup EXIT INT TERM
|
|
|
291
291
|
# Generate a unique session name
|
|
292
292
|
SESSION_NAME="${TMUX_SESSION_PREFIX}-$$"
|
|
293
293
|
|
|
294
|
+
# ─── CodeVibe system-prompt injection ─────────────────────────────────
|
|
295
|
+
# Append a small instruction to Claude's default system prompt that
|
|
296
|
+
# narrows AskUserQuestion to single-select only. The CodeVibe mobile
|
|
297
|
+
# companion does not support multiSelect: true questions; without this
|
|
298
|
+
# hint, Claude occasionally emits multi-select AUQ which falls back to
|
|
299
|
+
# legacy single-Q rendering on mobile (only Q1 visible, walker breaks).
|
|
300
|
+
# Multi-question AUQ with all-single-select is fully supported.
|
|
301
|
+
# Injected via --append-system-prompt at every claude launch path below.
|
|
302
|
+
CODEVIBE_SYSTEM_PROMPT="When using the AskUserQuestion tool, set multiSelect to false (or omit it) on every question. Multi-select questions are not supported by the CodeVibe mobile companion. Multiple single-select questions in one AskUserQuestion call are supported and preferred."
|
|
303
|
+
|
|
294
304
|
log "Starting codevibe-claude with session: $SESSION_NAME"
|
|
295
305
|
log "Arguments: $*"
|
|
296
306
|
|
|
@@ -310,7 +320,7 @@ if [ -n "$TMUX" ]; then
|
|
|
310
320
|
# 0 leaves _CV_RC at its 0 default. printf's `|| true` keeps a
|
|
311
321
|
# disk-full failure from clobbering diagnostics.
|
|
312
322
|
_CV_RC=0
|
|
313
|
-
claude "$@" || _CV_RC=$?
|
|
323
|
+
claude --append-system-prompt "$CODEVIBE_SYSTEM_PROMPT" "$@" || _CV_RC=$?
|
|
314
324
|
printf '%s' "$_CV_RC" > "$_CV_CLAUDE_EXIT_FILE" 2>/dev/null || true
|
|
315
325
|
exit "$_CV_RC"
|
|
316
326
|
fi
|
|
@@ -321,7 +331,7 @@ if [ ! -t 0 ] || [ ! -t 1 ]; then
|
|
|
321
331
|
_CV_AGENT_INVOKED="true"
|
|
322
332
|
_CV_AGENT_STARTED_AT="$(date +%s)"
|
|
323
333
|
_CV_RC=0
|
|
324
|
-
claude "$@" || _CV_RC=$?
|
|
334
|
+
claude --append-system-prompt "$CODEVIBE_SYSTEM_PROMPT" "$@" || _CV_RC=$?
|
|
325
335
|
printf '%s' "$_CV_RC" > "$_CV_CLAUDE_EXIT_FILE" 2>/dev/null || true
|
|
326
336
|
exit "$_CV_RC"
|
|
327
337
|
fi
|
|
@@ -331,8 +341,12 @@ fi
|
|
|
331
341
|
# Then attach to it
|
|
332
342
|
log "Creating tmux session: $SESSION_NAME"
|
|
333
343
|
|
|
334
|
-
# Build the claude command with proper escaping
|
|
344
|
+
# Build the claude command with proper escaping.
|
|
345
|
+
# Inject the CodeVibe system-prompt addendum FIRST so user-supplied args
|
|
346
|
+
# can still override or stack with their own --append-system-prompt.
|
|
335
347
|
CLAUDE_CMD="claude"
|
|
348
|
+
escaped_codevibe_prompt=$(printf '%s' "$CODEVIBE_SYSTEM_PROMPT" | sed "s/'/'\\\\''/g")
|
|
349
|
+
CLAUDE_CMD="$CLAUDE_CMD --append-system-prompt '$escaped_codevibe_prompt'"
|
|
336
350
|
for arg in "$@"; do
|
|
337
351
|
# Escape single quotes in arguments
|
|
338
352
|
escaped_arg=$(printf '%s' "$arg" | sed "s/'/'\\\\''/g")
|
package/dist/server.js
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
"use strict";var
|
|
2
|
-
`);for(let i=t.length-1;i>=0;i--){let n=t[i].trim();if(this.detectInteractivePrompt(n))return n}return null}};var
|
|
3
|
-
`)){let
|
|
4
|
-
\u26A0\uFE0F E2E ENCRYPTION WARNING: Cannot decrypt this session!`),console.error(` Your device ID (${
|
|
5
|
-
`)}}catch(c){if(this.isSessionLimitExceeded(c)){this.displaySubscriptionLimitError(c,"session"),this.activeSessions.delete(i),this.removePortFile(t);return}s.error("Failed to create/resume session:",c)}this.subscribeToMobileEvents(i),this.appSyncClient.startHeartbeat(i);let g=this.activeSessions.get(i);g&&(g.mobileEndWatcher=this.appSyncClient.watchForMobileEnd(i,async()=>{s.info("Mobile ended session \u2014 sending desktop quit",{sessionId:i});let c=process.env[re];if(!c){s.warn("No tmux session set; skipping desktop self-terminate",{sessionId:i,expectedEnv:re});return}await Ie(c,we)}))}async handleSessionEnd(e){let t=e.session_id,i=this.claudeToBackendSessionId.get(t)||this.generateBackendSessionId(t);s.info("Session ended",{claudeSessionId:t,sessionId:i,reason:e.metadata?.reason});let n=this.activeSessions.get(i);if(n?.mobileEndWatcher&&(n.mobileEndWatcher.stop(),n.mobileEndWatcher=void 0),this.removePortFile(t),n?.waitingForPromptResponse&&(s.info("Clearing prompt wait state - session ending",{sessionId:i}),this.clearWalkerAndLegacyState(n)),this.appSyncClient.stopHeartbeat(i),n)try{await this.appSyncClient.updateSession({sessionId:i,status:p.SessionStatus.INACTIVE}),s.info("Session marked as INACTIVE in AppSync",{sessionId:i})}catch(r){s.warn("Failed to update session in AppSync:",r)}else s.warn("Cannot update session - session state not found",{sessionId:i});this.activeSessions.delete(i),this.claudeToBackendSessionId.delete(t),s.debug("Session cleanup completed",{sessionId:i})}subscribeToMobileEvents(e){s.info("Subscribing to mobile events",{sessionId:e});let t=this.activeSessions.get(e);if(!t){s.error("Session not found",{sessionId:e});return}this.appSyncClient.subscribeToEvents(e,async i=>{await this.dispatchMobileEvent(e,i)},i=>{s.error("Subscription error",{sessionId:e,error:i})}),t.subscriptionActive=!0,s.info("Subscription active",{sessionId:e})}async dispatchMobileEvent(e,t){s.info("Received mobile event",{eventId:t.eventId,type:t.type,sessionId:t.sessionId,isEncrypted:t.isEncrypted});let i,n,r=!1,o,a,g;if(t.type===p.EventType.USER_PROMPT)if(n=this.activeSessions.get(e),!n)i="no-session";else if(n.processedEventIds?.has(t.eventId))i="skip-dedup";else if(n.inFlightEventIds?.has(t.eventId))i="drop-event-redeliver";else if(n.waitingForPromptResponse){let u=n.promptGenerationToken;if(!u)i="regular";else{let l=u.promptId.length>0?u.promptId:`__prompt_gen_${u.gen}`;n.inFlightPromptIds?.has(l)?i="drop-in-flight":(n.inFlightPromptIds||(n.inFlightPromptIds=new Set),n.inFlightEventIds||(n.inFlightEventIds=new Set),n.inFlightPromptIds.add(l),n.inFlightEventIds.add(t.eventId),r=!0,a=l,g=t.eventId,o={promptId:u.promptId,gen:u.gen},i="walker")}}else i="regular";else i="not-user-prompt";let c=t.content||"";if(t.isEncrypted&&this.sessionKey)try{c=p.cryptoService.decryptContent(t.content,this.sessionKey),s.debug("Event decrypted successfully",{eventId:t.eventId})}catch(u){s.error("Failed to decrypt event:",{eventId:t.eventId,error:u}),c=t.content}let d={...t,content:c};try{await this.appSyncClient.updateEventStatus({eventId:t.eventId,sessionId:t.sessionId,timestamp:t.timestamp,deliveryStatus:p.DeliveryStatus.DELIVERED}),s.info("Event marked as DELIVERED",{eventId:t.eventId})}catch(u){s.warn("Failed to mark event as DELIVERED",{eventId:t.eventId,error:u})}if(i==="skip-dedup"){s.info("[walker] Subscription-level dedup hit (already processed) \u2014 skipping",{sessionId:e,eventId:t.eventId});return}if(i==="drop-in-flight"){s.warn("[walker] Subscription-level in-flight guard \u2014 dropping duplicate USER_PROMPT (different eventId, same prompt)",{sessionId:e,eventId:t.eventId}),n&&(n.processedEventIds||(n.processedEventIds=new Set),n.processedEventIds.add(t.eventId));try{await this.markEventExecuted(t)}catch(u){s.warn("[walker] markEventExecuted threw on subscription-level duplicate drop \u2014 relying on processedEventIds Set",{sessionId:e,eventId:t.eventId,error:String(u)})}return}if(i==="drop-event-redeliver"){s.info("[walker] Subscription-level event-level redelivery \u2014 silent skip (original still in flight)",{sessionId:e,eventId:t.eventId});return}if(i==="walker"){await this.handleMobilePromptResponse(e,t,c,n,r,o,a,g);return}if(i==="regular"){await this.executeMobilePrompt(e,d);return}if(i==="no-session"){s.warn("Received USER_PROMPT for unknown session \u2014 ignoring",{sessionId:e,eventId:t.eventId});return}}async handleMobilePromptResponse(e,t,i,n,r=!1,o,a,g){let c=o??n.promptGenerationToken,d=a,u=g;if(!r&&c){let l=c.promptId.length>0?c.promptId:`__prompt_gen_${c.gen}`;if(n.inFlightPromptIds?.has(l)){s.warn("[walker] Duplicate mobile USER_PROMPT for same prompt \u2014 dropping",{sessionId:e,eventId:t.eventId,lockKey:l}),await this.markEventExecutedIdempotent(n,t);return}n.inFlightPromptIds||(n.inFlightPromptIds=new Set),n.inFlightEventIds||(n.inFlightEventIds=new Set),n.inFlightPromptIds.add(l),n.inFlightEventIds.add(t.eventId),d=l,u=t.eventId}try{if(!r&&n.processedEventIds?.has(t.eventId)){s.info("[walker] Redelivered event already processed \u2014 skipping",{sessionId:e,eventId:t.eventId});return}let l=i.trim(),y=n.pendingPromptId,_=n.pendingQuestionsSubmitMaps,O=n.pendingSubmitMap,T=n.pendingQuestionAnswerCount??0,N=!!_,C=N?_[T]:O,w=C?Object.keys(C).length:3,E=this.parseInteractivePromptInput(l,w);s.info("Parsed interactive prompt input",{sessionId:e,content:l,parsed:E,isMultiQWalk:N,capturedAnswerCount:T,hasSubmitMap:!!C});let I=()=>{let S=n.promptGenerationToken,h=S?.gen,v=c?.gen;return h!==v?(s.warn("[walker] Token mismatch \u2014 external cleanup or new prompt during in-flight handler \u2014 aborting",{sessionId:e,eventId:t.eventId,entryToken:c,currentToken:S}),!0):!1};if(I()){await this.markEventExecutedIdempotent(n,t);return}if(E.action==="select_option"){let S=C?.[E.option]||E.option;s.info("User selected option",{option:E.option,terminalInput:S});let h=await this.promptResponder.answerInteractivePrompt(e,S);if(I()){await this.markEventExecutedIdempotent(n,t);return}if(h){if(await this.markEventExecutedIdempotent(n,t),I())return;if(N){let v=T,b=v+1,x=_.length,M=b>=x;if(await this.emitWalkerNotification(e,`Selected option ${E.option}`,{promptId:y,questionIndex:v,isTerminal:M}),I())return;M?this.clearWalkerAndLegacyState(n):n.pendingQuestionAnswerCount=b}else{if(await this.emitWalkerNotification(e,`Selected option ${E.option}`,{promptId:y,questionIndex:0,isTerminal:!0}),I())return;this.clearWalkerAndLegacyState(n)}}else try{await this.sendWalkerError(e,"Failed to select option")}catch(v){s.warn("[walker] sendWalkerError threw \u2014 relying on idempotent mark in finally",{sessionId:e,eventId:t.eventId,error:String(v)})}finally{await this.markEventExecutedIdempotent(n,t)}}else if(E.action==="option_with_followup"){let S=C?.[E.option]||E.option,h=T;s.info("User selected option with follow-up",{option:E.option,terminalInput:S,followUpText:E.followUpText});let v=await this.promptResponder.answerInteractivePrompt(e,S);if(I()){await this.markEventExecutedIdempotent(n,t);return}if(v){if(await this.markEventExecutedIdempotent(n,t),I()||(await this.emitWalkerNotification(e,`Selected option ${E.option}`,{promptId:y,questionIndex:h,isTerminal:!0}),I()))return;if(this.clearWalkerAndLegacyState(n),E.followUpText){await new Promise(x=>setTimeout(x,1e3));let b={...t,content:E.followUpText};await this.executeMobilePrompt(e,b)}}else try{await this.sendWalkerError(e,"Failed to select option. Your reply (including the follow-up text) was not sent. Please retry.")}catch(b){s.warn("[walker] sendWalkerError threw \u2014 relying on idempotent mark in finally",{sessionId:e,eventId:t.eventId,error:String(b)})}finally{await this.markEventExecutedIdempotent(n,t)}}else{let S=T;s.info("Sending as free-form response to interactive prompt",{response:l});let h=await this.promptResponder.answerInteractivePrompt(e,l);if(I()){await this.markEventExecutedIdempotent(n,t);return}if(h){if(await this.markEventExecutedIdempotent(n,t),I()||(await this.emitWalkerNotification(e,"Response sent to interactive prompt",{promptId:y,questionIndex:S,isTerminal:!0}),I()))return;this.clearWalkerAndLegacyState(n)}else try{await this.sendWalkerError(e,"Failed to send response")}catch(v){s.warn("[walker] sendWalkerError threw \u2014 relying on idempotent mark in finally",{sessionId:e,eventId:t.eventId,error:String(v)})}finally{await this.markEventExecutedIdempotent(n,t)}}}finally{d&&n.inFlightPromptIds&&n.inFlightPromptIds.delete(d),u&&n.inFlightEventIds&&n.inFlightEventIds.delete(u)}}async sendInteractivePromptAsync(e,t,i){let n=this.activeSessions.get(e),r=n?.promptGenerationToken?{...n.promptGenerationToken}:void 0;await new Promise(w=>setTimeout(w,500));let o=process.env.CODEVIBE_TMUX_SESSION,a={...t.metadata||{}},g=t.metadata?.tool_name,c=t.metadata?.tool_input,d=g==="AskUserQuestion"&&Array.isArray(c?.questions)?c.questions:[],u=d.length>1&&d.every(w=>!w.multiSelect),l=d.length===1||u;if(d.length>0&&Array.isArray(d[0]?.options)&&d[0].options.length>0){let w=d.map(h=>{let v=(h.options||[]).map((A,de)=>{let B=typeof A=="string",le=B?A:A.label||"",H=B?"":A.description||"",Q={number:String(de+1),text:le};return H&&(Q.description=H),Q}),b=String(v.length+1),x=String(v.length+2);v.push({number:b,text:"Type something"},{number:x,text:"Chat about this"});let M=Object.fromEntries(v.map(A=>[A.number,A.number])),j=h.multiSelect?`Reply with comma-separated numbers (e.g., 1,3) for "${h.header||h.question}"`:`Reply with the number of your choice. For option ${b} (Type something), reply "${b}, your answer".`;return{question:h.question,header:h.header,multiSelect:!!h.multiSelect,options:v,submitMap:M,instructions:j}}),E=w[0];a.options=JSON.parse(JSON.stringify(E.options)),a.submitMap=JSON.parse(JSON.stringify(E.submitMap)),a.instructions=E.instructions,i=d[0].question;let I=typeof t.prompt_id=="string"&&t.prompt_id.length>0,S=l&&I;if(S){a.questions=w;let h=this.activeSessions.get(e);if(h){let v=h.promptGenerationToken;r&&v?.gen===r.gen?(h.pendingQuestionsSubmitMaps=w.map(b=>b.submitMap),h.pendingQuestionAnswerCount=0):s.warn("AskUserQuestion multi-Q: stale async \u2014 token gen mismatch, skipping walker-field write",{tokenAtEmit:r,currentToken:v,sessionId:e})}}l&&!I&&s.warn("AskUserQuestion multi-Q: empty prompt_id, degrading to single-Q legacy emit",{questionCount:d.length}),s.info("AskUserQuestion: using tool_input directly (skipped tmux parse)",{questionCount:d.length,multiQEmit:S,optionCountFirst:E.options.length,multiSelectFirst:!!d[0].multiSelect,questionPreview:d[0].question.slice(0,80)})}else if(o)try{let{exec:w}=await import("child_process"),E=v=>new Promise((b,x)=>{w(v,{timeout:5e3},(M,j)=>{M?x(M):b({stdout:j||""})})}),{stdout:I}=await E(`tmux capture-pane -p -e -S -30 -t '${o}'`),S=I.split(`
|
|
6
|
-
`);
|
|
1
|
+
"use strict";var Se=Object.create;var j=Object.defineProperty;var Ie=Object.getOwnPropertyDescriptor;var ke=Object.getOwnPropertyNames;var Pe=Object.getPrototypeOf,be=Object.prototype.hasOwnProperty;var Ce=(f,e)=>{for(var t in e)j(f,t,{get:e[t],enumerable:!0})},oe=(f,e,t,i)=>{if(e&&typeof e=="object"||typeof e=="function")for(let n of ke(e))!be.call(f,n)&&n!==t&&j(f,n,{get:()=>e[n],enumerable:!(i=Ie(e,n))||i.enumerable});return f};var A=(f,e,t)=>(t=f!=null?Se(Pe(f)):{},oe(e||!f||!f.__esModule?j(t,"default",{value:f,enumerable:!0}):t,f)),Ae=f=>oe(j({},"__esModule",{value:!0}),f);var Me={};Ce(Me,{McpServer:()=>H,parseInteractivePromptInput:()=>we});module.exports=Ae(Me);var x=A(require("fs")),R=A(require("path")),B=A(require("os")),he=require("child_process"),ye=require("util"),ve=require("child_process"),$=require("crypto");var ae=A(require("os")),pe=A(require("path")),ce=require("@quantiya/codevibe-core"),r=(0,ce.createLogger)({name:"codevibe-claude",logFile:pe.default.join(ae.default.tmpdir(),"codevibe-claude-mcp.log"),level:"info"});var p=require("@quantiya/codevibe-core");var z=A(require("express")),Q=A(require("fs")),Y=A(require("path")),J=A(require("os")),de=require("@quantiya/codevibe-core");var y=require("@quantiya/codevibe-core");var q=class{constructor(){this.assignedPort=0;this.app=(0,z.default)(),this.setupMiddleware(),this.setupRoutes()}setSessionId(e){this.sessionId=e}getPort(){return this.assignedPort}setupMiddleware(){this.app.use(z.default.json({limit:"1mb"})),this.app.use((e,t,i)=>{r.debug(`${e.method} ${e.path}`,{body:e.body,query:e.query}),i()}),this.app.use((e,t,i,n)=>{r.error("Express error:",e);let s={success:!1,error:e.message||"Internal server error"};i.status(500).json(s)})}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,t){let i={success:!0,data:{status:"healthy",uptime:process.uptime(),version:"0.1.0",timestamp:new Date().toISOString()}};t.json(i)}async handleEvent(e,t){try{let i=e.body;if(!i.session_id){let o={success:!1,error:"Missing required field: session_id"};t.status(400).json(o);return}if(!i.hook_event_name){let o={success:!1,error:"Missing required field: hook_event_name"};t.status(400).json(o);return}let n=this.transformHookToEvent(i);r.info("Received event from hook",{sessionId:i.session_id,hookEvent:i.hook_event_name,type:n.type}),this.eventHandler?await this.eventHandler(n):r.warn("No event handler registered");let s={success:!0,message:"Event processed successfully"};t.json(s)}catch(i){r.error("Error handling event:",i);let n={success:!1,error:i instanceof Error?i.message:"Unknown error"};t.status(500).json(n)}}async handleTestExecute(e,t){try{let{sessionId:i,prompt:n}=e.body;if(!i||!n){let o={success:!1,error:"Missing required fields: sessionId, prompt"};t.status(400).json(o);return}r.info("Test execute request",{sessionId:i,prompt:n});let s={success:!0,message:"Test execution endpoint - not implemented yet",data:{sessionId:i,prompt:n}};t.json(s)}catch(i){r.error("Error in test execute:",i);let n={success:!1,error:i instanceof Error?i.message:"Unknown error"};t.status(500).json(n)}}transformHookToEvent(e){let t,i,n={cwd:e.cwd,hook_event_name:e.hook_event_name,...e.metadata||{}};if(e.type&&e.content!==void 0)t=e.type,i=e.content;else switch(e.hook_event_name){case"SessionStart":t=y.EventType.NOTIFICATION,i="Session started",n.source=e.source;break;case"SessionEnd":t=y.EventType.NOTIFICATION,i=`Session ended: ${e.reason||"unknown"}`,n.reason=e.reason;break;case"UserPromptSubmit":t=y.EventType.USER_PROMPT,i=e.prompt||"";break;case"PostToolUse":t=y.EventType.TOOL_USE,i=JSON.stringify({tool_name:e.tool_name,tool_input:e.tool_input,tool_response:e.tool_response}),n.tool_name=e.tool_name;break;case"Notification":t=y.EventType.NOTIFICATION,i=e.message||"",n.notification_type=e.notification_type;break;default:t=y.EventType.NOTIFICATION,i=`Hook event: ${e.hook_event_name}`}return{session_id:e.session_id,hook_event_name:e.hook_event_name,type:t,source:y.EventSource.DESKTOP,content:i,metadata:n}}onEvent(e){this.eventHandler=e}async start(e){let t=e||this.sessionId;return t&&(this.sessionId=t),new Promise((i,n)=>{try{let s=(0,de.getConfig)(),o=s.server.dynamicPort?0:s.server.port;this.server=this.app.listen(o,s.server.host,()=>{let a=this.server.address();this.assignedPort=a.port,r.info(`HTTP API listening on http://${s.server.host}:${this.assignedPort}`),this.sessionId&&this.writePortFile(this.sessionId,this.assignedPort),i(this.assignedPort)}),this.server.on("error",a=>{r.error("HTTP server error:",a),n(a)})}catch(s){n(s)}})}writePortFile(e,t){let i=Y.join(J.tmpdir(),`codevibe-claude-${e}.port`);try{Q.writeFileSync(i,t.toString()),r.info(`Port file written: ${i} -> ${t}`)}catch(n){r.error(`Failed to write port file: ${i}`,n)}}removePortFile(){if(this.sessionId){let e=Y.join(J.tmpdir(),`codevibe-claude-${this.sessionId}.port`);try{Q.existsSync(e)&&(Q.unlinkSync(e),r.info(`Port file removed: ${e}`))}catch(t){r.warn(`Failed to remove port file: ${e}`,t)}}}async stop(e){return new Promise((t,i)=>{this.sessionId&&e?.protectedSessionIds?.has(this.sessionId)?r.info("Skipping port file removal \u2014 another daemon still serves this session",{sessionId:this.sessionId}):this.removePortFile(),this.server?this.server.close(n=>{n?(r.error("Error stopping HTTP server:",n),i(n)):(r.info("HTTP API stopped"),t())}):t()})}};var le=require("child_process"),ue=require("@quantiya/codevibe-core");var G=class{async executePrompt(e,t){let i=(0,ue.getConfig)(),n=i.claude.defaultTimeout;return r.info("Executing prompt from mobile",{sessionId:e,promptLength:t.length,timeout:n}),new Promise(s=>{let o=["--resume",e,"--print","--output-format","stream-json",t];r.debug("Spawning Claude command",{command:i.claude.command,args:o});let a=(0,le.spawn)(i.claude.command,o,{stdio:["pipe","pipe","pipe"],shell:!0}),m="",d="",c=!1,l=setTimeout(()=>{c=!0,r.warn("Command execution timed out",{sessionId:e,timeout:n}),a.kill("SIGTERM")},n);a.stdout?.on("data",u=>{let g=u.toString();m+=g,r.debug("Command stdout",{output:g.slice(0,200)})}),a.stderr?.on("data",u=>{let g=u.toString();d+=g,r.debug("Command stderr",{output:g.slice(0,200)})}),a.on("close",u=>{clearTimeout(l);let g={success:u===0&&!c,output:m,error:d,exitCode:u||void 0,timedOut:c};g.success?r.info("Command executed successfully",{sessionId:e,exitCode:u,outputLength:m.length}):r.error("Command execution failed",{sessionId:e,exitCode:u,timedOut:c,error:d.slice(0,500)}),s(g)}),a.on("error",u=>{clearTimeout(l),r.error("Failed to spawn command",{error:u.message}),s({success:!1,error:u.message,timedOut:!1})})})}detectInteractivePrompt(e){return[/\[Y\/n\]/i,/\[y\/N\]/i,/\(y\/n\)/i,/Continue\?/i,/Proceed\?/i].some(i=>i.test(e))}extractPromptText(e){let t=e.split(`
|
|
2
|
+
`);for(let i=t.length-1;i>=0;i--){let n=t[i].trim();if(this.detectInteractivePrompt(n))return n}return null}};var me=require("child_process"),ge=require("util");var Z=(0,ge.promisify)(me.exec),W=class{async answerInteractivePrompt(e,t,i={}){let{pressEnter:n=!0}=i;r.info("Attempting to answer interactive prompt",{sessionId:e,response:t,pressEnter:n});try{let s=process.env.CODEVIBE_TMUX_SESSION;return r.info("Checking tmux session environment",{tmuxSession:s||"(not set)",allEnvKeys:Object.keys(process.env).filter(o=>o.includes("CODEVIBE")||o.includes("TMUX"))}),s?(r.info("Using tmux send-keys",{tmuxSession:s,pressEnter:n}),await this.sendViaTmux(s,t,n),r.info("Successfully sent response to interactive prompt",{sessionId:e,response:t,pressEnter:n}),!0):(r.error("No tmux session found - codevibe-claude wrapper is required",{sessionId:e,hint:"Start Claude Code using the codevibe-claude wrapper script"}),!1)}catch(s){return r.error("Failed to answer interactive prompt",{sessionId:e,error:s instanceof Error?s.message:String(s)}),!1}}async sendViaTmux(e,t,i){let n=t.replace(/\\/g,"\\\\").replace(/"/g,'\\"').replace(/\$/g,"\\$").replace(/`/g,"\\`");r.info("Sending via tmux",{sessionName:e,inputLength:t.length,pressEnter:i});try{let s=`tmux send-keys -t "${e}" -l "${n}"`,o=await Z(s);if(r.info("tmux send-keys (text) completed",{stdout:o.stdout||"(empty)",stderr:o.stderr||"(empty)"}),i){await this.delay(500);let a=`tmux send-keys -t "${e}" Enter`,m=await Z(a);r.info("tmux send-keys (Enter) completed",{stdout:m.stdout||"(empty)",stderr:m.stderr||"(empty)"})}else r.info("tmux send-keys: skipping Enter (caller requested digit-only)")}catch(s){throw r.error("tmux send-keys failed",{sessionName:e,error:s}),s}}async sendKey(e,t){let i=process.env.CODEVIBE_TMUX_SESSION;if(!i)return r.error("No tmux session found for sendKey",{sessionId:e,keyName:t}),!1;try{let n=`tmux send-keys -t "${i}" ${t}`,s=await Z(n);return r.info("tmux send-keys (single key) completed",{sessionId:e,keyName:t,stdout:s.stdout||"(empty)",stderr:s.stderr||"(empty)"}),!0}catch(n){return r.error("tmux send-keys (single key) failed",{sessionId:e,keyName:t,error:n instanceof Error?n.message:String(n)}),!1}}delay(e){return new Promise(t=>setTimeout(t,e))}isPromptResponse(e){let t=e.trim().toLowerCase();return!!(t==="y"||t==="n"||t==="yes"||t==="no"||/^[0-9]+$/.test(t)||/^[a-z]$/.test(t)||["exit","quit","q","continue","skip","abort","retry","cancel"].includes(t))}};var xe=(0,ye.promisify)(ve.exec),Te="/exit",fe="CODEVIBE_TMUX_SESSION";async function _e(f,e){let t=async(i,n)=>{try{await xe(i)}catch(s){r.warn("tmux send-keys failed during self-terminate",{sessionName:f,label:n,error:String(s)})}};await t(`tmux send-keys -t "${f}" C-c`,"ctrl-c"),await new Promise(i=>setTimeout(i,200)),await t(`tmux send-keys -t "${f}" -l "${e}"`,"quit-text"),await new Promise(i=>setTimeout(i,500)),await t(`tmux send-keys -t "${f}" Enter`,"enter")}var H=class f{constructor(e){this.activeSessions=new Map;this.assignedPort=0;this.sessionKey=null;this.claudeToBackendSessionId=new Map;this.pendingMobilePrompts=new Map;this.nextPromptGen=1;this.httpApi=new q,this.commandExecutor=new G,this.promptResponder=new W,this.initialSessionId=e}static{this.MOBILE_PROMPT_EXPIRY_MS=3e3}getPort(){return this.assignedPort}generateBackendSessionId(e){return`claude-${e}`}trackMobilePrompt(e,t){this.pendingMobilePrompts.has(e)||this.pendingMobilePrompts.set(e,[]),this.pendingMobilePrompts.get(e).push({prompt:t.trim(),timestamp:Date.now()}),r.debug("Tracking mobile prompt for deduplication",{sessionId:e,promptLength:t.length})}isRecentMobilePrompt(e,t){let i=this.pendingMobilePrompts.get(e);if(!i)return!1;let n=Date.now(),s=t.trim(),o=[],a=!1;for(let m of i)if(!(n-m.timestamp>f.MOBILE_PROMPT_EXPIRY_MS)){if(!a&&m.prompt===s){a=!0,r.debug("Found matching mobile prompt, filtering duplicate",{sessionId:e});continue}o.push(m)}return o.length>0?this.pendingMobilePrompts.set(e,o):this.pendingMobilePrompts.delete(e),a}writePortFile(e){let t=R.join(B.tmpdir(),`codevibe-claude-${e}.port`);try{x.writeFileSync(t,this.assignedPort.toString()),r.info(`Port file written: ${t} -> ${this.assignedPort}`)}catch(i){r.error(`Failed to write port file: ${t}`,i)}}removePortFile(e){let t=R.join(B.tmpdir(),`codevibe-claude-${e}.port`);try{x.existsSync(t)&&(x.unlinkSync(t),r.info(`Port file removed: ${t}`))}catch(i){r.warn(`Failed to remove port file: ${t}`,i)}}hasOtherLiveDaemonForSession(e){try{let t=(0,he.execSync)("ps -eww -o pid= -o args=",{encoding:"utf8",timeout:2e3}),i=process.pid;for(let n of t.split(`
|
|
3
|
+
`)){let s=n.trim();if(!s)continue;let o=s.indexOf(" ");if(o<0)continue;let a=parseInt(s.substring(0,o),10);if(isNaN(a)||a===i)continue;let m=s.substring(o+1);if(/node.*codevibe-claude.*server\.js/.test(m)&&m.includes(e))return!0}return!1}catch(t){return r.warn('hasOtherLiveDaemonForSession: ps query failed; falling back to "no other daemon"',{error:String(t)}),!1}}async start(){try{if(r.info("Starting CodeVibe MCP Server...",{environment:(0,p.getEnvironment)()}),this.appSyncClient=new p.AppSyncClient,await this.appSyncClient.authenticateWithStoredTokens()){r.info("Authenticated with stored OAuth tokens",{userId:this.appSyncClient.getCurrentUserId(),email:this.appSyncClient.getCurrentUserEmail()}),await(0,p.registerDeviceEncryptionKey)(this.appSyncClient,r),(0,p.startDeviceKeyWatcher)(this.appSyncClient,r);try{let t=await this.appSyncClient.sweepOrphanSessions({agentType:"CLAUDE"});t>0&&r.info("Orphan sweep: marked stale Claude sessions INACTIVE",{swept:t})}catch(t){r.warn("Orphan sweep failed, continuing startup",{error:t instanceof Error?t.message:String(t)})}}else r.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),r.info("MCP Server started successfully",{port:this.assignedPort,host:(0,p.getConfig)().server.host,dynamicPort:(0,p.getConfig)().server.dynamicPort,sessionId:this.initialSessionId,authenticated:this.appSyncClient.isAuthenticated(),userId:this.appSyncClient.getCurrentUserId()})}catch(e){throw r.error("Failed to start MCP Server:",e),e}}async stop(){r.info("Stopping MCP Server...");let e=Array.from(this.activeSessions.keys()),t=new Set;r.info(`Marking ${e.length} active session(s) as INACTIVE...`);for(let i of e){let n=this.activeSessions.get(i);n?.mobileEndWatcher&&(n.mobileEndWatcher.stop(),n.mobileEndWatcher=void 0)}for(let i of e)try{let n=this.activeSessions.get(i);if(n&&this.hasOtherLiveDaemonForSession(n.claudeSessionId)){r.info("Another daemon serves this session \u2014 skipping mark INACTIVE AND port file removal during shutdown",{sessionId:i,claudeSessionId:n.claudeSessionId,myPid:process.pid}),t.add(n.claudeSessionId);continue}await this.appSyncClient.updateSession({sessionId:i,status:p.SessionStatus.INACTIVE}),r.info("Session marked as INACTIVE during shutdown",{sessionId:i}),n&&this.removePortFile(n.claudeSessionId)}catch(n){r.warn("Failed to mark session as INACTIVE during shutdown",{sessionId:i,error:n})}this.appSyncClient.cleanupSubscriptions(),this.activeSessions.clear(),await this.httpApi.stop({protectedSessionIds:t}),r.info("MCP Server stopped")}async handleEventFromHook(e){let{session_id:t,hook_event_name:i,type:n,content:s}=e;r.info("Processing hook event",{sessionId:t,hookEvent:i,type:n});try{i==="SessionStart"?await this.handleSessionStart(e):i==="SessionEnd"&&await this.handleSessionEnd(e);let o=this.claudeToBackendSessionId.get(t)||this.generateBackendSessionId(t);if(i==="UserPromptSubmit"){let l=this.activeSessions.get(o);if(l?.completedAskUserQuestionFingerprints?.size){let u=l.completedAskUserQuestionFingerprints.size;l.completedAskUserQuestionFingerprints.clear(),r.info("Turn boundary \u2014 cleared closed-AskUserQuestion fingerprints",{sessionId:o,clearedCount:u})}}if(n===p.EventType.USER_PROMPT&&e.source===p.EventSource.DESKTOP&&i==="UserPromptSubmit"&&s&&this.isRecentMobilePrompt(o,s)){r.info("Skipping duplicate USER_PROMPT from mobile-originated prompt",{sessionId:o,contentLength:s.length});return}if(n===p.EventType.INTERACTIVE_PROMPT){(typeof e.prompt_id!="string"||e.prompt_id.length===0)&&(e.prompt_id=`synth-${(0,$.randomUUID)()}`,r.info("Synthesized prompt_id for INTERACTIVE_PROMPT (hook omitted it)",{sessionId:o,synthesizedPromptId:e.prompt_id}));let l=this.activeSessions.get(o),u;if(l&&e.metadata?.tool_name==="AskUserQuestion"&&(u=this.computeAskUserQuestionFingerprint(e.metadata.tool_input?.questions),u)){let g=l.activeAskUserQuestionFingerprint===u,O=l.completedAskUserQuestionFingerprints?.has(u)??!1;if(g||O){r.info("Dropping duplicate INTERACTIVE_PROMPT \u2014 AskUserQuestion already tracked",{sessionId:o,fingerprint:u.slice(0,16),status:g?"in-flight":"completed",hookEvent:e.hook_event_name,promptId:e.prompt_id});return}}if(l){this.clearPromptState(l),l.waitingForPromptResponse=!0,l.pendingPromptId=e.prompt_id;let g=this.nextPromptGen++;l.promptGenerationToken={promptId:e.prompt_id||"",gen:g},u&&(l.activeAskUserQuestionFingerprint=u),r.info("Interactive prompt detected - will parse options from tmux",{sessionId:o,promptId:e.prompt_id,tokenGen:g,askUserQuestionFingerprint:u?.slice(0,16)})}this.sendInteractivePromptAsync(o,e,s).catch(g=>{r.error("Failed to send interactive prompt with dynamic options",{error:g})});return}let a=s,m=e.metadata,d=!1;r.info("Hook event encryption state",{type:n,sessionId:o,hasSessionKey:!!this.sessionKey,sessionKeyLength:this.sessionKey?.length||0}),this.sessionKey?(a=p.cryptoService.encryptContent(s,this.sessionKey),m&&(m={encrypted:p.cryptoService.encryptMetadata(m,this.sessionKey)}),d=!0,r.info("Event encrypted for hook",{type:n,sessionId:o,isEncrypted:!0})):r.warn("No session key - event will NOT be encrypted",{type:n,sessionId:o});let c=await this.appSyncClient.createEvent({sessionId:o,type:n,source:e.source,content:a,metadata:m,promptId:e.prompt_id,isEncrypted:d?!0:void 0});if(n===p.EventType.USER_PROMPT&&e.source===p.EventSource.DESKTOP){let l=this.activeSessions.get(o);l?.waitingForPromptResponse&&(this.promoteFingerprintAndClearPromptState(l),r.info("Clearing prompt wait state - new desktop prompt received",{sessionId:o}))}r.debug("Event sent to AppSync successfully")}catch(o){throw r.error("Failed to process hook event:",o),o}}async handleSessionStart(e){let t=e.session_id,i=this.generateBackendSessionId(t),n=e.metadata?.cwd||process.cwd();this.claudeToBackendSessionId.set(t,i),r.info("Session started",{claudeSessionId:t,sessionId:i,cwd:n});let s=Array.from(this.activeSessions.keys()).filter(d=>d!==i);if(s.length>0){r.info(`Marking ${s.length} previous session(s) as INACTIVE`);for(let d of s){let c=this.activeSessions.get(d);c?.mobileEndWatcher&&(c.mobileEndWatcher.stop(),c.mobileEndWatcher=void 0);try{await this.appSyncClient.updateSession({sessionId:d,status:p.SessionStatus.INACTIVE}),r.info("Previous session marked INACTIVE",{prevId:d,newSessionId:i})}catch(l){r.warn("Failed to mark previous session as INACTIVE",{prevId:d,error:l})}c&&this.removePortFile(c.claudeSessionId),this.activeSessions.delete(d)}}this.writePortFile(t);let o=this.appSyncClient.getCurrentUserId(),a={sessionId:i,claudeSessionId:t,userId:o,projectPath:n,cwd:n,createdAt:new Date,subscriptionActive:!1,waitingForPromptResponse:!1,metadata:e.metadata||{}};this.activeSessions.set(i,a);try{let d=await(0,p.resumeOrCreateSession)({sessionId:i,userId:a.userId,agentType:p.AgentType.CLAUDE,projectPath:n,metadata:e.metadata||{}},this.appSyncClient,r);if(this.sessionKey=d.sessionKey,d.resumed&&!d.sessionKey){let c=await p.keychainManager.getDeviceId();r.error("Device key not found in session encryptedKeys",{sessionId:i,pluginDeviceId:c}),console.error(`
|
|
4
|
+
\u26A0\uFE0F E2E ENCRYPTION WARNING: Cannot decrypt this session!`),console.error(` Your device ID (${c.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.
|
|
5
|
+
`)}}catch(d){if(this.isSessionLimitExceeded(d)){this.displaySubscriptionLimitError(d,"session"),this.activeSessions.delete(i),this.removePortFile(t);return}r.error("Failed to create/resume session:",d)}this.subscribeToMobileEvents(i),this.appSyncClient.startHeartbeat(i);let m=this.activeSessions.get(i);m&&(m.mobileEndWatcher=this.appSyncClient.watchForMobileEnd(i,async()=>{r.info("Mobile ended session \u2014 sending desktop quit",{sessionId:i});let d=process.env[fe];if(!d){r.warn("No tmux session set; skipping desktop self-terminate",{sessionId:i,expectedEnv:fe});return}await _e(d,Te)}))}async handleSessionEnd(e){let t=e.session_id,i=this.claudeToBackendSessionId.get(t)||this.generateBackendSessionId(t);r.info("Session ended",{claudeSessionId:t,sessionId:i,reason:e.metadata?.reason});let n=this.activeSessions.get(i);if(n?.mobileEndWatcher&&(n.mobileEndWatcher.stop(),n.mobileEndWatcher=void 0),this.removePortFile(t),n?.waitingForPromptResponse&&(r.info("Clearing prompt wait state - session ending",{sessionId:i}),this.clearPromptState(n)),this.appSyncClient.stopHeartbeat(i),n)try{await this.appSyncClient.updateSession({sessionId:i,status:p.SessionStatus.INACTIVE}),r.info("Session marked as INACTIVE in AppSync",{sessionId:i})}catch(s){r.warn("Failed to update session in AppSync:",s)}else r.warn("Cannot update session - session state not found",{sessionId:i});this.activeSessions.delete(i),this.claudeToBackendSessionId.delete(t),r.debug("Session cleanup completed",{sessionId:i})}subscribeToMobileEvents(e){r.info("Subscribing to mobile events",{sessionId:e});let t=this.activeSessions.get(e);if(!t){r.error("Session not found",{sessionId:e});return}this.appSyncClient.subscribeToEvents(e,async i=>{await this.dispatchMobileEvent(e,i)},i=>{r.error("Subscription error",{sessionId:e,error:i})}),t.subscriptionActive=!0,r.info("Subscription active",{sessionId:e})}async dispatchMobileEvent(e,t){r.info("Received mobile event",{eventId:t.eventId,type:t.type,sessionId:t.sessionId,isEncrypted:t.isEncrypted});let i,n,s=!1,o,a,m;if(t.type===p.EventType.USER_PROMPT)if(n=this.activeSessions.get(e),!n)i="no-session";else if(n.processedEventIds?.has(t.eventId))i="skip-dedup";else if(n.inFlightEventIds?.has(t.eventId))i="drop-event-redeliver";else if(n.waitingForPromptResponse){let l=n.promptGenerationToken;if(!l)i="regular";else if(t.promptId&&t.promptId.length>0&&l.promptId.length>0&&t.promptId!==l.promptId)i="drop-stale-answer";else{let u=l.promptId.length>0?l.promptId:`__prompt_gen_${l.gen}`;n.inFlightPromptIds?.has(u)?i="drop-in-flight":(n.inFlightPromptIds||(n.inFlightPromptIds=new Set),n.inFlightEventIds||(n.inFlightEventIds=new Set),n.inFlightPromptIds.add(u),n.inFlightEventIds.add(t.eventId),s=!0,a=u,m=t.eventId,o={promptId:l.promptId,gen:l.gen},i="walker")}}else i="regular";else i="not-user-prompt";let d=t.content||"";if(t.isEncrypted&&this.sessionKey)try{d=p.cryptoService.decryptContent(t.content,this.sessionKey),r.debug("Event decrypted successfully",{eventId:t.eventId})}catch(l){r.error("Failed to decrypt event:",{eventId:t.eventId,error:l}),d=t.content}let c={...t,content:d};try{await this.appSyncClient.updateEventStatus({eventId:t.eventId,sessionId:t.sessionId,timestamp:t.timestamp,deliveryStatus:p.DeliveryStatus.DELIVERED}),r.info("Event marked as DELIVERED",{eventId:t.eventId})}catch(l){r.warn("Failed to mark event as DELIVERED",{eventId:t.eventId,error:l})}if(i==="skip-dedup"){r.info("[walker] Subscription-level dedup hit (already processed) \u2014 skipping",{sessionId:e,eventId:t.eventId});return}if(i==="drop-stale-answer"){r.info("[walker] Stale answer dropped \u2014 event.promptId does not match current pending promptId",{sessionId:e,eventId:t.eventId,eventPromptId:t.promptId,currentPromptId:n?.promptGenerationToken?.promptId}),n&&(n.processedEventIds||(n.processedEventIds=new Set),n.processedEventIds.add(t.eventId));try{await this.markEventExecuted(t)}catch(l){r.warn("[walker] markEventExecuted threw on stale-answer drop \u2014 relying on processedEventIds Set",{sessionId:e,eventId:t.eventId,error:String(l)})}return}if(i==="drop-in-flight"){r.warn("[walker] Subscription-level in-flight guard \u2014 dropping duplicate USER_PROMPT (different eventId, same prompt)",{sessionId:e,eventId:t.eventId}),n&&(n.processedEventIds||(n.processedEventIds=new Set),n.processedEventIds.add(t.eventId));try{await this.markEventExecuted(t)}catch(l){r.warn("[walker] markEventExecuted threw on subscription-level duplicate drop \u2014 relying on processedEventIds Set",{sessionId:e,eventId:t.eventId,error:String(l)})}return}if(i==="drop-event-redeliver"){r.info("[walker] Subscription-level event-level redelivery \u2014 silent skip (original still in flight)",{sessionId:e,eventId:t.eventId});return}if(i==="walker"){await this.handleMobilePromptResponse(e,t,d,n,s,o,a,m);return}if(i==="regular"){await this.executeMobilePrompt(e,c);return}if(i==="no-session"){r.warn("Received USER_PROMPT for unknown session \u2014 ignoring",{sessionId:e,eventId:t.eventId});return}}async handleMobilePromptResponse(e,t,i,n,s=!1,o,a,m){let d=o??n.promptGenerationToken,c=a,l=m;if(!s&&d){let u=d.promptId.length>0?d.promptId:`__prompt_gen_${d.gen}`;if(n.inFlightPromptIds?.has(u)){r.warn("[walker] Duplicate mobile USER_PROMPT for same prompt \u2014 dropping",{sessionId:e,eventId:t.eventId,lockKey:u}),await this.markEventExecutedIdempotent(n,t);return}n.inFlightPromptIds||(n.inFlightPromptIds=new Set),n.inFlightEventIds||(n.inFlightEventIds=new Set),n.inFlightPromptIds.add(u),n.inFlightEventIds.add(t.eventId),c=u,l=t.eventId}try{if(!s&&n.processedEventIds?.has(t.eventId)){r.info("[walker] Redelivered event already processed \u2014 skipping",{sessionId:e,eventId:t.eventId});return}let u=i.trim(),g=n.pendingPromptId,T=n.pendingSubmitMap,K=T?Object.keys(T).length:3,S=this.parseInteractivePromptInput(u,K);r.info("Parsed interactive prompt input",{sessionId:e,content:u,parsed:S,hasSubmitMap:!!T});let k=()=>{let v=n.promptGenerationToken,P=v?.gen,I=d?.gen;return P!==I?(r.warn("[walker] Token mismatch \u2014 external cleanup or new prompt during in-flight handler \u2014 aborting",{sessionId:e,eventId:t.eventId,entryToken:d,currentToken:v}),!0):!1};if(k()){await this.markEventExecutedIdempotent(n,t);return}{let v=n.pendingQuestionsQueue!==void 0,I=u.trim().match(/^(\d+)$/);if(v&&n.pendingCurrentQuestion&&I){let w=n.pendingCurrentQuestion.options?.length??0,h=I[1],E=parseInt(h,10),b=!Number.isFinite(E)||E<1||E>w,F=String(E)!==h;if(b||F){if(r.info("V2 walker \u2014 bare out-of-range or non-canonical option; routing to cancel",{sessionId:e,option:h,optionNum:E,realOptionCount:w,isOutOfRange:b,isNonCanonical:F,isSubmitStep:!!n.pendingCurrentQuestion._isSubmit,parsedAction:S.action}),await this.markEventExecutedIdempotent(n,t),k())return;await this.cancelV2WalkerAndExit(e,n,"invalid_option",!1,k);return}}}if(S.action==="select_option"){let v=T?.[S.option]||S.option,P=n.pendingQuestionsQueue!==void 0;r.info("User selected option",{option:S.option,terminalInput:v,isV2AskUserQuestion:P});let I=await this.promptResponder.answerInteractivePrompt(e,v,{pressEnter:!P});if(k()){await this.markEventExecutedIdempotent(n,t);return}if(I){if(await this.markEventExecutedIdempotent(n,t),k())return;if(!g){r.warn("emitAnswerAck called without promptId \u2014 clearing state + skipping ack",{sessionId:e,source:"select_option",eventId:t.eventId}),this.promoteFingerprintAndClearPromptState(n);return}let w=(n.pendingQuestionsQueue?.length??0)===0;try{if(P){let C=parseInt(S.option,10)-1,_=n.pendingCurrentQuestion?.options?.[C],M=typeof _=="string"?_:_&&typeof _=="object"?_.label:`option ${S.option}`,re=n.pendingCurrentQuestion?._isSubmit===!0,se=M.toLowerCase(),L;re&&se==="cancel"?L="\u2192 Cancel \u2014 AskUserQuestion cancelled, no answers submitted":re&&se.startsWith("submit")?L="\u2192 Submit answers \u2014 AskUserQuestion completed":L=`\u2192 ${M}`,await this.emitUserChoice(e,L)}else await this.emitAnswerAck(e,`Selected option ${S.option}`,{promptId:g,questionIndex:0,isTerminal:w})}catch(C){r.warn("[walker] user-choice/ack emit failed \u2014 continuing to STEP 7/8",{sessionId:e,promptId:g,isV2AskUserQuestion:P,error:C instanceof Error?C.message:String(C)})}if(k())return;let h=n.pendingQuestionsQueue?.shift();if(h&&(n.pendingCurrentQuestion=h),!h){n.activeAskUserQuestionFingerprint&&(n.completedAskUserQuestionFingerprints||(n.completedAskUserQuestionFingerprints=new Set),n.completedAskUserQuestionFingerprints.add(n.activeAskUserQuestionFingerprint),r.info("AskUserQuestion V2 walker complete \u2014 fingerprint marked closed",{sessionId:e,fingerprint:n.activeAskUserQuestionFingerprint.slice(0,16)})),this.clearPromptState(n);return}let E=`synth-${(0,$.randomUUID)()}`;if(!E){this.promoteFingerprintAndClearPromptState(n),r.warn("Q[next] emit aborted: synthesized promptId was empty; promoted fingerprint + cleared prompt state",{sessionId:e,eventId:t.eventId});return}let b=this.buildQuestionWireData(h),F=h.question,N={tool_name:"AskUserQuestion",tool_input:{questions:[h]},options:b.options,submitMap:b.submitMap,instructions:b.instructions},D=this.sessionKey,ee=F,te=N,ne=!1;D&&(ee=p.cryptoService.encryptContent(F,D),te={encrypted:p.cryptoService.encryptMetadata(N,D)},ne=!0);let Ee=this.nextPromptGen++,X={promptId:E,gen:Ee};n.pendingPromptId=E,n.pendingSubmitMap=b.submitMap,n.promptGenerationToken=X;let U=X,V=this.activeSessions.get(e)?.promptGenerationToken;if(!V||V.gen!==U.gen||V.promptId!==U.promptId){r.warn("Q[next] emit aborted: token replaced before await dispatch",{sessionId:e,tokenAtAwait:U,currentToken:V});return}let ie=E.length>0?E:`__prompt_gen_${X.gen}`;n.inFlightPromptIds||(n.inFlightPromptIds=new Set),n.inFlightPromptIds.add(ie);try{try{await this.appSyncClient.createEvent({sessionId:e,type:p.EventType.INTERACTIVE_PROMPT,source:p.EventSource.DESKTOP,content:ee,metadata:te,promptId:E,isEncrypted:ne?!0:void 0}),r.info("Q[next] emit succeeded",{sessionId:e,promptId:E,remaining:n.pendingQuestionsQueue?.length??0})}catch(C){let _=this.activeSessions.get(e),M=_?.promptGenerationToken;M&&M.gen===U.gen&&M.promptId===U.promptId?(this.promoteFingerprintAndClearPromptState(_),r.warn("Q[next] emit failed; promoted fingerprint + cleared prompt state. User must answer remaining questions on desktop terminal.",{sessionId:e,promptId:E,error:C instanceof Error?C.message:String(C)})):r.warn("Q[next] emit failed but a NEW prompt replaced our token during await; not clearing state (would wipe new prompt). Q[next..QN] of the original AskUserQuestion are lost; new prompt continues normally.",{sessionId:e,tokenAtAwait:U,currentToken:M,error:C instanceof Error?C.message:String(C)})}}finally{n.inFlightPromptIds.delete(ie)}}else try{await this.sendPromptError(e,"Failed to select option")}catch(w){r.warn("[walker] sendPromptError threw \u2014 relying on idempotent mark in finally",{sessionId:e,eventId:t.eventId,error:String(w)})}finally{await this.markEventExecutedIdempotent(n,t)}}else if(S.action==="option_with_followup"){if(n.pendingQuestionsQueue!==void 0){if(r.info("V2 walker \u2014 option_with_followup \u2192 cancel + new prompt",{sessionId:e,option:S.option,followUpText:S.followUpText}),await this.markEventExecutedIdempotent(n,t),k())return;if(await this.cancelV2WalkerAndExit(e,n,"option_with_followup",!!S.followUpText,k)&&S.followUpText){await new Promise(b=>setTimeout(b,1500));let h=this.activeSessions.get(e);if(h?.pendingPromptId||h?.waitingForPromptResponse){r.warn("Post-cancel followup suppressed: new prompt B is active",{sessionId:e,bPromptId:h.pendingPromptId});return}let E={...t,content:S.followUpText};await this.executeMobilePrompt(e,E)}return}let P=T?.[S.option]||S.option;r.info("User selected option with follow-up",{option:S.option,terminalInput:P,followUpText:S.followUpText});let I=await this.promptResponder.answerInteractivePrompt(e,P);if(k()){await this.markEventExecutedIdempotent(n,t);return}if(I){if(await this.markEventExecutedIdempotent(n,t),k())return;if(!g){r.warn("emitAnswerAck called without promptId \u2014 clearing state + skipping ack",{sessionId:e,source:"option_with_followup",eventId:t.eventId}),this.promoteFingerprintAndClearPromptState(n);return}try{await this.emitAnswerAck(e,`Selected option ${S.option}`,{promptId:g,questionIndex:0,isTerminal:!0})}catch(w){r.warn("[walker] emitAnswerAck (option_with_followup) failed \u2014 continuing to clearPromptState + executeMobilePrompt",{sessionId:e,promptId:g,error:w instanceof Error?w.message:String(w)})}if(k())return;if(this.promoteFingerprintAndClearPromptState(n),S.followUpText){await new Promise(h=>setTimeout(h,1e3));let w={...t,content:S.followUpText};await this.executeMobilePrompt(e,w)}}else try{await this.sendPromptError(e,"Failed to select option. Your reply (including the follow-up text) was not sent. Please retry.")}catch(w){r.warn("[walker] sendPromptError threw \u2014 relying on idempotent mark in finally",{sessionId:e,eventId:t.eventId,error:String(w)})}finally{await this.markEventExecutedIdempotent(n,t)}}else{if(n.pendingQuestionsQueue!==void 0){if(r.info("V2 walker \u2014 send_as_response \u2192 cancel + new prompt",{sessionId:e,contentPreview:u.slice(0,80)}),await this.markEventExecutedIdempotent(n,t),k())return;if(await this.cancelV2WalkerAndExit(e,n,"send_as_response",!0,k)){await new Promise(h=>setTimeout(h,1500));let w=this.activeSessions.get(e);if(w?.pendingPromptId||w?.waitingForPromptResponse){r.warn("Post-cancel free-text suppressed: new prompt B is active",{sessionId:e,bPromptId:w.pendingPromptId});return}await this.executeMobilePrompt(e,t)}return}r.info("Sending as free-form response to interactive prompt",{response:u});let P=await this.promptResponder.answerInteractivePrompt(e,u);if(k()){await this.markEventExecutedIdempotent(n,t);return}if(P){if(await this.markEventExecutedIdempotent(n,t),k())return;if(!g){r.warn("emitAnswerAck called without promptId \u2014 clearing state + skipping ack",{sessionId:e,source:"send_as_response",eventId:t.eventId}),this.promoteFingerprintAndClearPromptState(n);return}try{await this.emitAnswerAck(e,"Response sent to interactive prompt",{promptId:g,questionIndex:0,isTerminal:!0})}catch(I){r.warn("[walker] emitAnswerAck (send_as_response) failed \u2014 continuing to clearPromptState",{sessionId:e,promptId:g,error:I instanceof Error?I.message:String(I)})}if(k())return;this.promoteFingerprintAndClearPromptState(n)}else try{await this.sendPromptError(e,"Failed to send response")}catch(I){r.warn("[walker] sendPromptError threw \u2014 relying on idempotent mark in finally",{sessionId:e,eventId:t.eventId,error:String(I)})}finally{await this.markEventExecutedIdempotent(n,t)}}}finally{c&&n.inFlightPromptIds&&n.inFlightPromptIds.delete(c),l&&n.inFlightEventIds&&n.inFlightEventIds.delete(l)}}async sendInteractivePromptAsync(e,t,i){let n=this.activeSessions.get(e),s=n?.promptGenerationToken?{...n.promptGenerationToken}:void 0;await new Promise(v=>setTimeout(v,500));let o=process.env.CODEVIBE_TMUX_SESSION,a={...t.metadata||{}},m=t.metadata?.tool_name,d=t.metadata?.tool_input,c=m==="AskUserQuestion"&&Array.isArray(d?.questions)?d.questions:[],l=c.every(v=>!v.multiSelect),u=c.length>=1&&l;if(c.length>0&&Array.isArray(c[0]?.options)&&c[0].options.length>0){let v=c[0],P=this.buildQuestionWireData(v);a.options=JSON.parse(JSON.stringify(P.options)),a.submitMap=JSON.parse(JSON.stringify(P.submitMap)),a.instructions=P.instructions,a.tool_name="AskUserQuestion",a.tool_input={questions:[v]},i=v.question;let I=typeof t.prompt_id=="string"&&t.prompt_id.length>0,w=u&&I;if(w){let h=c.slice(1);h.push({question:"Ready to submit your answers?",options:[{label:"Submit answers",description:"Send your selections to the assistant"},{label:"Cancel",description:"Discard your answers"}],multiSelect:!1,_isSubmit:!0});let E=this.activeSessions.get(e);if(E){let b=E.promptGenerationToken;s&&b?.gen===s.gen?(E.pendingQuestionsQueue=h,E.pendingCurrentQuestion=v):r.warn("AskUserQuestion V2: stale async \u2014 token gen mismatch, skipping pendingQuestionsQueue write",{tokenAtEmit:s,currentToken:b,sessionId:e})}}else u&&!I&&r.warn("AskUserQuestion V2: empty prompt_id, degrading to single-Q legacy emit",{questionCount:c.length});r.info("AskUserQuestion V2: emitting Q1 only (Q2..QN queued)",{questionCount:c.length,v2SequentialEmit:w,queuedRemaining:w?c.length-1:0,optionCountFirst:P.options.length,anyMultiSelect:!l,questionPreview:v.question.slice(0,80)})}else if(o)try{let{exec:v}=await import("child_process"),P=E=>new Promise((b,F)=>{v(E,{timeout:5e3},(N,D)=>{N?F(N):b({stdout:D||""})})}),{stdout:I}=await P(`tmux capture-pane -p -e -S -30 -t '${o}'`),w=I.split(`
|
|
6
|
+
`);r.info("tmux capture result",{tmuxSession:o,totalLines:w.length,lastLines:w.slice(-15).map(E=>E.replace(/\x1B[^m]*m/g,"").trim()).filter(Boolean)});let h=(0,p.parseInteractivePrompt)(I);h&&h.options.length>0?(a.options=h.options,a.submitMap=h.submitMap,a.instructions=this.buildPromptInstructions(h),r.info("Parsed dynamic options from tmux",{optionCount:h.options.length,kind:h.kind,options:h.options})):(r.info("No dynamic options parsed from tmux, using fallback",{parsedResult:h}),this.addFallbackOptions(a))}catch(v){r.warn("Failed to capture tmux pane for options",{error:v}),this.addFallbackOptions(a)}else r.warn("No tmux session \u2014 using fallback options"),this.addFallbackOptions(a);let g=this.activeSessions.get(e);if(g&&a.submitMap){let v=g.promptGenerationToken;s&&v?.gen===s.gen?g.pendingSubmitMap=a.submitMap:r.warn("Interactive prompt async: stale async \u2014 token gen mismatch, skipping pendingSubmitMap write",{tokenAtEmit:s,currentToken:v,sessionId:e})}let O=i,T=a,K=!1;this.sessionKey&&(O=p.cryptoService.encryptContent(i,this.sessionKey),T={encrypted:p.cryptoService.encryptMetadata(T,this.sessionKey)},K=!0);let k=this.activeSessions.get(e)?.promptGenerationToken;if(s&&k?.gen!==s.gen){r.warn("Interactive prompt emit: stale token \u2014 newer INTERACTIVE_PROMPT replaced ours; skipping AppSync emit",{sessionId:e,tokenAtEmit:s,currentToken:k});return}await this.appSyncClient.createEvent({sessionId:e,type:p.EventType.INTERACTIVE_PROMPT,source:t.source,content:O,metadata:T,promptId:t.prompt_id,isEncrypted:K?!0:void 0}),r.info("Interactive prompt sent to AppSync with dynamic options",{sessionId:e})}buildQuestionWireData(e){let t=(e.options||[]).map((s,o)=>{let a=typeof s=="string",m=a?s:s.label||"",d=a?"":s.description||"",c={number:String(o+1),text:m};return d&&(c.description=d),c});if(!e._isSubmit){let s=String(t.length+1),o=String(t.length+2);t.push({number:s,text:"Type something"},{number:o,text:"Chat about this"})}let i=Object.fromEntries(t.map(s=>[s.number,s.number])),n;if(e._isSubmit)n="Reply with 1 to submit your answers or 2 to cancel.";else if(e.multiSelect)n=`Reply with comma-separated numbers (e.g., 1,3) for "${e.header||e.question}"`;else{let s=String(t.length-1);n=`Reply with the number of your choice. For option ${s} (Type something), reply "${s}, your answer".`}return{options:t,submitMap:i,instructions:n}}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(i=>i.number).join(", ")}. Append a message to provide alternative instructions.`}parseInteractivePromptInput(e,t=3){return we(e,t)}async markEventExecuted(e){try{await this.appSyncClient.updateEventStatus({eventId:e.eventId,sessionId:e.sessionId,timestamp:e.timestamp,deliveryStatus:p.DeliveryStatus.EXECUTED}),r.info("Event marked as EXECUTED",{eventId:e.eventId})}catch(t){r.warn("Failed to mark event as EXECUTED",{eventId:e.eventId,error:t})}}async sendPromptError(e,t){let i={error:!0},n=t,s=i,o=!1;this.sessionKey&&(n=p.cryptoService.encryptContent(t,this.sessionKey),s={encrypted:p.cryptoService.encryptMetadata(i,this.sessionKey)},o=!0),await this.appSyncClient.createEvent({sessionId:e,type:p.EventType.NOTIFICATION,source:p.EventSource.DESKTOP,content:n,metadata:s,isEncrypted:o?!0:void 0})}async emitUserChoice(e,t){let i=t,n={source:"codevibe_v2_user_choice"},s=!1;this.sessionKey&&(i=p.cryptoService.encryptContent(t,this.sessionKey),n={encrypted:p.cryptoService.encryptMetadata({source:"codevibe_v2_user_choice"},this.sessionKey)},s=!0),await this.appSyncClient.createEvent({sessionId:e,type:p.EventType.ASSISTANT_RESPONSE,source:p.EventSource.DESKTOP,content:i,metadata:n,isEncrypted:s?!0:void 0})}async emitAnswerAck(e,t,i){let n={promptAnswered:!0,...i},s=t,o=n,a=!1;this.sessionKey&&(s=p.cryptoService.encryptContent(t,this.sessionKey),o={encrypted:p.cryptoService.encryptMetadata(n,this.sessionKey)},a=!0),await this.appSyncClient.createEvent({sessionId:e,type:p.EventType.NOTIFICATION,source:p.EventSource.DESKTOP,content:s,metadata:o,isEncrypted:a?!0:void 0})}promoteFingerprintAndClearPromptState(e){e.activeAskUserQuestionFingerprint&&(e.completedAskUserQuestionFingerprints||(e.completedAskUserQuestionFingerprints=new Set),e.completedAskUserQuestionFingerprints.add(e.activeAskUserQuestionFingerprint)),this.clearPromptState(e)}clearPromptState(e){e.waitingForPromptResponse=!1,e.pendingPromptId=void 0,e.pendingSubmitMap=void 0,e.pendingQuestionsQueue=void 0,e.pendingCurrentQuestion=void 0,e.activeAskUserQuestionFingerprint=void 0,e.promptGenerationToken=void 0}async cancelV2WalkerAndExit(e,t,i,n,s){let o=t.pendingQuestionsQueue?.length??0;r.info("V2 walker mid-walker bailout \u2014 navigating to Submit/Cancel",{sessionId:e,advanceCount:o,triggerReason:i,hasFollowupText:n});let a=!1;for(let c=0;c<o;c++){let l=await this.promptResponder.sendKey(e,"Enter");if(s())return r.warn("cancelV2WalkerAndExit: aborted mid-loop (token rotated); leaving session state for new prompt",{sessionId:e,attemptedAdvances:c,totalAdvances:o,triggerReason:i}),!1;if(!l){a=!0,r.warn("cancelV2WalkerAndExit: sendKey returned false; aborting cancel sequence",{sessionId:e,attemptedAdvances:c,totalAdvances:o,triggerReason:i});break}if(await new Promise(u=>setTimeout(u,200)),s())return r.warn("cancelV2WalkerAndExit: aborted post-delay (token rotated); leaving session state",{sessionId:e,attemptedAdvances:c+1,totalAdvances:o,triggerReason:i}),!1}if(a){this.promoteFingerprintAndClearPromptState(t);try{await this.emitUserChoice(e,"AskUserQuestion bailout \u2014 desktop walker state unknown (tmux unavailable); your reply was not sent")}catch(c){r.warn("emitUserChoice on V2 cancel (sendKey failed) failed",{sessionId:e,error:c instanceof Error?c.message:String(c)})}return!1}let m=await this.promptResponder.answerInteractivePrompt(e,"2",{pressEnter:!1});if(s())return r.warn("cancelV2WalkerAndExit: aborted post-Cancel send (token rotated); leaving session state",{sessionId:e,triggerReason:i}),!1;if(!m){r.warn('cancelV2WalkerAndExit: final "2" Cancel send returned false; treating as bailout',{sessionId:e,triggerReason:i}),this.promoteFingerprintAndClearPromptState(t);try{await this.emitUserChoice(e,"AskUserQuestion bailout \u2014 Cancel keypress did not land (tmux unavailable); your reply was not sent")}catch(c){r.warn("emitUserChoice on V2 cancel (cancel send failed) failed",{sessionId:e,error:c instanceof Error?c.message:String(c)})}return!1}t.activeAskUserQuestionFingerprint&&(t.completedAskUserQuestionFingerprints||(t.completedAskUserQuestionFingerprints=new Set),t.completedAskUserQuestionFingerprints.add(t.activeAskUserQuestionFingerprint),r.info("V2 walker cancelled via mid-walker bailout \u2014 fingerprint marked closed",{sessionId:e,fingerprint:t.activeAskUserQuestionFingerprint.slice(0,16),triggerReason:i}));let d=n?"AskUserQuestion cancelled \u2014 sending your reply as a new prompt":"AskUserQuestion cancelled, no answers submitted";try{await this.emitUserChoice(e,d)}catch(c){r.warn("emitUserChoice on V2 cancel failed",{sessionId:e,error:c instanceof Error?c.message:String(c)})}return s()?(r.warn("cancelV2WalkerAndExit: aborted post-emitUserChoice (token rotated); leaving session state for new prompt",{sessionId:e,triggerReason:i}),!1):(this.clearPromptState(t),!0)}computeAskUserQuestionFingerprint(e){if(!(!e||typeof e!="object"))try{let t=this.stringifyCanonical(e);return(0,$.createHash)("sha256").update(t).digest("hex")}catch(t){r.warn("Failed to fingerprint AskUserQuestion questions",{error:t instanceof Error?t.message:String(t)});return}}stringifyCanonical(e){return e===null||typeof e!="object"?JSON.stringify(e):Array.isArray(e)?"["+e.map(i=>this.stringifyCanonical(i)).join(",")+"]":"{"+Object.keys(e).sort().map(i=>JSON.stringify(i)+":"+this.stringifyCanonical(e[i])).join(",")+"}"}async markEventExecutedIdempotent(e,t){e.processedEventIds||(e.processedEventIds=new Set),e.processedEventIds.add(t.eventId);try{await this.markEventExecuted(t)}catch(i){r.warn("[walker] markEventExecuted threw \u2014 relying on processedEventIds set for dedup",{sessionId:t.sessionId,eventId:t.eventId,error:String(i)})}}isSessionLimitExceeded(e){return this.getErrorMessage(e).includes("SESSION_LIMIT_EXCEEDED")}isUsageLimitExceeded(e){let t=this.getErrorMessage(e);return t.includes("MESSAGE_LIMIT_EXCEEDED")||t.includes("IMAGE_LIMIT_EXCEEDED")}getErrorMessage(e){if(e instanceof Error)return e.message;if(typeof e=="object"&&e!==null){let t=e;if(t.errors&&Array.isArray(t.errors))return t.errors.map(i=>i.message||"").join(" ");if(typeof t.message=="string")return t.message}return String(e)}displaySubscriptionLimitError(e,t){let i=this.getErrorMessage(e),n="",s=i.match(/for your (\w+) plan/i);s&&(n=` (${s[1]} tier)`);let o="",a=i.match(/of (\d+)/);switch(a&&(o=` [Limit: ${a[1]}]`),console.log(`
|
|
7
7
|
`+"=".repeat(60)),console.log("\u26A0\uFE0F SUBSCRIPTION LIMIT REACHED"),console.log("=".repeat(60)),t){case"session":console.log(`You have reached the maximum number of active sessions${n}.`),console.log(`${o}`),console.log(`
|
|
8
8
|
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${n}.`),console.log(`${o}`),console.log(`
|
|
9
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;case"image":console.log(`You have reached your monthly image attachment limit${n}.`),console.log(`${o}`),console.log(`
|
|
10
10
|
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(`
|
|
11
11
|
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)+`
|
|
12
|
-
`),
|
|
12
|
+
`),r.error("Subscription limit exceeded",{limitType:t,errorMessage:i})}async downloadAttachment(e,t,i){try{let n=e.isEncrypted??i??!1;r.info("Downloading attachment - START",{id:e.id,type:e.type,filename:e.filename,s3Key:e.s3Key,attachmentIsEncrypted:e.isEncrypted,eventIsEncrypted:i,shouldDecrypt:n,hasSessionKey:!!this.sessionKey});let{downloadUrl:s}=await this.appSyncClient.getAttachmentDownloadUrl(e.s3Key),o=await fetch(s);if(!o.ok)throw new Error(`Failed to download attachment: ${o.status} ${o.statusText}`);let a=Buffer.from(await o.arrayBuffer());if(r.info("Attachment downloaded",{id:e.id,downloadedSize:a.length,first20Bytes:a.slice(0,20).toString("hex")}),r.info("Checking decryption conditions",{id:e.id,shouldDecrypt:n,hasSessionKey:!!this.sessionKey,willDecrypt:!!(n&&this.sessionKey)}),n&&this.sessionKey)try{r.info("Decrypting attachment",{id:e.id,encryptedSize:a.length}),a=p.cryptoService.decryptData(a,this.sessionKey),r.info("Attachment decrypted successfully",{id:e.id,decryptedSize:a.length,first20Bytes:a.slice(0,20).toString("hex")})}catch(g){throw r.error("Failed to decrypt attachment:",{id:e.id,error:g}),new Error("Failed to decrypt attachment")}else n&&!this.sessionKey?r.warn("Cannot decrypt attachment - no session key available",{id:e.id}):r.info("Skipping decryption - attachment not encrypted or no session key",{id:e.id,shouldDecrypt:n,hasSessionKey:!!this.sessionKey});let m=R.join(B.tmpdir(),"codevibe-claude",t);x.existsSync(m)||x.mkdirSync(m,{recursive:!0});let d="",c=e.filename;if(n&&e.filename&&this.sessionKey)try{c=p.cryptoService.decryptContent(e.filename,this.sessionKey)}catch{c=e.filename}if(c){let g=R.extname(c);g&&(d=g)}d||(d={"image/jpeg":".jpg","image/png":".png","image/gif":".gif","image/webp":".webp","image/heic":".heic","application/pdf":".pdf"}[e.type]||".bin");let l=`attachment-${e.id}${d}`,u=R.join(m,l);return x.writeFileSync(u,a),r.info("Attachment saved to temp file",{id:e.id,filePath:u,size:a.length,wasDecrypted:n&&!!this.sessionKey}),u}catch(n){return r.error("Failed to download attachment:",{id:e.id,error:n}),null}}async executeMobilePrompt(e,t){let i=t.content||"",n=t.attachments||[];r.info("Executing mobile prompt via tmux",{sessionId:e,promptLength:i.length,attachmentCount:n.length});let s=[];if(n.length>0){r.info("Downloading attachments for prompt",{count:n.length});for(let o of n){let a=await this.downloadAttachment(o,e,t.isEncrypted);a&&s.push(a)}if(s.length>0){let o=s.map(a=>`[Attached file: ${a}]`).join(`
|
|
13
13
|
`);i?i=`${o}
|
|
14
14
|
|
|
15
15
|
${i}`:i=`${o}
|
|
16
16
|
|
|
17
|
-
Please analyze the attached file(s).`,
|
|
17
|
+
Please analyze the attached file(s).`,r.info("Prompt updated with attachment paths",{attachmentCount:s.length,newPromptLength:i.length})}}this.trackMobilePrompt(e,i);try{if(await this.promptResponder.answerInteractivePrompt(e,i)){try{await this.appSyncClient.updateEventStatus({eventId:t.eventId,sessionId:t.sessionId,timestamp:t.timestamp,deliveryStatus:p.DeliveryStatus.EXECUTED}),r.info("Event marked as EXECUTED",{eventId:t.eventId})}catch(m){r.warn("Failed to mark event as EXECUTED",{eventId:t.eventId,error:m})}r.info("Mobile prompt sent successfully",{sessionId:e});let a=s.length>0?`Prompt with ${s.length} attachment(s) sent to Claude Code`:`Prompt "${i.substring(0,50)}${i.length>50?"...":""}" sent to Claude Code`;await this.appSyncClient.createEvent({sessionId:e,type:p.EventType.NOTIFICATION,source:p.EventSource.DESKTOP,content:a,metadata:{mobilePrompt:!0,attachmentCount:s.length}})}else r.error("Failed to send mobile prompt",{sessionId:e}),await this.appSyncClient.createEvent({sessionId:e,type:p.EventType.NOTIFICATION,source:p.EventSource.DESKTOP,content:"Failed to send prompt to Claude Code",metadata:{error:!0}})}catch(o){r.error("Failed to execute mobile prompt:",o)}}};async function Fe(){let f=process.argv[2]||process.env.CLAUDE_SESSION_ID;f?r.info(`Starting MCP server for session: ${f}`):r.info("Starting MCP server without initial session ID (will be set on SessionStart)");let e=new H(f);try{await e.start();let t=e.getPort();console.log(`PORT=${t}`);let i=!1,n=async s=>{if(i){r.info("Shutdown already in progress, ignoring additional signal");return}i=!0,r.info(`Received ${s} signal, stopping server...`);try{await e.stop(),r.info("Graceful shutdown completed"),process.exit(0)}catch(o){r.error("Error during shutdown:",o),process.exit(1)}};process.on("SIGINT",()=>n("SIGINT")),process.on("SIGTERM",()=>n("SIGTERM")),process.on("SIGHUP",()=>n("SIGHUP")),process.on("uncaughtException",async s=>{r.error("Uncaught exception:",s),await n("uncaughtException")}),process.on("unhandledRejection",async s=>{r.error("Unhandled rejection:",s),await n("unhandledRejection")})}catch(t){r.error("Failed to start MCP Server:",t),process.exit(1)}}function we(f,e=3){let t=f.trim(),i=t.match(/^(\d+)$/);if(i){let s=parseInt(i[1]);if(s>=1&&s<=e)return{action:"select_option",option:i[1]}}let n=t.match(/^(\d+)[,.:;\-\s\n]+(.+)$/s);if(n){let s=parseInt(n[1]);if(s>=1&&s<=e)return{action:"option_with_followup",option:n[1],followUpText:n[2].trim()}}return{action:"send_as_response"}}process.env.JEST_WORKER_ID||Fe().catch(f=>{r.error("Unhandled error in main:",f),process.exit(1)});0&&(module.exports={McpServer,parseInteractivePromptInput});
|
package/hooks/stop.sh
CHANGED
|
@@ -35,6 +35,16 @@ log "DEBUG" "Reading transcript: $TRANSCRIPT_PATH"
|
|
|
35
35
|
# Single jq --slurp invocation: finds last user UUID, builds parent chain,
|
|
36
36
|
# extracts assistant text + tool_use events. ~200x faster than per-line bash+jq loops.
|
|
37
37
|
EVENTS_SENT=0
|
|
38
|
+
# Track whether we emitted an ASSISTANT_RESPONSE specifically. The fallback
|
|
39
|
+
# at the bottom (`last_assistant_message` from hook input) needs to fire
|
|
40
|
+
# whenever the final assistant text is missing, regardless of whether
|
|
41
|
+
# tool_use / INTERACTIVE_PROMPT events were emitted. Without this, the
|
|
42
|
+
# bug-#292 partial-write trim that drops the in-flight final assistant
|
|
43
|
+
# entry silently swallows Claude's response when the same turn also
|
|
44
|
+
# emitted a tool_use earlier (e.g., AskUserQuestion answered from mobile
|
|
45
|
+
# → walker submits → Claude responds with "Got it..." while transcript
|
|
46
|
+
# write is still in flight).
|
|
47
|
+
ASSISTANT_TEXT_SENT=0
|
|
38
48
|
|
|
39
49
|
# Track sent message UUIDs to avoid duplicates (shared with PermissionRequest hook)
|
|
40
50
|
SENT_UUIDS_FILE="${CODEVIBE_TMPDIR}/codevibe-claude-sent-uuids-${SESSION_ID}.txt"
|
|
@@ -138,6 +148,7 @@ if [ -n "$TRANSCRIPT_EVENTS" ]; then
|
|
|
138
148
|
if [ "$MSG_TYPE" = "already_sent" ]; then
|
|
139
149
|
log "DEBUG" "Skipping already sent message UUID: $MSG_UUID"
|
|
140
150
|
EVENTS_SENT=$((EVENTS_SENT + 1)) # Count as sent to prevent fallback duplicate
|
|
151
|
+
ASSISTANT_TEXT_SENT=1 # Permission-request hook already sent it
|
|
141
152
|
continue
|
|
142
153
|
fi
|
|
143
154
|
|
|
@@ -163,6 +174,7 @@ if [ -n "$TRANSCRIPT_EVENTS" ]; then
|
|
|
163
174
|
|
|
164
175
|
if [ $? -eq 0 ]; then
|
|
165
176
|
EVENTS_SENT=$((EVENTS_SENT + 1))
|
|
177
|
+
ASSISTANT_TEXT_SENT=1
|
|
166
178
|
log "INFO" "Assistant response sent successfully"
|
|
167
179
|
echo "$MSG_UUID" >> "$SENT_UUIDS_FILE"
|
|
168
180
|
else
|
|
@@ -245,13 +257,21 @@ if [ -n "$TRANSCRIPT_EVENTS" ]; then
|
|
|
245
257
|
done <<< "$TRANSCRIPT_EVENTS"
|
|
246
258
|
fi
|
|
247
259
|
|
|
248
|
-
# Fallback: if no
|
|
249
|
-
#
|
|
250
|
-
#
|
|
251
|
-
|
|
260
|
+
# Fallback: if no ASSISTANT_RESPONSE was extracted from the transcript but
|
|
261
|
+
# last_assistant_message is available in the hook input, send it directly.
|
|
262
|
+
# This handles two race conditions:
|
|
263
|
+
# (1) Stop hook fires before any assistant content is written to the
|
|
264
|
+
# transcript (gate: EVENTS_SENT === 0).
|
|
265
|
+
# (2) Bug-#292 partial-write trim drops Claude's final assistant text
|
|
266
|
+
# while earlier tool_use entries in the same turn are fully written —
|
|
267
|
+
# EVENTS_SENT > 0 but ASSISTANT_TEXT_SENT === 0. Without checking
|
|
268
|
+
# ASSISTANT_TEXT_SENT separately, the AskUserQuestion case (mobile
|
|
269
|
+
# walker submits → Claude writes "Got it..." while transcript flush
|
|
270
|
+
# is still in flight) silently swallows the final response.
|
|
271
|
+
if [ "$ASSISTANT_TEXT_SENT" -eq 0 ]; then
|
|
252
272
|
LAST_ASSISTANT_MSG=$(echo "$INPUT" | jq -r '.last_assistant_message // empty')
|
|
253
273
|
if [ -n "$LAST_ASSISTANT_MSG" ] && [ "$LAST_ASSISTANT_MSG" != "null" ]; then
|
|
254
|
-
log "INFO" "
|
|
274
|
+
log "INFO" "Assistant text missing from transcript scan (EVENTS_SENT=$EVENTS_SENT), using last_assistant_message fallback"
|
|
255
275
|
FALLBACK_PAYLOAD=$(jq -n \
|
|
256
276
|
--arg session_id "$SESSION_ID" \
|
|
257
277
|
--arg content "$LAST_ASSISTANT_MSG" \
|
|
@@ -277,10 +297,10 @@ fi
|
|
|
277
297
|
|
|
278
298
|
log "INFO" "Stop hook completed. Events sent: $EVENTS_SENT"
|
|
279
299
|
|
|
280
|
-
#
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
300
|
+
# SENT_UUIDS_FILE persists across multiple Stop hook fires within the same
|
|
301
|
+
# session. Stop can fire more than once per user turn (e.g., once when
|
|
302
|
+
# Claude pauses for an AskUserQuestion tool result, again after the walker
|
|
303
|
+
# submits and Claude resumes). Dedup must survive across those fires;
|
|
304
|
+
# session-end.sh wipes the file when the session actually ends.
|
|
285
305
|
|
|
286
306
|
exit 0
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CreateEventInput, CreateSessionInput, UpdateSessionInput, UpdateEventStatusInput, Event, Session, EventSource, DeviceKey, GrantSessionKeyInput
|
|
1
|
+
import { CreateEventInput, CreateSessionInput, UpdateSessionInput, UpdateEventStatusInput, Event, Session, EventSource, DeviceKey, GrantSessionKeyInput } from '../types';
|
|
2
2
|
/**
|
|
3
3
|
* Download URL response
|
|
4
4
|
*/
|
|
@@ -6,23 +6,6 @@ export interface DownloadUrlResponse {
|
|
|
6
6
|
downloadUrl: string;
|
|
7
7
|
expiresAt: string;
|
|
8
8
|
}
|
|
9
|
-
/**
|
|
10
|
-
* Discriminator for the most recent `authenticateWithStoredTokens()`
|
|
11
|
-
* failure. Lets callers distinguish a genuine "no tokens / refresh
|
|
12
|
-
* rejected" outcome from a transient network failure during the
|
|
13
|
-
* Cognito refresh-token POST.
|
|
14
|
-
*
|
|
15
|
-
* Stage 2 round-1 Codex M1: the production refresh path returns
|
|
16
|
-
* `false` on every error (including network blow-ups inside
|
|
17
|
-
* `callCognitoRefresh`'s catch block), so the wizard's auth-vs-network
|
|
18
|
-
* heuristic — which only ran on caught throws — never fired in
|
|
19
|
-
* production. Recording the kind on every false-return path lets
|
|
20
|
-
* `setup-bootstrap.ts:defaultClientFactory()` route network-shaped
|
|
21
|
-
* refresh failures to `subscription_status_network` (per §6 row
|
|
22
|
-
* "transient 5xx during refresh") instead of misleading the user with
|
|
23
|
-
* a "not signed in" abort.
|
|
24
|
-
*/
|
|
25
|
-
export type AuthFailureKind = 'no_tokens' | 'refresh_auth_rejected' | 'refresh_network';
|
|
26
9
|
/**
|
|
27
10
|
* AppSync GraphQL client with WebSocket subscriptions
|
|
28
11
|
*/
|
|
@@ -32,30 +15,11 @@ export declare class AppSyncClient {
|
|
|
32
15
|
private currentEmail;
|
|
33
16
|
private tokens;
|
|
34
17
|
private activeSubscriptions;
|
|
35
|
-
/**
|
|
36
|
-
* Set by `authenticateWithStoredTokens()` on every false-return path
|
|
37
|
-
* (and reset to `null` on success). Read by callers (e.g., the
|
|
38
|
-
* wizard's `defaultClientFactory`) to discriminate auth-rejection
|
|
39
|
-
* vs network-failure without re-running the auth call. Stage 2
|
|
40
|
-
* round-1 Codex M1.
|
|
41
|
-
*/
|
|
42
|
-
private lastAuthFailureKind;
|
|
43
|
-
/**
|
|
44
|
-
* Sentinel set inside `performRefresh` / `callCognitoRefresh` when
|
|
45
|
-
* the refresh round-trip fails with a network-shaped error (DNS,
|
|
46
|
-
* socket reset, fetch failed, 5xx). Reset to `false` at the start
|
|
47
|
-
* of each `performRefresh`. Read by `authenticateWithStoredTokens`
|
|
48
|
-
* to classify a `refreshTokens()=false` return as
|
|
49
|
-
* `refresh_network` vs `refresh_auth_rejected`. Internal — never
|
|
50
|
-
* exposed.
|
|
51
|
-
*/
|
|
52
|
-
private lastRefreshNetworkError;
|
|
53
18
|
private pendingRefresh;
|
|
54
19
|
private lastRefreshFailureAt;
|
|
55
20
|
private static readonly REFRESH_BACKOFF_MS;
|
|
56
21
|
private deviceKeyWatcher;
|
|
57
22
|
private sessionUpdateWatchers;
|
|
58
|
-
private userDecisionAppliedWatchers;
|
|
59
23
|
private environment;
|
|
60
24
|
constructor();
|
|
61
25
|
/**
|
|
@@ -66,30 +30,6 @@ export declare class AppSyncClient {
|
|
|
66
30
|
* Get the current authenticated user email
|
|
67
31
|
*/
|
|
68
32
|
getCurrentUserEmail(): string | null;
|
|
69
|
-
/**
|
|
70
|
-
* Returns the kind of the most recent
|
|
71
|
-
* `authenticateWithStoredTokens()` failure, or `null` if the call
|
|
72
|
-
* succeeded (or has never been called).
|
|
73
|
-
*
|
|
74
|
-
* Stage 2 round-1 Codex M1. Callers (today: `setup-bootstrap.ts
|
|
75
|
-
* :defaultClientFactory`) use this to distinguish network blow-ups
|
|
76
|
-
* during the Cognito refresh-token POST from genuine auth
|
|
77
|
-
* rejections. The wizard maps `'refresh_network'` to
|
|
78
|
-
* `subscription_status_network` (don't tell a signed-in user to
|
|
79
|
-
* re-login when their tokens are valid and the network is broken)
|
|
80
|
-
* and the other two kinds to `not_signed_in` (preserves
|
|
81
|
-
* pre-Codex-M1 behavior).
|
|
82
|
-
*/
|
|
83
|
-
getLastAuthFailureKind(): AuthFailureKind | null;
|
|
84
|
-
/**
|
|
85
|
-
* Heuristic — does the error message look like a transient network
|
|
86
|
-
* failure rather than an auth-token rejection? Mirrors
|
|
87
|
-
* `setup-bootstrap.ts:isNetworkLikeError` byte-for-byte so the same
|
|
88
|
-
* classifier runs both inside the client (for refresh-path
|
|
89
|
-
* discrimination) and at the bootstrap boundary (for caught-throw
|
|
90
|
-
* routing). Stage 2 round-1 Codex M1.
|
|
91
|
-
*/
|
|
92
|
-
private static isNetworkLikeMessage;
|
|
93
33
|
/**
|
|
94
34
|
* Authenticate using stored OAuth tokens from keychain
|
|
95
35
|
*/
|
|
@@ -171,25 +111,6 @@ export declare class AppSyncClient {
|
|
|
171
111
|
* Update event status
|
|
172
112
|
*/
|
|
173
113
|
updateEventStatus(input: UpdateEventStatusInput): Promise<Event>;
|
|
174
|
-
/**
|
|
175
|
-
* Submit the user's decision on an orchestration-escalated gate (Phase
|
|
176
|
-
* 3.b mobile V1 response-path bridge, #305). Returns the typed
|
|
177
|
-
* `UserDecisionAppliedEvent` envelope so the caller can update local
|
|
178
|
-
* UI / state tracking BEFORE the `onApplyUserDecision` subscription
|
|
179
|
-
* echoes back (the design is order-agnostic across the two — see
|
|
180
|
-
* `PHASE-3-B-MOBILE-V1-BRIDGE-DESIGN.md` §4.4).
|
|
181
|
-
*
|
|
182
|
-
* The plugin V1 bridge (codex / claude / gemini) adds the gateId to
|
|
183
|
-
* its `originatedDecisions` set BEFORE awaiting this call so the
|
|
184
|
-
* subscription handler can suppress the self-echo regardless of
|
|
185
|
-
* delivery order.
|
|
186
|
-
*
|
|
187
|
-
* Audit-layer dedup at the engine keys on
|
|
188
|
-
* `(task_id, current_round, decision)` — same-triple retries are
|
|
189
|
-
* absorbed silently per §4.1; different-triple races process both
|
|
190
|
-
* submissions independently per §4.3 (V1 last-arrival-wins).
|
|
191
|
-
*/
|
|
192
|
-
applyUserDecision(input: ApplyUserDecisionInput): Promise<UserDecisionAppliedEvent>;
|
|
193
114
|
/**
|
|
194
115
|
* List events for a session
|
|
195
116
|
*/
|
|
@@ -257,35 +178,6 @@ export declare class AppSyncClient {
|
|
|
257
178
|
* Get attachment download URL
|
|
258
179
|
*/
|
|
259
180
|
getAttachmentDownloadUrl(s3Key: string): Promise<DownloadUrlResponse>;
|
|
260
|
-
/**
|
|
261
|
-
* Plugin startup pushes the user's locally-detected agents
|
|
262
|
-
* (`CLAUDE` / `GEMINI` / `CODEX`). Idempotent — safe to call every
|
|
263
|
-
* launch. Backend stores in `User.availableAgents`; used later to
|
|
264
|
-
* derive tier-default reviewer seat assignments.
|
|
265
|
-
*/
|
|
266
|
-
updateAvailableAgents(agents: Array<'CLAUDE' | 'GEMINI' | 'CODEX'>): Promise<UserReviewerPolicySnapshot>;
|
|
267
|
-
/**
|
|
268
|
-
* Persist the user's orchestration opt-in default and/or custom
|
|
269
|
-
* reviewer panel. Backend validates seat-count against tier, seat_id
|
|
270
|
-
* uniqueness + range, and role uniqueness. Throws on validation
|
|
271
|
-
* failure — error message is user-facing (surfaced to the
|
|
272
|
-
* configure-reviewers wizard).
|
|
273
|
-
*/
|
|
274
|
-
updateReviewerPolicy(input: UpdateReviewerPolicyInput): Promise<UserReviewerPolicySnapshot>;
|
|
275
|
-
/**
|
|
276
|
-
* Fetch the user's subscription tier + status. Used by the Phase 3.a
|
|
277
|
-
* setup wizard (#190) at bootstrap to gate Free → upgrade interstitial
|
|
278
|
-
* and to size the seat budget (Pro=2, Max=3).
|
|
279
|
-
*
|
|
280
|
-
* Backend resolver returns a default FREE row when the user has no
|
|
281
|
-
* Users-table entry yet (Lambda resolver — lambda/subscription/index.ts).
|
|
282
|
-
* Network failure / auth expiry surface as graphqlRequest exceptions.
|
|
283
|
-
*/
|
|
284
|
-
getSubscriptionStatus(): Promise<{
|
|
285
|
-
tier: 'FREE' | 'PRO' | 'MAX';
|
|
286
|
-
status: 'ACTIVE' | 'EXPIRED' | 'GRACE_PERIOD' | 'BILLING_RETRY';
|
|
287
|
-
expiresAt: string | null;
|
|
288
|
-
}>;
|
|
289
181
|
/**
|
|
290
182
|
* Subscribe to events for a session
|
|
291
183
|
*/
|
|
@@ -396,36 +288,6 @@ export declare class AppSyncClient {
|
|
|
396
288
|
private resetSessionUpdateWatcherKeepAlive;
|
|
397
289
|
private handleSessionUpdateWatcherError;
|
|
398
290
|
private cleanupSessionUpdateWatcherState;
|
|
399
|
-
/**
|
|
400
|
-
* Subscribe to `onApplyUserDecision(sessionId)` per
|
|
401
|
-
* `PHASE-3-B-MOBILE-V1-BRIDGE-DESIGN.md` §3.2. Returns a stop()
|
|
402
|
-
* function the caller MUST invoke when ending the session to release
|
|
403
|
-
* the WebSocket. Replace-on-duplicate-sessionId semantics mirror
|
|
404
|
-
* `subscribeToEvents` and `watchForMobileEnd` — calling with the same
|
|
405
|
-
* sessionId twice stops the prior watcher.
|
|
406
|
-
*
|
|
407
|
-
* V1 contract:
|
|
408
|
-
* - Events from this subscription cover BOTH self-originated decisions
|
|
409
|
-
* (the plugin's own `applyUserDecision` call echoes back) AND
|
|
410
|
-
* foreign-origin decisions (mobile or another desktop session
|
|
411
|
-
* answered on the same sessionId).
|
|
412
|
-
* - The plugin distinguishes via its `originatedDecisions` set
|
|
413
|
-
* (populated BEFORE the mutation `await`) and routes accordingly
|
|
414
|
-
* per the design's §3.3 two-set model.
|
|
415
|
-
* - No missed-event recovery in V1 (§5.2). A WebSocket drop during a
|
|
416
|
-
* foreign decision leaves the plugin without the dismissal signal;
|
|
417
|
-
* the open agent prompt stays open until the desktop user types a
|
|
418
|
-
* number. Acceptable for V1 because drops are sub-percent of cross-
|
|
419
|
-
* device flows.
|
|
420
|
-
*/
|
|
421
|
-
subscribeToOnApplyUserDecision(sessionId: string, onEvent: (event: UserDecisionAppliedEvent) => void, onError?: (error: Error) => void): {
|
|
422
|
-
stop: () => void;
|
|
423
|
-
};
|
|
424
|
-
private createUserDecisionAppliedWatcherConnection;
|
|
425
|
-
private sendOnApplyUserDecisionStart;
|
|
426
|
-
private resetOnApplyUserDecisionKeepAlive;
|
|
427
|
-
private handleOnApplyUserDecisionError;
|
|
428
|
-
private cleanupUserDecisionAppliedWatcherState;
|
|
429
291
|
private heartbeatTimers;
|
|
430
292
|
/**
|
|
431
293
|
* Start periodic heartbeat for a session.
|
|
@@ -10,7 +10,6 @@ export declare const queries: {
|
|
|
10
10
|
* check.
|
|
11
11
|
*/
|
|
12
12
|
listSessions: string;
|
|
13
|
-
getSubscriptionStatus: string;
|
|
14
13
|
};
|
|
15
14
|
export declare const mutations: {
|
|
16
15
|
createSession: string;
|
|
@@ -20,13 +19,9 @@ export declare const mutations: {
|
|
|
20
19
|
registerDeviceKey: string;
|
|
21
20
|
grantSessionKey: string;
|
|
22
21
|
getAttachmentDownloadUrl: string;
|
|
23
|
-
updateAvailableAgents: string;
|
|
24
|
-
updateReviewerPolicy: string;
|
|
25
|
-
applyUserDecision: string;
|
|
26
22
|
};
|
|
27
23
|
export declare const subscriptions: {
|
|
28
24
|
onEventCreated: string;
|
|
29
25
|
onDeviceKeyRegistered: string;
|
|
30
|
-
onApplyUserDecision: string;
|
|
31
26
|
onSessionUpdated: string;
|
|
32
27
|
};
|