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