@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: input.tool_name ?? "",
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: `PENDING APPROVAL (${response.approval_id}): ${response.rule}. ` +
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);
@@ -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",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@askance/cli",
3
- "version": "0.2.4",
3
+ "version": "0.3.0",
4
4
  "description": "Askance CLI — Tool call interception & approval management for AI coding agents",
5
5
  "license": "MIT",
6
6
  "homepage": "https://askance.app",