@juspay/shooter 1.8.0 → 1.9.0
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/hooks/notifier.cjs +481 -134
- package/bin/shooter.cjs +50 -14
- package/build/client/_app/immutable/chunks/3EfvnCrr.js +1 -0
- package/build/client/_app/immutable/chunks/3EfvnCrr.js.br +0 -0
- package/build/client/_app/immutable/chunks/3EfvnCrr.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{B6b4w6vf.js → C2Qh_9aV.js} +1 -1
- package/build/client/_app/immutable/chunks/C2Qh_9aV.js.br +0 -0
- package/build/client/_app/immutable/chunks/C2Qh_9aV.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{BEa4nlMF.js → C2yx8lo8.js} +2 -2
- package/build/client/_app/immutable/chunks/C2yx8lo8.js.br +0 -0
- package/build/client/_app/immutable/chunks/C2yx8lo8.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{C7SeOWDG.js → klpn9j-A.js} +1 -1
- package/build/client/_app/immutable/chunks/klpn9j-A.js.br +0 -0
- package/build/client/_app/immutable/chunks/klpn9j-A.js.gz +0 -0
- package/build/client/_app/immutable/entry/{app.CP7226A7.js → app.Bfisx3a0.js} +2 -2
- package/build/client/_app/immutable/entry/app.Bfisx3a0.js.br +0 -0
- package/build/client/_app/immutable/entry/app.Bfisx3a0.js.gz +0 -0
- package/build/client/_app/immutable/entry/start.BfgeeQYG.js +1 -0
- package/build/client/_app/immutable/entry/start.BfgeeQYG.js.br +2 -0
- package/build/client/_app/immutable/entry/start.BfgeeQYG.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{0.DwU44ZAj.js → 0.Vg8bq-s8.js} +1 -1
- package/build/client/_app/immutable/nodes/0.Vg8bq-s8.js.br +0 -0
- package/build/client/_app/immutable/nodes/0.Vg8bq-s8.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{1.CChG-n6d.js → 1.B7kXtFIl.js} +1 -1
- package/build/client/_app/immutable/nodes/1.B7kXtFIl.js.br +0 -0
- package/build/client/_app/immutable/nodes/1.B7kXtFIl.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{2.CzexDbwp.js → 2.DVFe_SN2.js} +2 -2
- package/build/client/_app/immutable/nodes/2.DVFe_SN2.js.br +0 -0
- package/build/client/_app/immutable/nodes/2.DVFe_SN2.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{3.DC3WghxB.js → 3.Deb3vtJl.js} +3 -3
- package/build/client/_app/immutable/nodes/3.Deb3vtJl.js.br +0 -0
- package/build/client/_app/immutable/nodes/3.Deb3vtJl.js.gz +0 -0
- package/build/client/_app/immutable/nodes/5.BN2SM61w.js +1 -0
- package/build/client/_app/immutable/nodes/5.BN2SM61w.js.br +0 -0
- package/build/client/_app/immutable/nodes/5.BN2SM61w.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{6.C4aXlZQd.js → 6.CS_KYbQ7.js} +1 -1
- package/build/client/_app/immutable/nodes/6.CS_KYbQ7.js.br +0 -0
- package/build/client/_app/immutable/nodes/6.CS_KYbQ7.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{7.DfniCleW.js → 7.CEiUUm74.js} +1 -1
- package/build/client/_app/immutable/nodes/7.CEiUUm74.js.br +0 -0
- package/build/client/_app/immutable/nodes/7.CEiUUm74.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{8.D4AzZWcq.js → 8.DGStHrkF.js} +1 -1
- package/build/client/_app/immutable/nodes/8.DGStHrkF.js.br +0 -0
- package/build/client/_app/immutable/nodes/8.DGStHrkF.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{9.gV8oJWv_.js → 9.CbIw97FV.js} +1 -1
- package/build/client/_app/immutable/nodes/9.CbIw97FV.js.br +0 -0
- package/build/client/_app/immutable/nodes/9.CbIw97FV.js.gz +0 -0
- package/build/client/_app/version.json +1 -1
- package/build/client/_app/version.json.br +0 -0
- package/build/client/_app/version.json.gz +0 -0
- package/build/server/chunks/{0-XyVDlEyN.js → 0-ikRVFZQS.js} +2 -2
- package/build/server/chunks/{0-XyVDlEyN.js.map → 0-ikRVFZQS.js.map} +1 -1
- package/build/server/chunks/{1-C3vZx9QL.js → 1-CG-fUUbt.js} +2 -2
- package/build/server/chunks/{1-C3vZx9QL.js.map → 1-CG-fUUbt.js.map} +1 -1
- package/build/server/chunks/{2-LWO3Q9-s.js → 2-BaV1q6GP.js} +2 -2
- package/build/server/chunks/{2-LWO3Q9-s.js.map → 2-BaV1q6GP.js.map} +1 -1
- package/build/server/chunks/{3-3WzO52IA.js → 3-BpzQzk4m.js} +2 -2
- package/build/server/chunks/{3-3WzO52IA.js.map → 3-BpzQzk4m.js.map} +1 -1
- package/build/server/chunks/{5-Bj49x3to.js → 5-DRhcUdp_.js} +2 -2
- package/build/server/chunks/{5-Bj49x3to.js.map → 5-DRhcUdp_.js.map} +1 -1
- package/build/server/chunks/{6-DjPIWYcj.js → 6-Bx-SE48t.js} +2 -2
- package/build/server/chunks/{6-DjPIWYcj.js.map → 6-Bx-SE48t.js.map} +1 -1
- package/build/server/chunks/{7-DF5FUXhP.js → 7-LeGA4bt3.js} +2 -2
- package/build/server/chunks/{7-DF5FUXhP.js.map → 7-LeGA4bt3.js.map} +1 -1
- package/build/server/chunks/{8-CejJgM0l.js → 8-CXwtG-B-.js} +2 -2
- package/build/server/chunks/{8-CejJgM0l.js.map → 8-CXwtG-B-.js.map} +1 -1
- package/build/server/chunks/{9-D1YMozmH.js → 9-DBztKl9o.js} +2 -2
- package/build/server/chunks/{9-D1YMozmH.js.map → 9-DBztKl9o.js.map} +1 -1
- package/build/server/chunks/{_server.ts-A9_tRR-K.js → _server.ts-9P1PrkiL.js} +2 -2
- package/build/server/chunks/{_server.ts-A9_tRR-K.js.map → _server.ts-9P1PrkiL.js.map} +1 -1
- package/build/server/chunks/{_server.ts-G8OeADGj.js → _server.ts-BAPg2K8u.js} +5 -2
- package/build/server/chunks/_server.ts-BAPg2K8u.js.map +1 -0
- package/build/server/chunks/{library-apns-Cf-E-DhM.js → library-apns-CUDtjHQk.js} +3 -2
- package/build/server/chunks/library-apns-CUDtjHQk.js.map +1 -0
- package/build/server/index.js +1 -1
- package/build/server/index.js.map +1 -1
- package/build/server/manifest.js +12 -12
- package/build/server/manifest.js.map +1 -1
- package/package.json +1 -1
- package/server.ts +55 -3
- package/src/lib/modules/client/activity/summarizer.ts +1 -1
- package/src/lib/modules/client/dashboard/summarizer.ts +1 -2
- package/src/lib/modules/client/neurolink/cdn.ts +6 -0
- package/src/lib/modules/client/terminal/LaunchSheet.svelte +5 -12
- package/src/lib/modules/server/apn/library-apns.ts +1 -0
- package/src/lib/modules/server/fcm/fcm-service.ts +1 -0
- package/src/lib/types/apn.ts +1 -0
- package/src/routes/api/notify/+server.ts +2 -0
- package/src/routes/neurolink/+page.svelte +4 -6
- package/build/client/_app/immutable/chunks/B6b4w6vf.js.br +0 -0
- package/build/client/_app/immutable/chunks/B6b4w6vf.js.gz +0 -0
- package/build/client/_app/immutable/chunks/BEa4nlMF.js.br +0 -0
- package/build/client/_app/immutable/chunks/BEa4nlMF.js.gz +0 -0
- package/build/client/_app/immutable/chunks/C7SeOWDG.js.br +0 -0
- package/build/client/_app/immutable/chunks/C7SeOWDG.js.gz +0 -0
- package/build/client/_app/immutable/chunks/DQM017d5.js +0 -1
- package/build/client/_app/immutable/chunks/DQM017d5.js.br +0 -0
- package/build/client/_app/immutable/chunks/DQM017d5.js.gz +0 -0
- package/build/client/_app/immutable/entry/app.CP7226A7.js.br +0 -0
- package/build/client/_app/immutable/entry/app.CP7226A7.js.gz +0 -0
- package/build/client/_app/immutable/entry/start.mGPvkOah.js +0 -1
- package/build/client/_app/immutable/entry/start.mGPvkOah.js.br +0 -2
- package/build/client/_app/immutable/entry/start.mGPvkOah.js.gz +0 -0
- package/build/client/_app/immutable/nodes/0.DwU44ZAj.js.br +0 -0
- package/build/client/_app/immutable/nodes/0.DwU44ZAj.js.gz +0 -0
- package/build/client/_app/immutable/nodes/1.CChG-n6d.js.br +0 -0
- package/build/client/_app/immutable/nodes/1.CChG-n6d.js.gz +0 -0
- package/build/client/_app/immutable/nodes/2.CzexDbwp.js.br +0 -0
- package/build/client/_app/immutable/nodes/2.CzexDbwp.js.gz +0 -0
- package/build/client/_app/immutable/nodes/3.DC3WghxB.js.br +0 -0
- package/build/client/_app/immutable/nodes/3.DC3WghxB.js.gz +0 -0
- package/build/client/_app/immutable/nodes/5.DRvLQ5NR.js +0 -1
- package/build/client/_app/immutable/nodes/5.DRvLQ5NR.js.br +0 -0
- package/build/client/_app/immutable/nodes/5.DRvLQ5NR.js.gz +0 -0
- package/build/client/_app/immutable/nodes/6.C4aXlZQd.js.br +0 -0
- package/build/client/_app/immutable/nodes/6.C4aXlZQd.js.gz +0 -0
- package/build/client/_app/immutable/nodes/7.DfniCleW.js.br +0 -0
- package/build/client/_app/immutable/nodes/7.DfniCleW.js.gz +0 -0
- package/build/client/_app/immutable/nodes/8.D4AzZWcq.js.br +0 -0
- package/build/client/_app/immutable/nodes/8.D4AzZWcq.js.gz +0 -0
- package/build/client/_app/immutable/nodes/9.gV8oJWv_.js.br +0 -0
- package/build/client/_app/immutable/nodes/9.gV8oJWv_.js.gz +0 -0
- package/build/server/chunks/_server.ts-G8OeADGj.js.map +0 -1
- package/build/server/chunks/library-apns-Cf-E-DhM.js.map +0 -1
|
@@ -221,6 +221,10 @@ function createCommonEvent(source, eventType, data = {}) {
|
|
|
221
221
|
function adaptClaudeCodeEvent(cliArg, stdinData) {
|
|
222
222
|
const data = {};
|
|
223
223
|
|
|
224
|
+
// session_id is sent by Claude Code on every hook — capture it once so
|
|
225
|
+
// getSessionContext() can read the goal + last user msg + last assistant text.
|
|
226
|
+
data.sessionId = stdinData?.session_id || '';
|
|
227
|
+
|
|
224
228
|
// --- PermissionRequest: Agent needs user permission to run a tool ---
|
|
225
229
|
if (cliArg === 'PermissionRequest') {
|
|
226
230
|
data.tool = stdinData?.tool_name || process.env.CLAUDE_TOOL_NAME || 'Unknown';
|
|
@@ -229,7 +233,6 @@ function adaptClaudeCodeEvent(cliArg, stdinData) {
|
|
|
229
233
|
data.command = data.toolInput.command || '';
|
|
230
234
|
data.filePath = data.toolInput.file_path || '';
|
|
231
235
|
data.description = data.toolInput.description || '';
|
|
232
|
-
data.sessionId = stdinData?.session_id || '';
|
|
233
236
|
return createCommonEvent('claude-code', 'permission', data);
|
|
234
237
|
}
|
|
235
238
|
|
|
@@ -623,7 +626,8 @@ async function handlePermission(event) {
|
|
|
623
626
|
const d = event.data;
|
|
624
627
|
debugLog(`Permission event: tool=${d.tool}, message=${d.message}`);
|
|
625
628
|
|
|
626
|
-
const
|
|
629
|
+
const ctx = getSessionContext(d.sessionId);
|
|
630
|
+
const { title, subtitle, body } = buildPermissionNotification(event, ctx);
|
|
627
631
|
|
|
628
632
|
// Check if WebSocket clients are connected — if so, the events channel
|
|
629
633
|
// will broadcast the permission-requested event and we skip the push notification
|
|
@@ -655,11 +659,19 @@ async function handlePermission(event) {
|
|
|
655
659
|
event.source,
|
|
656
660
|
requestId,
|
|
657
661
|
d,
|
|
658
|
-
{ skipPush: true }
|
|
662
|
+
{ skipPush: true, subtitle }
|
|
659
663
|
);
|
|
660
664
|
} else {
|
|
661
665
|
// No WebSocket clients — send push notification and poll
|
|
662
|
-
result = await sendNotificationAndPoll(
|
|
666
|
+
result = await sendNotificationAndPoll(
|
|
667
|
+
title,
|
|
668
|
+
body,
|
|
669
|
+
'permission',
|
|
670
|
+
event.source,
|
|
671
|
+
requestId,
|
|
672
|
+
d,
|
|
673
|
+
{ subtitle }
|
|
674
|
+
);
|
|
663
675
|
}
|
|
664
676
|
|
|
665
677
|
if (result && result.decision) {
|
|
@@ -684,7 +696,7 @@ async function handlePermission(event) {
|
|
|
684
696
|
if (wsActive) {
|
|
685
697
|
debugLog(`[Notifier] WebSocket clients connected, skipping push notification for permission`);
|
|
686
698
|
} else {
|
|
687
|
-
sendNotification(title, body, 'permission', event.source);
|
|
699
|
+
sendNotification(title, body, 'permission', event.source, subtitle);
|
|
688
700
|
}
|
|
689
701
|
}
|
|
690
702
|
|
|
@@ -706,8 +718,9 @@ async function handlePermissionNotification(event) {
|
|
|
706
718
|
return;
|
|
707
719
|
}
|
|
708
720
|
|
|
709
|
-
const
|
|
710
|
-
|
|
721
|
+
const ctx = getSessionContext(event.data.sessionId);
|
|
722
|
+
const { title, subtitle, body } = buildPermissionNotification(event, ctx);
|
|
723
|
+
sendNotification(title, body, 'permission', event.source, subtitle);
|
|
711
724
|
}
|
|
712
725
|
|
|
713
726
|
/**
|
|
@@ -720,9 +733,10 @@ function handleQuestion(event) {
|
|
|
720
733
|
const d = event.data;
|
|
721
734
|
debugLog(`Question event: message=${d.message}`);
|
|
722
735
|
|
|
723
|
-
const
|
|
736
|
+
const ctx = getSessionContext(d.sessionId);
|
|
737
|
+
const { title, subtitle, body } = buildQuestionNotification(event, ctx);
|
|
724
738
|
|
|
725
|
-
sendNotification(title, body, 'question', event.source);
|
|
739
|
+
sendNotification(title, body, 'question', event.source, subtitle);
|
|
726
740
|
}
|
|
727
741
|
|
|
728
742
|
/**
|
|
@@ -734,10 +748,34 @@ function handleIdleInput(event) {
|
|
|
734
748
|
const d = event.data;
|
|
735
749
|
debugLog(`Idle input event: message=${d.message}`);
|
|
736
750
|
|
|
737
|
-
const
|
|
738
|
-
const
|
|
751
|
+
const ctx = getSessionContext(d.sessionId);
|
|
752
|
+
const title = `${event.projectName} · Waiting for input`;
|
|
753
|
+
const subtitle = ctx.goal
|
|
754
|
+
? `Goal: ${summarize(ctx.goal, 70)}`
|
|
755
|
+
: summarize(d.message) || 'Agent is idle';
|
|
756
|
+
|
|
757
|
+
// Body priority: when Claude's hook message is boilerplate ("waiting for
|
|
758
|
+
// your input"), the agent's most recent text from the JSONL session carries
|
|
759
|
+
// the real "current state". Fall back to Claude's message when we can't
|
|
760
|
+
// read the session, or when the message has substance.
|
|
761
|
+
const lines = [];
|
|
762
|
+
const hookIsBoilerplate = isBoilerplate(d.message);
|
|
763
|
+
if (hookIsBoilerplate && ctx.lastAssistantText) {
|
|
764
|
+
lines.push(ctx.lastAssistantText);
|
|
765
|
+
} else if (d.message) {
|
|
766
|
+
lines.push(d.message);
|
|
767
|
+
} else if (ctx.lastAssistantText) {
|
|
768
|
+
lines.push(ctx.lastAssistantText);
|
|
769
|
+
} else {
|
|
770
|
+
lines.push('Agent is waiting.');
|
|
771
|
+
}
|
|
772
|
+
if (ctx.lastUserMessage && ctx.lastUserMessage !== ctx.goal) {
|
|
773
|
+
lines.push(`Asked: ${summarize(ctx.lastUserMessage, 120)}`);
|
|
774
|
+
}
|
|
775
|
+
let body = lines.join('\n');
|
|
776
|
+
if (body.length > 600) body = body.substring(0, 600) + '…';
|
|
739
777
|
|
|
740
|
-
sendNotification(title, body, 'idle_input', event.source);
|
|
778
|
+
sendNotification(title, body, 'idle_input', event.source, subtitle);
|
|
741
779
|
}
|
|
742
780
|
|
|
743
781
|
/**
|
|
@@ -749,20 +787,35 @@ function handleIntervention(event) {
|
|
|
749
787
|
const d = event.data;
|
|
750
788
|
debugLog(`Intervention event: message=${d.message}`);
|
|
751
789
|
|
|
752
|
-
const
|
|
753
|
-
const
|
|
790
|
+
const ctx = getSessionContext(d.sessionId);
|
|
791
|
+
const title = `${event.projectName} · Needs attention`;
|
|
792
|
+
const subtitle = ctx.goal
|
|
793
|
+
? `Goal: ${summarize(ctx.goal, 70)}`
|
|
794
|
+
: summarize(d.message) || summarize(d.title) || 'Intervention required';
|
|
795
|
+
|
|
796
|
+
const lines = [];
|
|
797
|
+
if (isBoilerplate(d.message) && ctx.lastAssistantText) {
|
|
798
|
+
lines.push(ctx.lastAssistantText);
|
|
799
|
+
} else if (d.message) {
|
|
800
|
+
lines.push(d.message);
|
|
801
|
+
} else if (ctx.lastAssistantText) {
|
|
802
|
+
lines.push(ctx.lastAssistantText);
|
|
803
|
+
} else {
|
|
804
|
+
lines.push('Agent needs your attention.');
|
|
805
|
+
}
|
|
806
|
+
if (ctx.lastUserMessage && ctx.lastUserMessage !== ctx.goal) {
|
|
807
|
+
lines.push(`Asked: ${summarize(ctx.lastUserMessage, 120)}`);
|
|
808
|
+
}
|
|
809
|
+
let body = lines.join('\n');
|
|
810
|
+
if (body.length > 600) body = body.substring(0, 600) + '…';
|
|
754
811
|
|
|
755
|
-
sendNotification(title, body, 'intervention', event.source);
|
|
812
|
+
sendNotification(title, body, 'intervention', event.source, subtitle);
|
|
756
813
|
}
|
|
757
814
|
|
|
758
815
|
function handleError(event) {
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
event.data.message || 'An error occurred',
|
|
763
|
-
'error',
|
|
764
|
-
event.source
|
|
765
|
-
);
|
|
816
|
+
// Errors are not actionable from the phone — the user can't fix a build error
|
|
817
|
+
// remotely. Track for telemetry only; do not send a push notification.
|
|
818
|
+
debugLog(`Error detected (not notifying): ${event.data.message}`);
|
|
766
819
|
}
|
|
767
820
|
|
|
768
821
|
function handleCheckCompletion(event) {
|
|
@@ -804,23 +857,13 @@ function handleUserPrompt(event) {
|
|
|
804
857
|
}
|
|
805
858
|
|
|
806
859
|
function handleTeammateIdle(event) {
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
`Teammate Idle`,
|
|
810
|
-
`${event.data.teammate} is idle in ${event.projectName}`,
|
|
811
|
-
'teammate_idle',
|
|
812
|
-
event.source
|
|
813
|
-
);
|
|
860
|
+
// Informational only — no user input required. Skip notification.
|
|
861
|
+
debugLog(`Teammate idle (not notifying): ${event.data.teammate}`);
|
|
814
862
|
}
|
|
815
863
|
|
|
816
864
|
function handleTaskCompleted(event) {
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
`Task Completed`,
|
|
820
|
-
event.data.message || `A task was completed in ${event.projectName}`,
|
|
821
|
-
'task_completed',
|
|
822
|
-
event.source
|
|
823
|
-
);
|
|
865
|
+
// Informational only — task completion does not require a decision. Skip.
|
|
866
|
+
debugLog(`Task completed (not notifying): ${event.data.message}`);
|
|
824
867
|
}
|
|
825
868
|
|
|
826
869
|
// ============================================
|
|
@@ -828,18 +871,274 @@ function handleTaskCompleted(event) {
|
|
|
828
871
|
// ============================================
|
|
829
872
|
|
|
830
873
|
/**
|
|
831
|
-
*
|
|
832
|
-
*
|
|
874
|
+
* Map a tool name to a short verb describing what it's about to do.
|
|
875
|
+
* Used in the subtitle line, e.g. "Bash — run command".
|
|
876
|
+
* Returns 'use tool' as the catch-all so MCP / unknown tools still render cleanly.
|
|
877
|
+
*/
|
|
878
|
+
function toolVerb(toolName) {
|
|
879
|
+
switch (toolName) {
|
|
880
|
+
case 'Bash':
|
|
881
|
+
return 'run command';
|
|
882
|
+
case 'BashOutput':
|
|
883
|
+
return 'read shell output';
|
|
884
|
+
case 'Edit':
|
|
885
|
+
return 'modify file';
|
|
886
|
+
case 'Glob':
|
|
887
|
+
return 'search files';
|
|
888
|
+
case 'Grep':
|
|
889
|
+
return 'search content';
|
|
890
|
+
case 'KillShell':
|
|
891
|
+
return 'kill shell';
|
|
892
|
+
case 'NotebookEdit':
|
|
893
|
+
return 'edit notebook';
|
|
894
|
+
case 'Read':
|
|
895
|
+
return 'read file';
|
|
896
|
+
case 'Skill':
|
|
897
|
+
return 'invoke skill';
|
|
898
|
+
case 'Task':
|
|
899
|
+
return 'spawn agent';
|
|
900
|
+
case 'TodoWrite':
|
|
901
|
+
return 'manage tasks';
|
|
902
|
+
case 'WebFetch':
|
|
903
|
+
return 'fetch URL';
|
|
904
|
+
case 'WebSearch':
|
|
905
|
+
return 'search web';
|
|
906
|
+
case 'Write':
|
|
907
|
+
return 'create file';
|
|
908
|
+
default:
|
|
909
|
+
return 'use tool';
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
/**
|
|
914
|
+
* Build the metadata footer appended to every notification body.
|
|
915
|
+
* Replaces the old "[Claude] [LOCAL]" title prefix with a low-weight suffix
|
|
916
|
+
* line, e.g. "— claude code" or "— opencode". The local/remote breadcrumb is
|
|
917
|
+
* intentionally omitted — `data.environment` carries it for debug consumers.
|
|
918
|
+
*/
|
|
919
|
+
function buildFooter(source) {
|
|
920
|
+
const runtime = source === 'opencode' ? 'opencode' : 'claude code';
|
|
921
|
+
return `— ${runtime}`;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
/** Append the runtime footer to a body, separated by a newline. */
|
|
925
|
+
function withFooter(body, source) {
|
|
926
|
+
const footer = buildFooter(source);
|
|
927
|
+
if (!body) return footer;
|
|
928
|
+
return `${body}\n${footer}`;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
/**
|
|
932
|
+
* Read head and tail chunks from a Claude Code session JSONL file.
|
|
833
933
|
*
|
|
834
|
-
*
|
|
835
|
-
*
|
|
836
|
-
*
|
|
934
|
+
* Claude Code stores sessions at ~/.claude/projects/<encoded-cwd>/<session_id>.jsonl
|
|
935
|
+
* where the encoded cwd replaces every '/' with '-'. Returns parsed lines from
|
|
936
|
+
* the head (for the goal — first user prompt) and tail (for the last user msg
|
|
937
|
+
* and last assistant text). For small sessions, both arrays are the same.
|
|
837
938
|
*
|
|
838
|
-
*
|
|
839
|
-
*
|
|
840
|
-
|
|
939
|
+
* Returns { firstLines, lastLines } as arrays of trimmed line strings.
|
|
940
|
+
* Returns { firstLines: [], lastLines: [] } on any failure.
|
|
941
|
+
*/
|
|
942
|
+
function readSessionLines(sessionId, cwd) {
|
|
943
|
+
if (!sessionId) return { firstLines: [], lastLines: [] };
|
|
944
|
+
try {
|
|
945
|
+
const projectsDir = path.join(require('os').homedir(), '.claude', 'projects');
|
|
946
|
+
const encodedCwd = (cwd || process.cwd()).replace(/\//g, '-');
|
|
947
|
+
const sessionFile = path.join(projectsDir, encodedCwd, `${sessionId}.jsonl`);
|
|
948
|
+
|
|
949
|
+
if (!fs.existsSync(sessionFile)) return { firstLines: [], lastLines: [] };
|
|
950
|
+
|
|
951
|
+
const stat = fs.statSync(sessionFile);
|
|
952
|
+
const headSize = 262144; // 256 KB — enough to find the first user message
|
|
953
|
+
const tailSize = 524288; // 512 KB — enough to find the most recent ones
|
|
954
|
+
const fd = fs.openSync(sessionFile, 'r');
|
|
955
|
+
|
|
956
|
+
let firstLines, lastLines;
|
|
957
|
+
if (stat.size <= headSize + tailSize) {
|
|
958
|
+
// Small/medium session — read whole file once
|
|
959
|
+
const buf = Buffer.alloc(stat.size);
|
|
960
|
+
fs.readSync(fd, buf, 0, stat.size, 0);
|
|
961
|
+
fs.closeSync(fd);
|
|
962
|
+
const all = buf.toString('utf-8').split('\n').filter((l) => l.trim());
|
|
963
|
+
firstLines = all;
|
|
964
|
+
lastLines = all;
|
|
965
|
+
} else {
|
|
966
|
+
// Huge session — read both ends. Drop the boundary lines (likely partial).
|
|
967
|
+
const headBuf = Buffer.alloc(headSize);
|
|
968
|
+
fs.readSync(fd, headBuf, 0, headSize, 0);
|
|
969
|
+
const tailBuf = Buffer.alloc(tailSize);
|
|
970
|
+
fs.readSync(fd, tailBuf, 0, tailSize, stat.size - tailSize);
|
|
971
|
+
fs.closeSync(fd);
|
|
972
|
+
|
|
973
|
+
firstLines = headBuf.toString('utf-8').split('\n').filter((l) => l.trim());
|
|
974
|
+
if (firstLines.length > 0) firstLines.pop(); // drop possibly-partial last line
|
|
975
|
+
lastLines = tailBuf.toString('utf-8').split('\n').filter((l) => l.trim());
|
|
976
|
+
if (lastLines.length > 0) lastLines.shift(); // drop possibly-partial first line
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
return { firstLines, lastLines };
|
|
980
|
+
} catch {
|
|
981
|
+
return { firstLines: [], lastLines: [] };
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
/**
|
|
986
|
+
* Extract the user prompt text from a parsed JSONL entry, or '' if it isn't a
|
|
987
|
+
* real user message (tool result, system-injected wrapper, empty, etc.).
|
|
841
988
|
*/
|
|
842
|
-
function
|
|
989
|
+
function extractUserPromptText(entry) {
|
|
990
|
+
if (!entry || entry.type !== 'user' || !entry.message) return '';
|
|
991
|
+
const content = entry.message.content;
|
|
992
|
+
let text = '';
|
|
993
|
+
if (typeof content === 'string') {
|
|
994
|
+
text = content;
|
|
995
|
+
} else if (Array.isArray(content)) {
|
|
996
|
+
for (const block of content) {
|
|
997
|
+
if (!block || typeof block !== 'object') continue;
|
|
998
|
+
if (block.type === 'text' && typeof block.text === 'string') {
|
|
999
|
+
text = block.text;
|
|
1000
|
+
break;
|
|
1001
|
+
}
|
|
1002
|
+
// tool_result and other non-text blocks are not user prompts
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
if (!text) return '';
|
|
1006
|
+
const trimmed = text.replace(/\s+/g, ' ').trim();
|
|
1007
|
+
if (!trimmed) return '';
|
|
1008
|
+
|
|
1009
|
+
// Wrappers Claude Code injects that aren't real user prompts.
|
|
1010
|
+
if (trimmed.startsWith('<system-reminder>')) return '';
|
|
1011
|
+
if (trimmed.startsWith('<command-message>')) return '';
|
|
1012
|
+
if (trimmed.startsWith('<command-name>')) return '';
|
|
1013
|
+
if (trimmed.startsWith('<bash-stdout>')) return '';
|
|
1014
|
+
if (trimmed.startsWith('<bash-stderr>')) return '';
|
|
1015
|
+
if (trimmed.startsWith('Caveat:')) return '';
|
|
1016
|
+
if (trimmed.startsWith('Base directory for this skill:')) return '';
|
|
1017
|
+
|
|
1018
|
+
return trimmed;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
/**
|
|
1022
|
+
* Extract user-facing assistant text from a parsed JSONL entry. Skips thinking
|
|
1023
|
+
* blocks and tool_use blocks; returns the first text block's content.
|
|
1024
|
+
*/
|
|
1025
|
+
function extractAssistantText(entry) {
|
|
1026
|
+
if (!entry || entry.type !== 'assistant' || !entry.message) return '';
|
|
1027
|
+
const content = entry.message.content;
|
|
1028
|
+
if (!Array.isArray(content)) return '';
|
|
1029
|
+
for (const block of content) {
|
|
1030
|
+
if (!block || typeof block !== 'object') continue;
|
|
1031
|
+
if (block.type === 'text' && typeof block.text === 'string' && block.text.trim()) {
|
|
1032
|
+
return block.text.replace(/\s+/g, ' ').trim();
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
return '';
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
/**
|
|
1039
|
+
* Walk session JSONL and surface the three pieces of context we need:
|
|
1040
|
+
* - goal: first real user prompt (the original ask)
|
|
1041
|
+
* - lastUserMessage: most recent real user prompt (the most recent ask)
|
|
1042
|
+
* - lastAssistantText: most recent agent text output (current state)
|
|
1043
|
+
*
|
|
1044
|
+
* Cheap enough to call per notification — bounded by readSessionLines's caps.
|
|
1045
|
+
*/
|
|
1046
|
+
function getSessionContext(sessionId, cwd) {
|
|
1047
|
+
const empty = { goal: '', lastUserMessage: '', lastAssistantText: '' };
|
|
1048
|
+
const { firstLines, lastLines } = readSessionLines(sessionId, cwd);
|
|
1049
|
+
if (firstLines.length === 0 && lastLines.length === 0) return empty;
|
|
1050
|
+
|
|
1051
|
+
// Goal: first user prompt in the head.
|
|
1052
|
+
let goal = '';
|
|
1053
|
+
for (const line of firstLines) {
|
|
1054
|
+
let entry;
|
|
1055
|
+
try {
|
|
1056
|
+
entry = JSON.parse(line);
|
|
1057
|
+
} catch {
|
|
1058
|
+
continue;
|
|
1059
|
+
}
|
|
1060
|
+
const text = extractUserPromptText(entry);
|
|
1061
|
+
if (text) {
|
|
1062
|
+
goal = text;
|
|
1063
|
+
break;
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
// Last user message and last assistant text: walk tail backwards.
|
|
1068
|
+
let lastUserMessage = '';
|
|
1069
|
+
let lastAssistantText = '';
|
|
1070
|
+
for (let i = lastLines.length - 1; i >= 0; i--) {
|
|
1071
|
+
if (lastUserMessage && lastAssistantText) break;
|
|
1072
|
+
let entry;
|
|
1073
|
+
try {
|
|
1074
|
+
entry = JSON.parse(lastLines[i]);
|
|
1075
|
+
} catch {
|
|
1076
|
+
continue;
|
|
1077
|
+
}
|
|
1078
|
+
if (!lastUserMessage) {
|
|
1079
|
+
const t = extractUserPromptText(entry);
|
|
1080
|
+
if (t) lastUserMessage = t;
|
|
1081
|
+
}
|
|
1082
|
+
if (!lastAssistantText) {
|
|
1083
|
+
const t = extractAssistantText(entry);
|
|
1084
|
+
if (t) lastAssistantText = t;
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
return { goal, lastUserMessage, lastAssistantText };
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
/**
|
|
1092
|
+
* Detect Claude Code's boilerplate hook messages — short, generic strings like
|
|
1093
|
+
* "Claude is waiting for your input." that don't tell the user anything new
|
|
1094
|
+
* beyond what the title already says. When a hook message looks like this we
|
|
1095
|
+
* fall back to JSONL-derived assistant text, which carries real context.
|
|
1096
|
+
*/
|
|
1097
|
+
function isBoilerplate(text) {
|
|
1098
|
+
if (!text) return true;
|
|
1099
|
+
const t = String(text).trim().toLowerCase();
|
|
1100
|
+
if (t.length < 8) return true;
|
|
1101
|
+
if (t.length > 80) return false;
|
|
1102
|
+
return (
|
|
1103
|
+
/\bwaiting\b/.test(t) ||
|
|
1104
|
+
/\bawaiting\b/.test(t) ||
|
|
1105
|
+
/\bidle\b/.test(t) ||
|
|
1106
|
+
/^(claude is|agent is|ready)\b/.test(t) ||
|
|
1107
|
+
/\b(needs your input|input needed|attention required)\b/.test(t)
|
|
1108
|
+
);
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
/**
|
|
1112
|
+
* Compress a message into a subtitle-sized summary.
|
|
1113
|
+
* Prefers the first sentence; falls back to a hard char limit. Whitespace and
|
|
1114
|
+
* newlines are normalised so multi-line agent prose collapses into one line.
|
|
1115
|
+
*/
|
|
1116
|
+
function summarize(text, maxLen = 80) {
|
|
1117
|
+
if (!text) return '';
|
|
1118
|
+
const trimmed = String(text).replace(/\s+/g, ' ').trim();
|
|
1119
|
+
if (!trimmed) return '';
|
|
1120
|
+
|
|
1121
|
+
// First sentence: ends in . ? or ! followed by whitespace or end-of-string.
|
|
1122
|
+
// Lookahead avoids slicing at file extensions ("auth.ts") or abbreviations.
|
|
1123
|
+
const sentenceMatch = trimmed.match(/^[^.!?]+[.!?](?=\s|$)/);
|
|
1124
|
+
if (sentenceMatch && sentenceMatch[0].length <= maxLen) {
|
|
1125
|
+
return sentenceMatch[0].trim();
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
if (trimmed.length <= maxLen) return trimmed;
|
|
1129
|
+
return trimmed.substring(0, maxLen).trim() + '…';
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
/**
|
|
1133
|
+
* Build permission notification content.
|
|
1134
|
+
* Returns a consistent { title, subtitle, body } shape regardless of tool or source.
|
|
1135
|
+
*
|
|
1136
|
+
* title: "<project> · Permission needed"
|
|
1137
|
+
* subtitle: "<Tool> — <verb>" (e.g. "Bash — run command")
|
|
1138
|
+
* body: <description if present>
|
|
1139
|
+
* <command / file path / argument summary>
|
|
1140
|
+
*/
|
|
1141
|
+
function buildPermissionNotification(event, ctx = {}) {
|
|
843
1142
|
const d = event.data;
|
|
844
1143
|
const toolName = d.tool || '';
|
|
845
1144
|
const command = d.command || '';
|
|
@@ -847,101 +1146,141 @@ function buildPermissionNotification(event) {
|
|
|
847
1146
|
const description = d.description || '';
|
|
848
1147
|
const message = d.message || '';
|
|
849
1148
|
|
|
850
|
-
|
|
1149
|
+
const goal = ctx.goal || '';
|
|
1150
|
+
const lastUserMessage = ctx.lastUserMessage || '';
|
|
1151
|
+
|
|
1152
|
+
const title = `${event.projectName} · Permission needed`;
|
|
1153
|
+
|
|
1154
|
+
// Layered shape:
|
|
1155
|
+
// title = project + kind
|
|
1156
|
+
// subtitle = original goal (highest-level context)
|
|
1157
|
+
// body = "<Tool>: <description>" (what the agent wants to do now)
|
|
1158
|
+
// "Asked: <last user msg>" (only if distinct from goal)
|
|
1159
|
+
// <command/file> (the specific data)
|
|
1160
|
+
// footer (added by sendNotification)
|
|
851
1161
|
if (toolName && toolName !== 'Unknown' && toolName !== '') {
|
|
852
|
-
const
|
|
853
|
-
|
|
1162
|
+
const subtitle = goal
|
|
1163
|
+
? `Goal: ${summarize(goal, 70)}`
|
|
1164
|
+
: description
|
|
1165
|
+
? `${toolName}: ${summarize(description, 70)}`
|
|
1166
|
+
: `${toolName} — ${toolVerb(toolName)}`;
|
|
854
1167
|
|
|
1168
|
+
let detail = '';
|
|
855
1169
|
if (toolName === 'Bash' && command) {
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
1170
|
+
detail = command;
|
|
1171
|
+
} else if (
|
|
1172
|
+
(toolName === 'Edit' ||
|
|
1173
|
+
toolName === 'Write' ||
|
|
1174
|
+
toolName === 'Read' ||
|
|
1175
|
+
toolName === 'NotebookEdit') &&
|
|
1176
|
+
filePath
|
|
1177
|
+
) {
|
|
1178
|
+
detail = filePath;
|
|
863
1179
|
} else if (command) {
|
|
864
|
-
|
|
1180
|
+
detail = command;
|
|
865
1181
|
} else if (filePath) {
|
|
866
|
-
|
|
867
|
-
} else {
|
|
868
|
-
body = `Approve ${toolName} in ${event.projectName}`;
|
|
1182
|
+
detail = filePath;
|
|
869
1183
|
}
|
|
1184
|
+
if (detail.length > 500) detail = detail.substring(0, 500) + '…';
|
|
870
1185
|
|
|
871
|
-
|
|
1186
|
+
const lines = [];
|
|
1187
|
+
// What the agent wants to do (description) — only push when goal occupies
|
|
1188
|
+
// the subtitle, otherwise it's in the subtitle already.
|
|
1189
|
+
if (goal && description) {
|
|
1190
|
+
lines.push(`${toolName}: ${description}`);
|
|
1191
|
+
}
|
|
1192
|
+
// What the user most recently asked — only when it's a distinct follow-up,
|
|
1193
|
+
// not the original goal restated.
|
|
1194
|
+
if (lastUserMessage && lastUserMessage !== goal) {
|
|
1195
|
+
lines.push(`Asked: ${summarize(lastUserMessage, 120)}`);
|
|
1196
|
+
}
|
|
1197
|
+
if (detail) {
|
|
1198
|
+
lines.push(detail);
|
|
1199
|
+
}
|
|
1200
|
+
if (lines.length === 0) {
|
|
1201
|
+
lines.push(message || `Approve ${toolName}`);
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
return { title, subtitle, body: lines.join('\n') };
|
|
872
1205
|
}
|
|
873
1206
|
|
|
874
|
-
//
|
|
1207
|
+
// No tool name (Notification permission_prompt path).
|
|
1208
|
+
const subtitle = goal
|
|
1209
|
+
? `Goal: ${summarize(goal, 70)}`
|
|
1210
|
+
: summarize(message) || 'Permission';
|
|
1211
|
+
|
|
875
1212
|
if (message) {
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
}
|
|
1213
|
+
const lines = [message];
|
|
1214
|
+
if (lastUserMessage && lastUserMessage !== goal && lastUserMessage !== message) {
|
|
1215
|
+
lines.push(`Asked: ${summarize(lastUserMessage, 120)}`);
|
|
1216
|
+
}
|
|
1217
|
+
return { title, subtitle, body: lines.join('\n') };
|
|
880
1218
|
}
|
|
881
1219
|
|
|
882
|
-
|
|
883
|
-
return {
|
|
884
|
-
title: `Permission Needed`,
|
|
885
|
-
body: `Agent needs permission in ${event.projectName}`,
|
|
886
|
-
};
|
|
1220
|
+
return { title, subtitle, body: 'Agent needs permission' };
|
|
887
1221
|
}
|
|
888
1222
|
|
|
889
1223
|
/**
|
|
890
1224
|
* Build question/elicitation notification content.
|
|
891
|
-
*
|
|
892
|
-
*
|
|
893
|
-
* Handles two formats:
|
|
894
|
-
* 1. Claude Code: { message: "question text", title: "..." }
|
|
895
|
-
* 2. OpenCode: { questions: [{ header, options: [{ label, description }] }] }
|
|
1225
|
+
* Returns { title, subtitle, body }.
|
|
896
1226
|
*
|
|
897
|
-
*
|
|
898
|
-
*
|
|
899
|
-
*
|
|
1227
|
+
* title: "<project> · Question"
|
|
1228
|
+
* subtitle: <question header (truncated)> (or 'Awaiting answer' fallback)
|
|
1229
|
+
* body: <question text>
|
|
1230
|
+
* [Options: A / B / C]
|
|
900
1231
|
*/
|
|
901
|
-
function buildQuestionNotification(event) {
|
|
1232
|
+
function buildQuestionNotification(event, ctx = {}) {
|
|
902
1233
|
const d = event.data;
|
|
903
1234
|
const message = d.message || '';
|
|
904
|
-
const title =
|
|
1235
|
+
const title = `${event.projectName} · Question`;
|
|
905
1236
|
const questions = d.questions || [];
|
|
906
1237
|
|
|
1238
|
+
const goal = ctx.goal || '';
|
|
1239
|
+
const lastUserMessage = ctx.lastUserMessage || '';
|
|
1240
|
+
|
|
907
1241
|
// Case 1: OpenCode question.asked with structured questions array
|
|
908
1242
|
if (questions.length > 0) {
|
|
909
|
-
const q = questions[0];
|
|
1243
|
+
const q = questions[0];
|
|
910
1244
|
const header = q.header || q.question || '';
|
|
911
1245
|
const options = (q.options || []).map((o) => o.label).filter(Boolean);
|
|
912
1246
|
|
|
913
|
-
const
|
|
914
|
-
|
|
1247
|
+
const subtitle = goal
|
|
1248
|
+
? `Goal: ${summarize(goal, 70)}`
|
|
1249
|
+
: summarize(header || q.question, 80) || 'Awaiting answer';
|
|
915
1250
|
|
|
916
|
-
|
|
917
|
-
|
|
1251
|
+
const lines = [];
|
|
1252
|
+
if (q.question) lines.push(q.question);
|
|
1253
|
+
else if (header) lines.push(header);
|
|
1254
|
+
if (options.length > 0) lines.push(`Options: ${options.join(' / ')}`);
|
|
1255
|
+
if (lastUserMessage && lastUserMessage !== goal) {
|
|
1256
|
+
lines.push(`Asked: ${summarize(lastUserMessage, 120)}`);
|
|
918
1257
|
}
|
|
1258
|
+
if (lines.length === 0) lines.push('Agent is asking a question');
|
|
919
1259
|
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
}
|
|
1260
|
+
let body = lines.join('\n');
|
|
1261
|
+
if (body.length > 500) body = body.substring(0, 500) + '…';
|
|
923
1262
|
|
|
924
|
-
return {
|
|
925
|
-
title: notifTitle,
|
|
926
|
-
body: body.length > 300 ? body.substring(0, 300) + '...' : body,
|
|
927
|
-
};
|
|
1263
|
+
return { title, subtitle, body };
|
|
928
1264
|
}
|
|
929
1265
|
|
|
930
|
-
// Case 2: Claude Code
|
|
931
|
-
|
|
1266
|
+
// Case 2: Claude Code Notification (elicitation_dialog). Goal is the original
|
|
1267
|
+
// user prompt; the agent's question itself is the body.
|
|
1268
|
+
const stdinTitle = d.title || '';
|
|
1269
|
+
const subtitle = goal
|
|
1270
|
+
? `Goal: ${summarize(goal, 70)}`
|
|
1271
|
+
: summarize(message, 80) ||
|
|
1272
|
+
(stdinTitle && stdinTitle !== 'Permission needed' ? summarize(stdinTitle, 80) : '') ||
|
|
1273
|
+
'Awaiting answer';
|
|
932
1274
|
|
|
933
1275
|
if (message) {
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
}
|
|
1276
|
+
const lines = [message.length > 500 ? message.substring(0, 500) + '…' : message];
|
|
1277
|
+
if (lastUserMessage && lastUserMessage !== goal && lastUserMessage !== message) {
|
|
1278
|
+
lines.push(`Asked: ${summarize(lastUserMessage, 120)}`);
|
|
1279
|
+
}
|
|
1280
|
+
return { title, subtitle, body: lines.join('\n') };
|
|
938
1281
|
}
|
|
939
1282
|
|
|
940
|
-
|
|
941
|
-
return {
|
|
942
|
-
title: 'Question',
|
|
943
|
-
body: `Agent is asking a question in ${event.projectName}`,
|
|
944
|
-
};
|
|
1283
|
+
return { title, subtitle, body: 'Agent is asking a question' };
|
|
945
1284
|
}
|
|
946
1285
|
|
|
947
1286
|
// ============================================
|
|
@@ -996,7 +1335,13 @@ function checkCompletion(projectName, source) {
|
|
|
996
1335
|
|
|
997
1336
|
const message = createCompletionMessage(state, projectName);
|
|
998
1337
|
|
|
999
|
-
sendNotification(
|
|
1338
|
+
sendNotification(
|
|
1339
|
+
`${projectName} · Session complete`,
|
|
1340
|
+
message,
|
|
1341
|
+
'completion',
|
|
1342
|
+
source,
|
|
1343
|
+
'Agent finished'
|
|
1344
|
+
);
|
|
1000
1345
|
|
|
1001
1346
|
state.pendingCompletion = false;
|
|
1002
1347
|
saveSessionState(state);
|
|
@@ -1005,26 +1350,24 @@ function checkCompletion(projectName, source) {
|
|
|
1005
1350
|
}
|
|
1006
1351
|
}
|
|
1007
1352
|
|
|
1008
|
-
function createCompletionMessage(state,
|
|
1353
|
+
function createCompletionMessage(state, _projectName) {
|
|
1009
1354
|
const timestamp = new Date().toLocaleTimeString();
|
|
1010
|
-
|
|
1355
|
+
const lines = [`Finished at ${timestamp}.`];
|
|
1011
1356
|
|
|
1012
1357
|
const totalTools = state.totalToolUses || 0;
|
|
1013
1358
|
if (totalTools > 0) {
|
|
1014
|
-
|
|
1359
|
+
lines.push(`${totalTools} tools used.`);
|
|
1015
1360
|
|
|
1016
1361
|
if (state.recentTools && state.recentTools.length > 0) {
|
|
1017
|
-
|
|
1018
|
-
message += ` | Recent: ${toolSummary}`;
|
|
1362
|
+
lines.push(`Recent: ${state.recentTools.slice(0, 3).join(', ')}`);
|
|
1019
1363
|
}
|
|
1020
1364
|
|
|
1021
1365
|
if (state.recentFiles && state.recentFiles.length > 0) {
|
|
1022
|
-
|
|
1023
|
-
message += ` | Files: ${fileSummary}`;
|
|
1366
|
+
lines.push(`Files: ${state.recentFiles.slice(0, 3).join(', ')}`);
|
|
1024
1367
|
}
|
|
1025
1368
|
}
|
|
1026
1369
|
|
|
1027
|
-
return
|
|
1370
|
+
return lines.join('\n');
|
|
1028
1371
|
}
|
|
1029
1372
|
|
|
1030
1373
|
// ============================================
|
|
@@ -1044,14 +1387,14 @@ function sendNotificationAndPoll(
|
|
|
1044
1387
|
source,
|
|
1045
1388
|
requestId,
|
|
1046
1389
|
eventData,
|
|
1047
|
-
{ skipPush = false } = {}
|
|
1390
|
+
{ skipPush = false, subtitle = '' } = {}
|
|
1048
1391
|
) {
|
|
1049
1392
|
return new Promise((resolve) => {
|
|
1050
1393
|
const timestamp = new Date().toISOString();
|
|
1051
1394
|
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
const
|
|
1395
|
+
// Title flows through unchanged; runtime/env metadata moves to a body suffix
|
|
1396
|
+
// line so it doesn't eat title space on the lock screen.
|
|
1397
|
+
const finalBody = withFooter(body, source);
|
|
1055
1398
|
|
|
1056
1399
|
// When skipPush is true (WebSocket clients connected), skip the actual
|
|
1057
1400
|
// POST to /api/notify but still register the pending request and poll.
|
|
@@ -1063,8 +1406,9 @@ function sendNotificationAndPoll(
|
|
|
1063
1406
|
// We still need to POST with waitForResponse so the server creates
|
|
1064
1407
|
// the pending-request entry, but we mark it as ws-only.
|
|
1065
1408
|
const registerPayload = JSON.stringify({
|
|
1066
|
-
title
|
|
1067
|
-
|
|
1409
|
+
title,
|
|
1410
|
+
...(subtitle ? { subtitle } : {}),
|
|
1411
|
+
message: finalBody,
|
|
1068
1412
|
waitForResponse: true,
|
|
1069
1413
|
skipPush: true,
|
|
1070
1414
|
...(DEVICE_TOKEN && { deviceToken: DEVICE_TOKEN }),
|
|
@@ -1128,11 +1472,12 @@ function sendNotificationAndPoll(
|
|
|
1128
1472
|
return;
|
|
1129
1473
|
}
|
|
1130
1474
|
|
|
1131
|
-
debugLog(`Sending bidirectional notification: "${
|
|
1475
|
+
debugLog(`Sending bidirectional notification: "${title}" (requestId: ${requestId})`);
|
|
1132
1476
|
|
|
1133
1477
|
const payload = JSON.stringify({
|
|
1134
|
-
title
|
|
1135
|
-
|
|
1478
|
+
title,
|
|
1479
|
+
...(subtitle ? { subtitle } : {}),
|
|
1480
|
+
message: finalBody,
|
|
1136
1481
|
waitForResponse: true,
|
|
1137
1482
|
...(DEVICE_TOKEN && { deviceToken: DEVICE_TOKEN }),
|
|
1138
1483
|
data: {
|
|
@@ -1168,8 +1513,9 @@ function sendNotificationAndPoll(
|
|
|
1168
1513
|
res.on('end', () => {
|
|
1169
1514
|
if (IS_CLAUDE_CODE) {
|
|
1170
1515
|
console.error(`\n=== BIDIRECTIONAL NOTIFICATION SENT [${requestId}] ===`);
|
|
1171
|
-
console.error(`Title: ${
|
|
1172
|
-
console.error(`
|
|
1516
|
+
console.error(`Title: ${title}`);
|
|
1517
|
+
if (subtitle) console.error(`Subtitle: ${subtitle}`);
|
|
1518
|
+
console.error(`Message: ${finalBody}`);
|
|
1173
1519
|
console.error(`Status: ${res.statusCode}`);
|
|
1174
1520
|
console.error(`=== NOW POLLING FOR RESPONSE ===\n`);
|
|
1175
1521
|
}
|
|
@@ -1285,22 +1631,22 @@ function startPolling(requestId, resolve) {
|
|
|
1285
1631
|
}, POLL_INTERVAL);
|
|
1286
1632
|
}
|
|
1287
1633
|
|
|
1288
|
-
function sendNotification(title, body, category = 'completion', source = RUNTIME) {
|
|
1634
|
+
function sendNotification(title, body, category = 'completion', source = RUNTIME, subtitle = '') {
|
|
1289
1635
|
const requestId = Math.random().toString(36).substring(2, 15);
|
|
1290
1636
|
const timestamp = new Date().toISOString();
|
|
1291
1637
|
|
|
1292
|
-
//
|
|
1293
|
-
|
|
1294
|
-
const
|
|
1295
|
-
const finalTitle = `${runtimePrefix}${envPrefix ? ' ' + envPrefix : ''} ${title}`;
|
|
1638
|
+
// Title flows through unchanged; runtime/env metadata is appended as a low-weight
|
|
1639
|
+
// body suffix (see buildFooter / withFooter in Section 8).
|
|
1640
|
+
const finalBody = withFooter(body, source);
|
|
1296
1641
|
|
|
1297
|
-
debugLog(`Sending notification: "${
|
|
1298
|
-
debugLog(` Message: "${
|
|
1642
|
+
debugLog(`Sending notification: "${title}"`);
|
|
1643
|
+
debugLog(` Message: "${finalBody.substring(0, 100)}..."`);
|
|
1299
1644
|
debugLog(` Category: ${category}, RequestID: ${requestId}`);
|
|
1300
1645
|
|
|
1301
1646
|
const payload = JSON.stringify({
|
|
1302
|
-
title
|
|
1303
|
-
|
|
1647
|
+
title,
|
|
1648
|
+
...(subtitle ? { subtitle } : {}),
|
|
1649
|
+
message: finalBody,
|
|
1304
1650
|
...(DEVICE_TOKEN && { deviceToken: DEVICE_TOKEN }),
|
|
1305
1651
|
data: {
|
|
1306
1652
|
category,
|
|
@@ -1333,8 +1679,9 @@ function sendNotification(title, body, category = 'completion', source = RUNTIME
|
|
|
1333
1679
|
console.error(`\n=== NOTIFICATION SENT [${requestId}] @ ${timestamp} ===`);
|
|
1334
1680
|
console.error(`Project: ${getProjectName()}`);
|
|
1335
1681
|
console.error(`Category: ${category}`);
|
|
1336
|
-
console.error(`Title: ${
|
|
1337
|
-
console.error(`
|
|
1682
|
+
console.error(`Title: ${title}`);
|
|
1683
|
+
if (subtitle) console.error(`Subtitle: ${subtitle}`);
|
|
1684
|
+
console.error(`Message: ${finalBody}`);
|
|
1338
1685
|
console.error(`API URL: ${API_URL} (${USE_LOCAL ? 'LOCAL' : 'REMOTE'})`);
|
|
1339
1686
|
console.error(`Status Code: ${res.statusCode}`);
|
|
1340
1687
|
console.error(`Response: ${responseData}`);
|