@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 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