@askance/cli 0.1.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/LICENSE +21 -0
- package/README.md +79 -0
- package/dist/cli/index.js +515 -0
- package/dist/common/api-client.js +186 -0
- package/dist/hook-handler/index.js +143 -0
- package/dist/hook-handler/stop-hook.js +45 -0
- package/dist/mcp-server/index.js +236 -0
- package/dist/templates/ci-safe.yml +22 -0
- package/dist/templates/conservative.yml +30 -0
- package/dist/templates/copilot-config.json +11 -0
- package/dist/templates/cursor-config.json +18 -0
- package/dist/templates/moderate.yml +34 -0
- package/dist/templates/permissive.yml +18 -0
- package/package.json +51 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Advancer Limited
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# @askance/cli
|
|
2
|
+
|
|
3
|
+
Tool call interception & approval management for AI coding agents.
|
|
4
|
+
|
|
5
|
+
Askance hooks into your coding agent (Claude Code, Cursor, GitHub Copilot) and routes tool calls through policy rules and a cloud dashboard for approval — so agents can work while you review from any device.
|
|
6
|
+
|
|
7
|
+
## Quick Start
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install --save-dev @askance/cli
|
|
11
|
+
npx askance init
|
|
12
|
+
npx askance login
|
|
13
|
+
# Restart your agent session
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## What it does
|
|
17
|
+
|
|
18
|
+
- **Hook handler** — intercepts tool calls before execution and evaluates them against your `.askance.yml` policy rules
|
|
19
|
+
- **MCP server** — provides tools for the agent to wait for approvals and check for instructions
|
|
20
|
+
- **CLI** — sets up config files and authenticates with the Askance cloud
|
|
21
|
+
|
|
22
|
+
## Commands
|
|
23
|
+
|
|
24
|
+
| Command | Description |
|
|
25
|
+
|---------|-------------|
|
|
26
|
+
| `npx askance init` | Set up hooks, MCP server, policy rules, and CLAUDE.md |
|
|
27
|
+
| `npx askance login` | Authenticate via browser (GitHub, Google, or Microsoft) |
|
|
28
|
+
| `npx askance template <name>` | Apply a policy template (`conservative`, `moderate`, `permissive`, `ci-safe`) |
|
|
29
|
+
| `npx askance help` | Show usage information |
|
|
30
|
+
|
|
31
|
+
### Init options
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npx askance init # Claude Code (default)
|
|
35
|
+
npx askance init --cursor # Cursor IDE
|
|
36
|
+
npx askance init --copilot # GitHub Copilot
|
|
37
|
+
npx askance init --all # All agents
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Policy rules
|
|
41
|
+
|
|
42
|
+
Policy rules in `.askance.yml` control what happens when the agent uses a tool:
|
|
43
|
+
|
|
44
|
+
- **allow** — tool call proceeds immediately
|
|
45
|
+
- **gate** — queued for approval in the dashboard
|
|
46
|
+
- **deny** — blocked automatically
|
|
47
|
+
|
|
48
|
+
```yaml
|
|
49
|
+
rules:
|
|
50
|
+
- name: "Allow read-only tools"
|
|
51
|
+
match:
|
|
52
|
+
tool: "^(Read|Glob|Grep|WebSearch)$"
|
|
53
|
+
action: allow
|
|
54
|
+
|
|
55
|
+
- name: "Gate file writes"
|
|
56
|
+
match:
|
|
57
|
+
tool: "^(Edit|Write)$"
|
|
58
|
+
action: gate
|
|
59
|
+
risk: medium
|
|
60
|
+
|
|
61
|
+
- name: "Deny destructive commands"
|
|
62
|
+
match:
|
|
63
|
+
tool: "^Bash$"
|
|
64
|
+
command: "(rm -rf|chmod 777)"
|
|
65
|
+
action: deny
|
|
66
|
+
risk: high
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Dashboard
|
|
70
|
+
|
|
71
|
+
Manage approvals at [app.askance.app](https://app.askance.app) — approve, deny, or send instructions to your agent from any device.
|
|
72
|
+
|
|
73
|
+
## Documentation
|
|
74
|
+
|
|
75
|
+
Full docs at [askance.app/docs](https://askance.app/docs)
|
|
76
|
+
|
|
77
|
+
## License
|
|
78
|
+
|
|
79
|
+
MIT
|
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
4
|
+
if (k2 === undefined) k2 = k;
|
|
5
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
6
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
7
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
8
|
+
}
|
|
9
|
+
Object.defineProperty(o, k2, desc);
|
|
10
|
+
}) : (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
o[k2] = m[k];
|
|
13
|
+
}));
|
|
14
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
15
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
16
|
+
}) : function(o, v) {
|
|
17
|
+
o["default"] = v;
|
|
18
|
+
});
|
|
19
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
20
|
+
var ownKeys = function(o) {
|
|
21
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
22
|
+
var ar = [];
|
|
23
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
24
|
+
return ar;
|
|
25
|
+
};
|
|
26
|
+
return ownKeys(o);
|
|
27
|
+
};
|
|
28
|
+
return function (mod) {
|
|
29
|
+
if (mod && mod.__esModule) return mod;
|
|
30
|
+
var result = {};
|
|
31
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
32
|
+
__setModuleDefault(result, mod);
|
|
33
|
+
return result;
|
|
34
|
+
};
|
|
35
|
+
})();
|
|
36
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
37
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
38
|
+
};
|
|
39
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
40
|
+
const fs_1 = __importDefault(require("fs"));
|
|
41
|
+
const path_1 = __importDefault(require("path"));
|
|
42
|
+
const api_client_1 = require("../common/api-client");
|
|
43
|
+
const COMMANDS = ["init", "login", "template", "help"];
|
|
44
|
+
const command = (process.argv[2] ?? "help");
|
|
45
|
+
const projectDir = process.cwd();
|
|
46
|
+
// Parse flags from argv (everything after the command)
|
|
47
|
+
const flags = new Set(process.argv.slice(3).filter((a) => a.startsWith("--")));
|
|
48
|
+
const flagCursor = flags.has("--cursor");
|
|
49
|
+
const flagCopilot = flags.has("--copilot");
|
|
50
|
+
const flagAll = flags.has("--all");
|
|
51
|
+
// Resolve the package root (where dist/ lives)
|
|
52
|
+
const pkgRoot = path_1.default.resolve(__dirname, "..", "..");
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
// Shared helpers
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
function ensurePolicy() {
|
|
57
|
+
const policyPath = path_1.default.join(projectDir, ".askance.yml");
|
|
58
|
+
if (!fs_1.default.existsSync(policyPath)) {
|
|
59
|
+
const defaultPolicy = `version: 1
|
|
60
|
+
|
|
61
|
+
api_url: https://api.askance.app
|
|
62
|
+
project_id: ""
|
|
63
|
+
|
|
64
|
+
keep_alive:
|
|
65
|
+
enabled: true
|
|
66
|
+
duration: 3600 # total seconds to stay alive polling (default: 1 hour)
|
|
67
|
+
poll_interval: 30 # seconds per poll cycle (default: 30s)
|
|
68
|
+
|
|
69
|
+
rules:
|
|
70
|
+
- name: "Allow read-only tools"
|
|
71
|
+
match:
|
|
72
|
+
tool: "^(Read|Glob|Grep|WebSearch|WebFetch)$"
|
|
73
|
+
action: allow
|
|
74
|
+
|
|
75
|
+
- name: "Allow safe bash commands"
|
|
76
|
+
match:
|
|
77
|
+
tool: "^Bash$"
|
|
78
|
+
command: "^(npm test|npm run lint|npm run build|git status|git diff|git log|ls)"
|
|
79
|
+
action: allow
|
|
80
|
+
|
|
81
|
+
- name: "Gate file writes"
|
|
82
|
+
match:
|
|
83
|
+
tool: "^(Edit|Write|NotebookEdit)$"
|
|
84
|
+
action: gate
|
|
85
|
+
risk: medium
|
|
86
|
+
|
|
87
|
+
- name: "Gate package installs"
|
|
88
|
+
match:
|
|
89
|
+
tool: "^Bash$"
|
|
90
|
+
command: "(npm install|pip install|dotnet add)"
|
|
91
|
+
action: gate
|
|
92
|
+
risk: medium
|
|
93
|
+
|
|
94
|
+
- name: "Deny destructive commands"
|
|
95
|
+
match:
|
|
96
|
+
tool: "^Bash$"
|
|
97
|
+
command: "(rm -rf|chmod 777|curl.*\\\\|.*bash)"
|
|
98
|
+
action: deny
|
|
99
|
+
risk: high
|
|
100
|
+
|
|
101
|
+
- name: "Default - gate everything else"
|
|
102
|
+
match:
|
|
103
|
+
tool: ".*"
|
|
104
|
+
action: gate
|
|
105
|
+
risk: low
|
|
106
|
+
`;
|
|
107
|
+
fs_1.default.writeFileSync(policyPath, defaultPolicy);
|
|
108
|
+
console.log("[askance] Created .askance.yml");
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
console.log("[askance] .askance.yml already exists, skipping");
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function ensureGitignore() {
|
|
115
|
+
const gitignorePath = path_1.default.join(projectDir, ".gitignore");
|
|
116
|
+
if (fs_1.default.existsSync(gitignorePath)) {
|
|
117
|
+
const content = fs_1.default.readFileSync(gitignorePath, "utf-8");
|
|
118
|
+
if (!content.includes(".askance/")) {
|
|
119
|
+
fs_1.default.appendFileSync(gitignorePath, "\n# Askance session data\n.askance/\n");
|
|
120
|
+
console.log("[askance] Added .askance/ to .gitignore");
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
fs_1.default.writeFileSync(gitignorePath, "# Askance session data\n.askance/\n");
|
|
125
|
+
console.log("[askance] Created .gitignore with .askance/");
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
// Claude Code init (default)
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
function initClaude() {
|
|
132
|
+
const claudeDir = path_1.default.join(projectDir, ".claude");
|
|
133
|
+
if (!fs_1.default.existsSync(claudeDir)) {
|
|
134
|
+
fs_1.default.mkdirSync(claudeDir, { recursive: true });
|
|
135
|
+
}
|
|
136
|
+
const settingsPath = path_1.default.join(claudeDir, "settings.json");
|
|
137
|
+
const hookHandlerPath = path_1.default.join(pkgRoot, "dist", "hook-handler", "index.js").replace(/\\/g, "/");
|
|
138
|
+
const stopHookPath = path_1.default.join(pkgRoot, "dist", "hook-handler", "stop-hook.js").replace(/\\/g, "/");
|
|
139
|
+
const mcpServerPath = path_1.default.join(pkgRoot, "dist", "mcp-server", "index.js").replace(/\\/g, "/");
|
|
140
|
+
const settings = fs_1.default.existsSync(settingsPath)
|
|
141
|
+
? JSON.parse(fs_1.default.readFileSync(settingsPath, "utf-8"))
|
|
142
|
+
: {};
|
|
143
|
+
// Set hooks
|
|
144
|
+
const hooks = (settings.hooks ?? {});
|
|
145
|
+
hooks.PreToolUse = [
|
|
146
|
+
{
|
|
147
|
+
matcher: "^(?!mcp__askance__).*",
|
|
148
|
+
hooks: [
|
|
149
|
+
{
|
|
150
|
+
type: "command",
|
|
151
|
+
command: `node "${hookHandlerPath}"`,
|
|
152
|
+
timeout: 120,
|
|
153
|
+
},
|
|
154
|
+
],
|
|
155
|
+
},
|
|
156
|
+
];
|
|
157
|
+
hooks.Stop = [
|
|
158
|
+
{
|
|
159
|
+
matcher: "",
|
|
160
|
+
hooks: [
|
|
161
|
+
{
|
|
162
|
+
type: "command",
|
|
163
|
+
command: `node "${stopHookPath}"`,
|
|
164
|
+
timeout: 10,
|
|
165
|
+
},
|
|
166
|
+
],
|
|
167
|
+
},
|
|
168
|
+
];
|
|
169
|
+
settings.hooks = hooks;
|
|
170
|
+
// Set MCP server
|
|
171
|
+
const mcpServers = (settings.mcpServers ?? {});
|
|
172
|
+
mcpServers.askance = {
|
|
173
|
+
command: "node",
|
|
174
|
+
args: [mcpServerPath],
|
|
175
|
+
};
|
|
176
|
+
settings.mcpServers = mcpServers;
|
|
177
|
+
fs_1.default.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
178
|
+
console.log("[askance] Updated .claude/settings.json");
|
|
179
|
+
}
|
|
180
|
+
// ---------------------------------------------------------------------------
|
|
181
|
+
// Cursor IDE init
|
|
182
|
+
// ---------------------------------------------------------------------------
|
|
183
|
+
function initCursor() {
|
|
184
|
+
const cursorDir = path_1.default.join(projectDir, ".cursor");
|
|
185
|
+
if (!fs_1.default.existsSync(cursorDir)) {
|
|
186
|
+
fs_1.default.mkdirSync(cursorDir, { recursive: true });
|
|
187
|
+
}
|
|
188
|
+
const settingsPath = path_1.default.join(cursorDir, "settings.json");
|
|
189
|
+
const hookHandlerPath = path_1.default.join(pkgRoot, "dist", "hook-handler", "index.js").replace(/\\/g, "/");
|
|
190
|
+
const mcpServerPath = path_1.default.join(pkgRoot, "dist", "mcp-server", "index.js").replace(/\\/g, "/");
|
|
191
|
+
const settings = fs_1.default.existsSync(settingsPath)
|
|
192
|
+
? JSON.parse(fs_1.default.readFileSync(settingsPath, "utf-8"))
|
|
193
|
+
: {};
|
|
194
|
+
// Set Cursor hooks
|
|
195
|
+
const cursorHooks = (settings["cursor.hooks"] ?? {});
|
|
196
|
+
cursorHooks.preToolUse = {
|
|
197
|
+
command: "node",
|
|
198
|
+
args: [hookHandlerPath],
|
|
199
|
+
matchTools: [".*"],
|
|
200
|
+
excludeTools: ["mcp__askance__.*"],
|
|
201
|
+
};
|
|
202
|
+
settings["cursor.hooks"] = cursorHooks;
|
|
203
|
+
// Set Cursor MCP servers
|
|
204
|
+
const cursorMcp = (settings["cursor.mcp"] ?? {});
|
|
205
|
+
const servers = (cursorMcp.servers ?? {});
|
|
206
|
+
servers.askance = {
|
|
207
|
+
command: "node",
|
|
208
|
+
args: [mcpServerPath],
|
|
209
|
+
};
|
|
210
|
+
cursorMcp.servers = servers;
|
|
211
|
+
settings["cursor.mcp"] = cursorMcp;
|
|
212
|
+
fs_1.default.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
213
|
+
console.log("[askance] Updated .cursor/settings.json");
|
|
214
|
+
}
|
|
215
|
+
// ---------------------------------------------------------------------------
|
|
216
|
+
// GitHub Copilot init
|
|
217
|
+
// ---------------------------------------------------------------------------
|
|
218
|
+
function initCopilot() {
|
|
219
|
+
const githubDir = path_1.default.join(projectDir, ".github");
|
|
220
|
+
if (!fs_1.default.existsSync(githubDir)) {
|
|
221
|
+
fs_1.default.mkdirSync(githubDir, { recursive: true });
|
|
222
|
+
}
|
|
223
|
+
const configPath = path_1.default.join(githubDir, "copilot-config.json");
|
|
224
|
+
const hookHandlerPath = path_1.default.join(pkgRoot, "dist", "hook-handler", "index.js").replace(/\\/g, "/");
|
|
225
|
+
const config = fs_1.default.existsSync(configPath)
|
|
226
|
+
? JSON.parse(fs_1.default.readFileSync(configPath, "utf-8"))
|
|
227
|
+
: {};
|
|
228
|
+
// Set Copilot agent hooks
|
|
229
|
+
const copilotAgent = (config["copilot.agent"] ?? {});
|
|
230
|
+
const hooks = (copilotAgent.hooks ?? {});
|
|
231
|
+
hooks.preToolUse = {
|
|
232
|
+
command: "node",
|
|
233
|
+
args: [hookHandlerPath],
|
|
234
|
+
matchTools: [".*"],
|
|
235
|
+
};
|
|
236
|
+
copilotAgent.hooks = hooks;
|
|
237
|
+
config["copilot.agent"] = copilotAgent;
|
|
238
|
+
fs_1.default.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
239
|
+
console.log("[askance] Updated .github/copilot-config.json");
|
|
240
|
+
}
|
|
241
|
+
// ---------------------------------------------------------------------------
|
|
242
|
+
// Main init orchestrator
|
|
243
|
+
// ---------------------------------------------------------------------------
|
|
244
|
+
const CLAUDE_MD_SECTION = `
|
|
245
|
+
## Askance — Tool Call Oversight
|
|
246
|
+
|
|
247
|
+
This project uses [Askance](https://askance.app) for AI tool call interception and approval management.
|
|
248
|
+
|
|
249
|
+
**How it works:**
|
|
250
|
+
- All tool calls are intercepted by Askance hooks and evaluated against policy rules in \`.askance.yml\`
|
|
251
|
+
- Safe operations (reads, searches) are auto-approved; risky operations require human approval
|
|
252
|
+
- When a tool call is gated for approval, use \`mcp__askance__wait\` to wait for the decision
|
|
253
|
+
- Use \`mcp__askance__check_instructions\` periodically to check for operator instructions
|
|
254
|
+
|
|
255
|
+
**Important:**
|
|
256
|
+
- If a tool call returns \`pending\` status with an \`approval_id\`, call \`mcp__askance__wait\` with that ID before retrying
|
|
257
|
+
- Do NOT bypass or work around gated tool calls — they require human approval
|
|
258
|
+
- The operator may send instructions via the dashboard; check for them using the MCP tools
|
|
259
|
+
`;
|
|
260
|
+
function ensureClaudeMd() {
|
|
261
|
+
const claudeMdPath = path_1.default.join(projectDir, "CLAUDE.md");
|
|
262
|
+
if (fs_1.default.existsSync(claudeMdPath)) {
|
|
263
|
+
const content = fs_1.default.readFileSync(claudeMdPath, "utf-8");
|
|
264
|
+
if (content.includes("Askance")) {
|
|
265
|
+
console.log("[askance] CLAUDE.md already contains Askance section, skipping");
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
fs_1.default.appendFileSync(claudeMdPath, "\n" + CLAUDE_MD_SECTION);
|
|
269
|
+
console.log("[askance] Added Askance section to existing CLAUDE.md");
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
fs_1.default.writeFileSync(claudeMdPath, CLAUDE_MD_SECTION.trimStart());
|
|
273
|
+
console.log("[askance] Created CLAUDE.md with Askance instructions");
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
function init() {
|
|
277
|
+
console.log("[askance] Initializing Askance in", projectDir);
|
|
278
|
+
// 1. Always write .askance.yml + .gitignore
|
|
279
|
+
ensurePolicy();
|
|
280
|
+
ensureGitignore();
|
|
281
|
+
// 2. Determine which IDE configs to set up
|
|
282
|
+
const setupClaude = flagAll || (!flagCursor && !flagCopilot);
|
|
283
|
+
const setupCursor = flagAll || flagCursor;
|
|
284
|
+
const setupCopilot = flagAll || flagCopilot;
|
|
285
|
+
if (setupClaude) {
|
|
286
|
+
initClaude();
|
|
287
|
+
ensureClaudeMd();
|
|
288
|
+
}
|
|
289
|
+
if (setupCursor) {
|
|
290
|
+
initCursor();
|
|
291
|
+
}
|
|
292
|
+
if (setupCopilot) {
|
|
293
|
+
initCopilot();
|
|
294
|
+
}
|
|
295
|
+
// 3. Print next steps
|
|
296
|
+
console.log("\n[askance] Setup complete! Next steps:");
|
|
297
|
+
console.log(" 1. Log in: npx askance login");
|
|
298
|
+
if (setupClaude) {
|
|
299
|
+
console.log(" 2. Restart your Claude Code session (hooks load at startup)");
|
|
300
|
+
}
|
|
301
|
+
if (setupCursor) {
|
|
302
|
+
console.log(" 2. Restart Cursor to load the hooks and MCP server");
|
|
303
|
+
}
|
|
304
|
+
if (setupCopilot) {
|
|
305
|
+
console.log(" 2. Copilot agent hooks are configured in .github/copilot-config.json");
|
|
306
|
+
}
|
|
307
|
+
console.log(" 3. Open the dashboard: https://app.askance.app");
|
|
308
|
+
console.log(" 4. Edit .askance.yml to customize your policy rules");
|
|
309
|
+
}
|
|
310
|
+
// ---------------------------------------------------------------------------
|
|
311
|
+
// Login — opens browser for auth, saves JWT to .askance/credentials
|
|
312
|
+
// ---------------------------------------------------------------------------
|
|
313
|
+
async function login() {
|
|
314
|
+
const config = (0, api_client_1.readConfig)();
|
|
315
|
+
const apiUrl = config.apiUrl;
|
|
316
|
+
// Check if already logged in
|
|
317
|
+
if (config.token) {
|
|
318
|
+
console.log("[askance] Already logged in.");
|
|
319
|
+
console.log(` API: ${apiUrl}`);
|
|
320
|
+
// Try to read email from credentials
|
|
321
|
+
const credPath = path_1.default.join(projectDir, ".askance", "credentials");
|
|
322
|
+
if (fs_1.default.existsSync(credPath)) {
|
|
323
|
+
try {
|
|
324
|
+
const creds = JSON.parse(fs_1.default.readFileSync(credPath, "utf-8"));
|
|
325
|
+
if (creds.user_email) {
|
|
326
|
+
console.log(` User: ${creds.user_email}`);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
catch { }
|
|
330
|
+
}
|
|
331
|
+
console.log("\n To log in as a different user, delete .askance/credentials and run login again.");
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
// Generate a device code / CLI login token
|
|
335
|
+
console.log("[askance] Logging in to Askance...");
|
|
336
|
+
console.log(` API: ${apiUrl}`);
|
|
337
|
+
// Request a CLI login session from the backend
|
|
338
|
+
const { apiPost, apiGet } = await Promise.resolve().then(() => __importStar(require("../common/api-client")));
|
|
339
|
+
const startResult = await apiPost("/api/auth/cli/start");
|
|
340
|
+
if (!startResult.ok || !startResult.data) {
|
|
341
|
+
console.error("[askance] Failed to start login. Is the API running?");
|
|
342
|
+
console.error(` URL: ${apiUrl}`);
|
|
343
|
+
if (startResult.error)
|
|
344
|
+
console.error(` Error: ${startResult.error}`);
|
|
345
|
+
console.log("\n You can also set ASKANCE_TOKEN environment variable directly.");
|
|
346
|
+
process.exit(1);
|
|
347
|
+
}
|
|
348
|
+
const { loginId, loginUrl } = startResult.data;
|
|
349
|
+
// Open browser
|
|
350
|
+
console.log("\n Opening browser for authentication...");
|
|
351
|
+
console.log(` If the browser doesn't open, visit: ${loginUrl}`);
|
|
352
|
+
try {
|
|
353
|
+
const { exec } = await Promise.resolve().then(() => __importStar(require("child_process")));
|
|
354
|
+
const openCmd = process.platform === "win32" ? `start "" "${loginUrl}"` :
|
|
355
|
+
process.platform === "darwin" ? `open "${loginUrl}"` :
|
|
356
|
+
`xdg-open "${loginUrl}"`;
|
|
357
|
+
exec(openCmd);
|
|
358
|
+
}
|
|
359
|
+
catch {
|
|
360
|
+
// Browser didn't open — user can manually visit the URL
|
|
361
|
+
}
|
|
362
|
+
// Poll for completion
|
|
363
|
+
console.log("\n Waiting for authentication...");
|
|
364
|
+
const maxWait = 300_000; // 5 minutes
|
|
365
|
+
const pollInterval = 3_000;
|
|
366
|
+
const startTime = Date.now();
|
|
367
|
+
while (Date.now() - startTime < maxWait) {
|
|
368
|
+
await new Promise((r) => setTimeout(r, pollInterval));
|
|
369
|
+
const pollResult = await apiGet(`/api/auth/cli/poll/${loginId}`);
|
|
370
|
+
if (pollResult.ok && pollResult.data) {
|
|
371
|
+
if (pollResult.data.status === "completed" && pollResult.data.token) {
|
|
372
|
+
// Save credentials
|
|
373
|
+
const askanceDir = path_1.default.join(projectDir, ".askance");
|
|
374
|
+
if (!fs_1.default.existsSync(askanceDir)) {
|
|
375
|
+
fs_1.default.mkdirSync(askanceDir, { recursive: true });
|
|
376
|
+
}
|
|
377
|
+
const credentials = {
|
|
378
|
+
token: pollResult.data.token,
|
|
379
|
+
refresh_token: pollResult.data.refreshToken ?? "",
|
|
380
|
+
api_url: apiUrl,
|
|
381
|
+
user_email: pollResult.data.email ?? "",
|
|
382
|
+
project_id: pollResult.data.projectId ?? "",
|
|
383
|
+
};
|
|
384
|
+
fs_1.default.writeFileSync(path_1.default.join(askanceDir, "credentials"), JSON.stringify(credentials, null, 2) + "\n", { mode: 0o600 });
|
|
385
|
+
// Update .askance.yml project_id if we got one
|
|
386
|
+
if (pollResult.data.projectId) {
|
|
387
|
+
const policyPath = path_1.default.join(projectDir, ".askance.yml");
|
|
388
|
+
if (fs_1.default.existsSync(policyPath)) {
|
|
389
|
+
let content = fs_1.default.readFileSync(policyPath, "utf-8");
|
|
390
|
+
content = content.replace(/^project_id:\s*"?"?.*"?"?\s*$/m, `project_id: "${pollResult.data.projectId}"`);
|
|
391
|
+
fs_1.default.writeFileSync(policyPath, content);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
(0, api_client_1.clearConfigCache)();
|
|
395
|
+
console.log("\n[askance] Login successful!");
|
|
396
|
+
if (pollResult.data.email) {
|
|
397
|
+
console.log(` Logged in as: ${pollResult.data.email}`);
|
|
398
|
+
}
|
|
399
|
+
console.log(" Credentials saved to .askance/credentials");
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
if (pollResult.data.status === "expired") {
|
|
403
|
+
console.error("\n[askance] Login session expired. Please try again.");
|
|
404
|
+
process.exit(1);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
console.error("\n[askance] Login timed out. Please try again.");
|
|
409
|
+
process.exit(1);
|
|
410
|
+
}
|
|
411
|
+
function template() {
|
|
412
|
+
const TEMPLATES = ["conservative", "moderate", "permissive", "ci-safe"];
|
|
413
|
+
const templateName = process.argv[3];
|
|
414
|
+
if (!templateName) {
|
|
415
|
+
console.log(`
|
|
416
|
+
[askance] Policy Template Packs
|
|
417
|
+
|
|
418
|
+
Available templates:
|
|
419
|
+
conservative Gate writes and bash, deny destructive commands
|
|
420
|
+
moderate Allow safe writes and bash, gate the rest
|
|
421
|
+
permissive Allow everything, deny only destructive commands
|
|
422
|
+
ci-safe Minimal permissions for CI/CD environments
|
|
423
|
+
|
|
424
|
+
Usage:
|
|
425
|
+
npx askance template <name>
|
|
426
|
+
|
|
427
|
+
Example:
|
|
428
|
+
npx askance template moderate
|
|
429
|
+
`);
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
if (!TEMPLATES.includes(templateName)) {
|
|
433
|
+
console.error(`[askance] Unknown template: "${templateName}"`);
|
|
434
|
+
console.error(`[askance] Available templates: ${TEMPLATES.join(", ")}`);
|
|
435
|
+
process.exit(1);
|
|
436
|
+
}
|
|
437
|
+
// Templates are bundled alongside the compiled output
|
|
438
|
+
// In source: src/templates/<name>.yml
|
|
439
|
+
// In dist: dist/templates/<name>.yml (copied by build)
|
|
440
|
+
// Also try source path for development
|
|
441
|
+
const distTemplatePath = path_1.default.join(pkgRoot, "dist", "templates", `${templateName}.yml`);
|
|
442
|
+
const srcTemplatePath = path_1.default.join(pkgRoot, "src", "templates", `${templateName}.yml`);
|
|
443
|
+
const templatePath = fs_1.default.existsSync(distTemplatePath) ? distTemplatePath : srcTemplatePath;
|
|
444
|
+
if (!fs_1.default.existsSync(templatePath)) {
|
|
445
|
+
console.error(`[askance] Template file not found at ${templatePath}`);
|
|
446
|
+
console.error("[askance] Try rebuilding: npm run build");
|
|
447
|
+
process.exit(1);
|
|
448
|
+
}
|
|
449
|
+
const destPath = path_1.default.join(projectDir, ".askance.yml");
|
|
450
|
+
const templateContent = fs_1.default.readFileSync(templatePath, "utf-8");
|
|
451
|
+
if (fs_1.default.existsSync(destPath)) {
|
|
452
|
+
console.log(`[askance] Overwriting existing .askance.yml with "${templateName}" template`);
|
|
453
|
+
}
|
|
454
|
+
fs_1.default.writeFileSync(destPath, templateContent);
|
|
455
|
+
console.log(`[askance] Applied "${templateName}" template to .askance.yml`);
|
|
456
|
+
console.log("[askance] Restart your agent session to apply the new policy");
|
|
457
|
+
}
|
|
458
|
+
function help() {
|
|
459
|
+
console.log(`
|
|
460
|
+
Askance — Tool Call Interception & Exception Management for AI Coding Agents
|
|
461
|
+
|
|
462
|
+
Usage:
|
|
463
|
+
npx askance <command> [options]
|
|
464
|
+
|
|
465
|
+
Commands:
|
|
466
|
+
init Set up Askance in the current project
|
|
467
|
+
- Creates .askance.yml (policy rules)
|
|
468
|
+
- Configures agent hooks and MCP servers
|
|
469
|
+
|
|
470
|
+
Options:
|
|
471
|
+
(no flag) Configure for Claude Code (default)
|
|
472
|
+
--cursor Configure for Cursor IDE (.cursor/settings.json)
|
|
473
|
+
--copilot Configure for GitHub Copilot (.github/copilot-config.json)
|
|
474
|
+
--all Configure for all supported agents
|
|
475
|
+
|
|
476
|
+
login Authenticate with Askance
|
|
477
|
+
- Opens browser for OAuth sign-in
|
|
478
|
+
- Saves access token to .askance/credentials
|
|
479
|
+
|
|
480
|
+
template Apply a pre-built policy template
|
|
481
|
+
- Available: conservative, moderate, permissive, ci-safe
|
|
482
|
+
- Usage: npx askance template <name>
|
|
483
|
+
|
|
484
|
+
help Show this help message
|
|
485
|
+
|
|
486
|
+
Quick Start:
|
|
487
|
+
cd /path/to/your-project
|
|
488
|
+
npx askance init # Claude Code (default)
|
|
489
|
+
npx askance init --cursor # Cursor IDE
|
|
490
|
+
npx askance init --copilot # GitHub Copilot
|
|
491
|
+
npx askance init --all # All agents
|
|
492
|
+
npx askance login # Authenticate
|
|
493
|
+
# Restart your agent session
|
|
494
|
+
# Open https://app.askance.app
|
|
495
|
+
|
|
496
|
+
Dashboard: https://app.askance.app
|
|
497
|
+
Docs: https://askance.app
|
|
498
|
+
`);
|
|
499
|
+
}
|
|
500
|
+
switch (command) {
|
|
501
|
+
case "init":
|
|
502
|
+
init();
|
|
503
|
+
break;
|
|
504
|
+
case "login":
|
|
505
|
+
login();
|
|
506
|
+
break;
|
|
507
|
+
case "template":
|
|
508
|
+
template();
|
|
509
|
+
break;
|
|
510
|
+
case "help":
|
|
511
|
+
default:
|
|
512
|
+
help();
|
|
513
|
+
break;
|
|
514
|
+
}
|
|
515
|
+
//# sourceMappingURL=index.js.map
|