@askance/cli 0.2.4 → 0.3.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.
|
@@ -9,6 +9,7 @@ exports.apiGet = apiGet;
|
|
|
9
9
|
exports.apiPost = apiPost;
|
|
10
10
|
exports.intercept = intercept;
|
|
11
11
|
exports.getInstructions = getInstructions;
|
|
12
|
+
exports.sessionHeartbeat = sessionHeartbeat;
|
|
12
13
|
exports.waitForInstruction = waitForInstruction;
|
|
13
14
|
exports.readKeepAliveConfig = readKeepAliveConfig;
|
|
14
15
|
const node_https_1 = __importDefault(require("node:https"));
|
|
@@ -164,6 +165,9 @@ function intercept(payload) {
|
|
|
164
165
|
function getInstructions(projectId) {
|
|
165
166
|
return apiGet(`/api/instructions/${projectId}`);
|
|
166
167
|
}
|
|
168
|
+
function sessionHeartbeat(sessionId, lastMessage) {
|
|
169
|
+
return apiPost("/api/sessions/heartbeat", { sessionId, lastMessage });
|
|
170
|
+
}
|
|
167
171
|
function waitForInstruction(projectId, timeoutMs) {
|
|
168
172
|
return apiGet(`/api/instructions/${projectId}/wait?timeout=${timeoutMs}`, timeoutMs + 5000);
|
|
169
173
|
}
|
|
@@ -6,6 +6,14 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
7
7
|
const api_client_1 = require("../common/api-client");
|
|
8
8
|
const CONTEXT_LINES = 20;
|
|
9
|
+
const QUESTION_TOOLS = new Set([
|
|
10
|
+
"askuserquestion", "askfollowupquestion", "askquestion",
|
|
11
|
+
"ask_user", "ask_followup", "ask_question",
|
|
12
|
+
]);
|
|
13
|
+
function isQuestionTool(toolName) {
|
|
14
|
+
return QUESTION_TOOLS.has(toolName.toLowerCase()) ||
|
|
15
|
+
toolName.toLowerCase().includes("ask");
|
|
16
|
+
}
|
|
9
17
|
function readStdin() {
|
|
10
18
|
return new Promise((resolve, reject) => {
|
|
11
19
|
let data = "";
|
|
@@ -63,14 +71,16 @@ async function main() {
|
|
|
63
71
|
// Extract recent Claude reasoning from transcript
|
|
64
72
|
input.context_summary = extractContext(input.transcript_path ?? "");
|
|
65
73
|
const config = (0, api_client_1.readConfig)();
|
|
74
|
+
const toolName = input.tool_name ?? "";
|
|
66
75
|
// Map to cloud API format (PascalCase)
|
|
67
76
|
const payload = {
|
|
68
|
-
ToolName:
|
|
77
|
+
ToolName: toolName,
|
|
69
78
|
ToolInput: input.tool_input ? JSON.stringify(input.tool_input) : undefined,
|
|
70
79
|
SessionId: input.session_id,
|
|
71
80
|
Cwd: input.cwd,
|
|
72
81
|
ContextSummary: input.context_summary,
|
|
73
82
|
ProjectId: config.projectId || undefined,
|
|
83
|
+
IsQuestion: isQuestionTool(toolName),
|
|
74
84
|
};
|
|
75
85
|
const result = await (0, api_client_1.intercept)(payload);
|
|
76
86
|
if (!result.ok || !result.data) {
|
|
@@ -111,15 +121,23 @@ async function main() {
|
|
|
111
121
|
process.exit(0);
|
|
112
122
|
}
|
|
113
123
|
if (response.action === "pending") {
|
|
124
|
+
const isQuestion = response.is_question || isQuestionTool(toolName);
|
|
125
|
+
const reason = isQuestion
|
|
126
|
+
? `PENDING QUESTION (${response.approval_id}): ${response.rule}. ` +
|
|
127
|
+
`This question has been forwarded to the operator on the Askance dashboard. ` +
|
|
128
|
+
`The operator will type an answer. Call the mcp__askance__wait tool with ` +
|
|
129
|
+
`approval_id "${response.approval_id}" to receive their response. ` +
|
|
130
|
+
`Continue working on other tasks while waiting.`
|
|
131
|
+
: `PENDING APPROVAL (${response.approval_id}): ${response.rule}. ` +
|
|
132
|
+
`This action is queued for human approval on the Askance dashboard. ` +
|
|
133
|
+
`Continue working on other tasks. When you need the result, call the ` +
|
|
134
|
+
`mcp__askance__wait tool with approval_id "${response.approval_id}" ` +
|
|
135
|
+
`to check if it has been approved. Once approved, retry the original action.`;
|
|
114
136
|
const output = JSON.stringify({
|
|
115
137
|
hookSpecificOutput: {
|
|
116
138
|
hookEventName: "PreToolUse",
|
|
117
139
|
permissionDecision: "deny",
|
|
118
|
-
permissionDecisionReason:
|
|
119
|
-
`This action is queued for human approval on the Askance dashboard. ` +
|
|
120
|
-
`Continue working on other tasks. When you need the result, call the ` +
|
|
121
|
-
`mcp__askance__wait tool with approval_id "${response.approval_id}" ` +
|
|
122
|
-
`to check if it has been approved. Once approved, retry the original action.`,
|
|
140
|
+
permissionDecisionReason: reason,
|
|
123
141
|
},
|
|
124
142
|
});
|
|
125
143
|
process.stdout.write(output);
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
3
7
|
const api_client_1 = require("../common/api-client");
|
|
4
8
|
function readStdin() {
|
|
5
9
|
return new Promise((resolve, reject) => {
|
|
@@ -10,6 +14,46 @@ function readStdin() {
|
|
|
10
14
|
process.stdin.on("error", reject);
|
|
11
15
|
});
|
|
12
16
|
}
|
|
17
|
+
function extractLastAssistantMessage(transcriptPath) {
|
|
18
|
+
try {
|
|
19
|
+
if (!transcriptPath || !node_fs_1.default.existsSync(transcriptPath))
|
|
20
|
+
return "";
|
|
21
|
+
const content = node_fs_1.default.readFileSync(transcriptPath, "utf-8");
|
|
22
|
+
const lines = content.trim().split("\n");
|
|
23
|
+
// Walk backwards to find the last assistant message
|
|
24
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
25
|
+
try {
|
|
26
|
+
const entry = JSON.parse(lines[i]);
|
|
27
|
+
if (entry.type === "assistant" && entry.message?.content) {
|
|
28
|
+
const parts = Array.isArray(entry.message.content)
|
|
29
|
+
? entry.message.content
|
|
30
|
+
: [entry.message.content];
|
|
31
|
+
const texts = [];
|
|
32
|
+
for (const part of parts) {
|
|
33
|
+
if (typeof part === "string" && part.trim()) {
|
|
34
|
+
texts.push(part.trim());
|
|
35
|
+
}
|
|
36
|
+
else if (part?.type === "text" && part.text?.trim()) {
|
|
37
|
+
texts.push(part.text.trim());
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (texts.length > 0) {
|
|
41
|
+
const message = texts.join("\n");
|
|
42
|
+
// Truncate to 2000 chars for storage
|
|
43
|
+
return message.length > 2000 ? message.slice(0, 2000) + "..." : message;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// skip unparseable lines
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return "";
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return "";
|
|
55
|
+
}
|
|
56
|
+
}
|
|
13
57
|
async function main() {
|
|
14
58
|
let input;
|
|
15
59
|
try {
|
|
@@ -23,6 +67,18 @@ async function main() {
|
|
|
23
67
|
if (!config.projectId) {
|
|
24
68
|
process.exit(0);
|
|
25
69
|
}
|
|
70
|
+
// Extract and post the last assistant message to the dashboard
|
|
71
|
+
if (input.session_id && input.transcript_path) {
|
|
72
|
+
try {
|
|
73
|
+
const lastMessage = extractLastAssistantMessage(input.transcript_path);
|
|
74
|
+
if (lastMessage) {
|
|
75
|
+
await (0, api_client_1.sessionHeartbeat)(input.session_id, lastMessage);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
// Non-critical — don't block on failure
|
|
80
|
+
}
|
|
81
|
+
}
|
|
26
82
|
// Check for queued instructions from the dashboard
|
|
27
83
|
try {
|
|
28
84
|
const result = await (0, api_client_1.getInstructions)(config.projectId);
|
package/dist/mcp-server/index.js
CHANGED
|
@@ -57,6 +57,15 @@ server.tool("wait", "Wait for a pending Askance approval to be decided. Use this
|
|
|
57
57
|
}],
|
|
58
58
|
};
|
|
59
59
|
}
|
|
60
|
+
if (approvalStatus === "answered") {
|
|
61
|
+
const response = approval.response ?? "";
|
|
62
|
+
return {
|
|
63
|
+
content: [{
|
|
64
|
+
type: "text",
|
|
65
|
+
text: `OPERATOR RESPONSE for ${approval_id}:\n\n${response}\n\nThe operator has answered your question. Use this response to continue your work.`,
|
|
66
|
+
}],
|
|
67
|
+
};
|
|
68
|
+
}
|
|
60
69
|
return {
|
|
61
70
|
content: [{
|
|
62
71
|
type: "text",
|