@askance/cli 0.1.0 → 0.2.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 CHANGED
@@ -1,21 +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.
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 CHANGED
@@ -1,79 +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
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
package/dist/cli/index.js CHANGED
@@ -39,6 +39,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
39
39
  Object.defineProperty(exports, "__esModule", { value: true });
40
40
  const fs_1 = __importDefault(require("fs"));
41
41
  const path_1 = __importDefault(require("path"));
42
+ const child_process_1 = require("child_process");
43
+ const readline_1 = __importDefault(require("readline"));
42
44
  const api_client_1 = require("../common/api-client");
43
45
  const COMMANDS = ["init", "login", "template", "help"];
44
46
  const command = (process.argv[2] ?? "help");
@@ -53,76 +55,42 @@ const pkgRoot = path_1.default.resolve(__dirname, "..", "..");
53
55
  // ---------------------------------------------------------------------------
54
56
  // Shared helpers
55
57
  // ---------------------------------------------------------------------------
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");
58
+ function ensureConfig() {
59
+ const askanceDir = path_1.default.join(projectDir, ".askance");
60
+ if (!fs_1.default.existsSync(askanceDir)) {
61
+ fs_1.default.mkdirSync(askanceDir, { recursive: true });
62
+ }
63
+ const configPath = path_1.default.join(askanceDir, "config.json");
64
+ if (!fs_1.default.existsSync(configPath)) {
65
+ const defaultConfig = {
66
+ apiUrl: "https://api.askance.app",
67
+ projectId: "",
68
+ keepAlive: {
69
+ enabled: true,
70
+ duration: 3600,
71
+ pollInterval: 30,
72
+ },
73
+ };
74
+ fs_1.default.writeFileSync(configPath, JSON.stringify(defaultConfig, null, 2) + "\n");
75
+ console.log("[askance] Created .askance/config.json");
109
76
  }
110
77
  else {
111
- console.log("[askance] .askance.yml already exists, skipping");
78
+ console.log("[askance] .askance/config.json already exists, skipping");
112
79
  }
113
80
  }
114
81
  function ensureGitignore() {
115
82
  const gitignorePath = path_1.default.join(projectDir, ".gitignore");
83
+ const ignoreEntry = ".askance/credentials";
116
84
  if (fs_1.default.existsSync(gitignorePath)) {
117
85
  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");
86
+ if (!content.includes(ignoreEntry)) {
87
+ fs_1.default.appendFileSync(gitignorePath, "\n# Askance credentials (do not commit)\n.askance/credentials\n");
88
+ console.log("[askance] Added .askance/credentials to .gitignore");
121
89
  }
122
90
  }
123
91
  else {
124
- fs_1.default.writeFileSync(gitignorePath, "# Askance session data\n.askance/\n");
125
- console.log("[askance] Created .gitignore with .askance/");
92
+ fs_1.default.writeFileSync(gitignorePath, "# Askance credentials (do not commit)\n.askance/credentials\n");
93
+ console.log("[askance] Created .gitignore with .askance/credentials");
126
94
  }
127
95
  }
128
96
  // ---------------------------------------------------------------------------
@@ -247,7 +215,7 @@ const CLAUDE_MD_SECTION = `
247
215
  This project uses [Askance](https://askance.app) for AI tool call interception and approval management.
248
216
 
249
217
  **How it works:**
250
- - All tool calls are intercepted by Askance hooks and evaluated against policy rules in \`.askance.yml\`
218
+ - All tool calls are intercepted by Askance hooks and evaluated against policy rules managed via the dashboard
251
219
  - Safe operations (reads, searches) are auto-approved; risky operations require human approval
252
220
  - When a tool call is gated for approval, use \`mcp__askance__wait\` to wait for the decision
253
221
  - Use \`mcp__askance__check_instructions\` periodically to check for operator instructions
@@ -275,8 +243,8 @@ function ensureClaudeMd() {
275
243
  }
276
244
  function init() {
277
245
  console.log("[askance] Initializing Askance in", projectDir);
278
- // 1. Always write .askance.yml + .gitignore
279
- ensurePolicy();
246
+ // 1. Always create .askance/config.json + update .gitignore
247
+ ensureConfig();
280
248
  ensureGitignore();
281
249
  // 2. Determine which IDE configs to set up
282
250
  const setupClaude = flagAll || (!flagCursor && !flagCopilot);
@@ -305,7 +273,139 @@ function init() {
305
273
  console.log(" 2. Copilot agent hooks are configured in .github/copilot-config.json");
306
274
  }
307
275
  console.log(" 3. Open the dashboard: https://app.askance.app");
308
- console.log(" 4. Edit .askance.yml to customize your policy rules");
276
+ console.log(" 4. Manage policy rules via the dashboard");
277
+ }
278
+ // ---------------------------------------------------------------------------
279
+ // Project setup helpers
280
+ // ---------------------------------------------------------------------------
281
+ function detectGitInfo() {
282
+ let repoUrl = "";
283
+ let branch = "main";
284
+ let repoName = path_1.default.basename(projectDir);
285
+ try {
286
+ repoUrl = (0, child_process_1.execSync)("git remote get-url origin", {
287
+ cwd: projectDir,
288
+ encoding: "utf-8",
289
+ stdio: ["pipe", "pipe", "pipe"],
290
+ }).trim();
291
+ }
292
+ catch { }
293
+ try {
294
+ branch = (0, child_process_1.execSync)("git branch --show-current", {
295
+ cwd: projectDir,
296
+ encoding: "utf-8",
297
+ stdio: ["pipe", "pipe", "pipe"],
298
+ }).trim() || "main";
299
+ }
300
+ catch { }
301
+ // Extract repo name from URL (e.g. git@github.com:user/my-repo.git → my-repo)
302
+ if (repoUrl) {
303
+ const match = repoUrl.match(/\/([^/]+?)(?:\.git)?$/) ?? repoUrl.match(/:([^/]+?)(?:\.git)?$/);
304
+ if (match)
305
+ repoName = match[1];
306
+ }
307
+ return { repoUrl, branch, repoName };
308
+ }
309
+ function promptSelection(message, max) {
310
+ const rl = readline_1.default.createInterface({ input: process.stdin, output: process.stdout });
311
+ return new Promise((resolve) => {
312
+ rl.question(message, (answer) => {
313
+ rl.close();
314
+ const num = parseInt(answer.trim(), 10);
315
+ if (isNaN(num) || num < 1 || num > max) {
316
+ resolve(-1);
317
+ }
318
+ else {
319
+ resolve(num);
320
+ }
321
+ });
322
+ });
323
+ }
324
+ async function setupProject() {
325
+ console.log("\n[askance] Setting up project...");
326
+ // Fetch existing projects
327
+ const projectsResult = await (0, api_client_1.apiGet)("/api/projects");
328
+ if (!projectsResult.ok || !projectsResult.data) {
329
+ console.log(" Could not fetch projects. You can link a project later via the dashboard.");
330
+ return;
331
+ }
332
+ const projects = projectsResult.data;
333
+ let selectedId = "";
334
+ let selectedName = "";
335
+ if (projects.length === 1) {
336
+ // Auto-select the only project
337
+ selectedId = projects[0].id;
338
+ selectedName = projects[0].name;
339
+ console.log(` Found 1 existing project: ${selectedName}`);
340
+ console.log(` Using project: ${selectedName} (${selectedId})`);
341
+ }
342
+ else if (projects.length > 1) {
343
+ // Prompt the user to select
344
+ console.log(` Found ${projects.length} projects:`);
345
+ for (let i = 0; i < projects.length; i++) {
346
+ console.log(` ${i + 1}. ${projects[i].name}`);
347
+ }
348
+ const choice = await promptSelection(` Select a project (1-${projects.length}): `, projects.length);
349
+ if (choice < 1) {
350
+ console.log(" Invalid selection. You can link a project later via the dashboard.");
351
+ return;
352
+ }
353
+ selectedId = projects[choice - 1].id;
354
+ selectedName = projects[choice - 1].name;
355
+ console.log(` Using project: ${selectedName} (${selectedId})`);
356
+ }
357
+ else {
358
+ // No projects — auto-create one
359
+ const git = detectGitInfo();
360
+ console.log(` No projects found. Creating "${git.repoName}" from ${git.repoUrl ? "git remote" : "directory name"}...`);
361
+ const createResult = await (0, api_client_1.apiPost)("/api/projects", {
362
+ Name: git.repoName,
363
+ RepoUrl: git.repoUrl,
364
+ Branch: git.branch,
365
+ });
366
+ if (!createResult.ok) {
367
+ if (createResult.status === 402) {
368
+ console.log("\n Your plan allows 1 project and you've reached the limit.");
369
+ console.log(" Upgrade at https://app.askance.app/settings");
370
+ console.log(" You're still logged in — link a project manually or upgrade first.");
371
+ }
372
+ else {
373
+ console.log(` Failed to create project: ${createResult.error}`);
374
+ console.log(" You can create a project later via the dashboard.");
375
+ }
376
+ return;
377
+ }
378
+ if (!createResult.data) {
379
+ console.log(" Failed to create project. You can create one later via the dashboard.");
380
+ return;
381
+ }
382
+ selectedId = createResult.data.id;
383
+ selectedName = createResult.data.name;
384
+ console.log(` Created project "${selectedName}"`);
385
+ console.log(` Project ID: ${selectedId}`);
386
+ }
387
+ // Save project ID to .askance/config.json
388
+ const configPath = path_1.default.join(projectDir, ".askance", "config.json");
389
+ if (fs_1.default.existsSync(configPath)) {
390
+ try {
391
+ const cfg = JSON.parse(fs_1.default.readFileSync(configPath, "utf-8"));
392
+ cfg.projectId = selectedId;
393
+ fs_1.default.writeFileSync(configPath, JSON.stringify(cfg, null, 2) + "\n");
394
+ }
395
+ catch { }
396
+ }
397
+ // Also save to credentials for backward compatibility
398
+ const credPath = path_1.default.join(projectDir, ".askance", "credentials");
399
+ if (fs_1.default.existsSync(credPath)) {
400
+ try {
401
+ const creds = JSON.parse(fs_1.default.readFileSync(credPath, "utf-8"));
402
+ creds.project_id = selectedId;
403
+ fs_1.default.writeFileSync(credPath, JSON.stringify(creds, null, 2) + "\n", { mode: 0o600 });
404
+ }
405
+ catch { }
406
+ }
407
+ (0, api_client_1.clearConfigCache)();
408
+ console.log(" Project linked successfully.");
309
409
  }
310
410
  // ---------------------------------------------------------------------------
311
411
  // Login — opens browser for auth, saves JWT to .askance/credentials
@@ -382,13 +482,16 @@ async function login() {
382
482
  project_id: pollResult.data.projectId ?? "",
383
483
  };
384
484
  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
485
+ // Update .askance/config.json project_id if we got one
386
486
  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);
487
+ const configPath = path_1.default.join(projectDir, ".askance", "config.json");
488
+ if (fs_1.default.existsSync(configPath)) {
489
+ try {
490
+ const cfg = JSON.parse(fs_1.default.readFileSync(configPath, "utf-8"));
491
+ cfg.projectId = pollResult.data.projectId;
492
+ fs_1.default.writeFileSync(configPath, JSON.stringify(cfg, null, 2) + "\n");
493
+ }
494
+ catch { }
392
495
  }
393
496
  }
394
497
  (0, api_client_1.clearConfigCache)();
@@ -397,6 +500,10 @@ async function login() {
397
500
  console.log(` Logged in as: ${pollResult.data.email}`);
398
501
  }
399
502
  console.log(" Credentials saved to .askance/credentials");
503
+ // If the server didn't provide a project_id, run interactive setup
504
+ if (!pollResult.data.projectId) {
505
+ await setupProject();
506
+ }
400
507
  return;
401
508
  }
402
509
  if (pollResult.data.status === "expired") {
@@ -408,8 +515,35 @@ async function login() {
408
515
  console.error("\n[askance] Login timed out. Please try again.");
409
516
  process.exit(1);
410
517
  }
411
- function template() {
412
- const TEMPLATES = ["conservative", "moderate", "permissive", "ci-safe"];
518
+ const TEMPLATE_RULES = {
519
+ conservative: [
520
+ { Name: "Allow read-only tools", ToolPattern: "^(Read|Glob|Grep|WebSearch|WebFetch)$", Action: "Allow", Risk: "Low" },
521
+ { Name: "Gate file writes", ToolPattern: "^(Edit|Write|NotebookEdit)$", Action: "Gate", Risk: "Medium" },
522
+ { Name: "Gate all bash commands", ToolPattern: "^Bash$", Action: "Gate", Risk: "Medium" },
523
+ { Name: "Deny destructive commands", ToolPattern: "^Bash$", CommandPattern: "(rm -rf|chmod 777|curl.*\\\\|.*bash)", Action: "Deny", Risk: "High" },
524
+ { Name: "Default — gate everything else", ToolPattern: ".*", Action: "Gate", Risk: "Low" },
525
+ ],
526
+ moderate: [
527
+ { Name: "Allow read-only tools", ToolPattern: "^(Read|Glob|Grep|WebSearch|WebFetch)$", Action: "Allow", Risk: "Low" },
528
+ { Name: "Allow safe bash commands", ToolPattern: "^Bash$", CommandPattern: "^(npm test|npm run lint|npm run build|git status|git diff|git log|ls)", Action: "Allow", Risk: "Low" },
529
+ { Name: "Gate file writes", ToolPattern: "^(Edit|Write|NotebookEdit)$", Action: "Gate", Risk: "Medium" },
530
+ { Name: "Gate package installs", ToolPattern: "^Bash$", CommandPattern: "(npm install|pip install|dotnet add)", Action: "Gate", Risk: "Medium" },
531
+ { Name: "Deny destructive commands", ToolPattern: "^Bash$", CommandPattern: "(rm -rf|chmod 777|curl.*\\\\|.*bash)", Action: "Deny", Risk: "High" },
532
+ { Name: "Default — gate everything else", ToolPattern: ".*", Action: "Gate", Risk: "Low" },
533
+ ],
534
+ permissive: [
535
+ { Name: "Deny destructive commands", ToolPattern: "^Bash$", CommandPattern: "(rm -rf|chmod 777|curl.*\\\\|.*bash)", Action: "Deny", Risk: "High" },
536
+ { Name: "Default — allow everything else", ToolPattern: ".*", Action: "Allow", Risk: "Low" },
537
+ ],
538
+ "ci-safe": [
539
+ { Name: "Allow read-only tools", ToolPattern: "^(Read|Glob|Grep)$", Action: "Allow", Risk: "Low" },
540
+ { Name: "Allow test and build commands", ToolPattern: "^Bash$", CommandPattern: "^(npm test|npm run lint|npm run build)", Action: "Allow", Risk: "Low" },
541
+ { Name: "Deny all bash", ToolPattern: "^Bash$", Action: "Deny", Risk: "High" },
542
+ { Name: "Default — deny everything else", ToolPattern: ".*", Action: "Deny", Risk: "Medium" },
543
+ ],
544
+ };
545
+ async function template() {
546
+ const TEMPLATES = Object.keys(TEMPLATE_RULES);
413
547
  const templateName = process.argv[3];
414
548
  if (!templateName) {
415
549
  console.log(`
@@ -417,7 +551,7 @@ function template() {
417
551
 
418
552
  Available templates:
419
553
  conservative Gate writes and bash, deny destructive commands
420
- moderate Allow safe writes and bash, gate the rest
554
+ moderate Allow safe writes and bash, gate the rest (default)
421
555
  permissive Allow everything, deny only destructive commands
422
556
  ci-safe Minimal permissions for CI/CD environments
423
557
 
@@ -434,26 +568,21 @@ Example:
434
568
  console.error(`[askance] Available templates: ${TEMPLATES.join(", ")}`);
435
569
  process.exit(1);
436
570
  }
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");
571
+ const config = (0, api_client_1.readConfig)();
572
+ if (!config.token || !config.projectId) {
573
+ console.error("[askance] You must be logged in and have a project linked.");
574
+ console.error(" Run: npx askance login");
447
575
  process.exit(1);
448
576
  }
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`);
577
+ const rules = TEMPLATE_RULES[templateName];
578
+ console.log(`[askance] Applying "${templateName}" template (${rules.length} rules)...`);
579
+ const result = await (0, api_client_1.apiPost)(`/api/projects/${config.projectId}/rules`, rules);
580
+ if (!result.ok) {
581
+ console.error(`[askance] Failed to apply template: ${result.error}`);
582
+ process.exit(1);
453
583
  }
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");
584
+ console.log(`[askance] Applied "${templateName}" template to project rules`);
585
+ console.log("[askance] Rules are active immediately — no restart needed");
457
586
  }
458
587
  function help() {
459
588
  console.log(`
@@ -464,7 +593,7 @@ Usage:
464
593
 
465
594
  Commands:
466
595
  init Set up Askance in the current project
467
- - Creates .askance.yml (policy rules)
596
+ - Creates .askance/config.json (settings)
468
597
  - Configures agent hooks and MCP servers
469
598
 
470
599
  Options:
@@ -477,7 +606,8 @@ Commands:
477
606
  - Opens browser for OAuth sign-in
478
607
  - Saves access token to .askance/credentials
479
608
 
480
- template Apply a pre-built policy template
609
+ template Apply a pre-built policy template to your project
610
+ - Pushes rules to the Askance API (requires login)
481
611
  - Available: conservative, moderate, permissive, ci-safe
482
612
  - Usage: npx askance template <name>
483
613
 
@@ -497,19 +627,25 @@ Dashboard: https://app.askance.app
497
627
  Docs: https://askance.app
498
628
  `);
499
629
  }
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;
630
+ async function main() {
631
+ switch (command) {
632
+ case "init":
633
+ init();
634
+ break;
635
+ case "login":
636
+ await login();
637
+ break;
638
+ case "template":
639
+ await template();
640
+ break;
641
+ case "help":
642
+ default:
643
+ help();
644
+ break;
645
+ }
514
646
  }
647
+ main().catch((err) => {
648
+ console.error("[askance] Error:", err.message);
649
+ process.exit(1);
650
+ });
515
651
  //# sourceMappingURL=index.js.map
@@ -15,23 +15,22 @@ const node_https_1 = __importDefault(require("node:https"));
15
15
  const node_http_1 = __importDefault(require("node:http"));
16
16
  const node_fs_1 = __importDefault(require("node:fs"));
17
17
  const node_path_1 = __importDefault(require("node:path"));
18
- const yaml_1 = require("yaml");
19
18
  let cachedConfig = null;
20
19
  function readConfig() {
21
20
  if (cachedConfig)
22
21
  return cachedConfig;
23
22
  const projectDir = process.env.ASKANCE_PROJECT_DIR ?? process.cwd();
24
- // Read API URL from .askance.yml or env
23
+ // Read from .askance/config.json (primary) or env
25
24
  let apiUrl = process.env.ASKANCE_API_URL ?? "";
26
25
  let projectId = process.env.ASKANCE_PROJECT_ID ?? "";
27
- const policyPath = node_path_1.default.join(projectDir, ".askance.yml");
28
- if (node_fs_1.default.existsSync(policyPath)) {
26
+ const configPath = node_path_1.default.join(projectDir, ".askance", "config.json");
27
+ if (node_fs_1.default.existsSync(configPath)) {
29
28
  try {
30
- const yml = (0, yaml_1.parse)(node_fs_1.default.readFileSync(policyPath, "utf-8"));
31
- if (yml?.api_url && !apiUrl)
32
- apiUrl = yml.api_url;
33
- if (yml?.project_id && !projectId)
34
- projectId = yml.project_id;
29
+ const cfg = JSON.parse(node_fs_1.default.readFileSync(configPath, "utf-8"));
30
+ if (cfg.apiUrl && !apiUrl)
31
+ apiUrl = cfg.apiUrl;
32
+ if (cfg.projectId && !projectId)
33
+ projectId = cfg.projectId;
35
34
  }
36
35
  catch { }
37
36
  }
@@ -170,13 +169,13 @@ function waitForInstruction(projectId, timeoutMs) {
170
169
  }
171
170
  function readKeepAliveConfig() {
172
171
  const projectDir = process.env.ASKANCE_PROJECT_DIR ?? process.cwd();
173
- const policyPath = node_path_1.default.join(projectDir, ".askance.yml");
172
+ const configPath = node_path_1.default.join(projectDir, ".askance", "config.json");
174
173
  try {
175
- const yml = (0, yaml_1.parse)(node_fs_1.default.readFileSync(policyPath, "utf-8"));
174
+ const cfg = JSON.parse(node_fs_1.default.readFileSync(configPath, "utf-8"));
176
175
  return {
177
- enabled: yml?.keep_alive?.enabled ?? true,
178
- duration: yml?.keep_alive?.duration ?? 3600,
179
- poll_interval: yml?.keep_alive?.poll_interval ?? 30,
176
+ enabled: cfg.keepAlive?.enabled ?? true,
177
+ duration: cfg.keepAlive?.duration ?? 3600,
178
+ poll_interval: cfg.keepAlive?.pollInterval ?? 30,
180
179
  };
181
180
  }
182
181
  catch {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@askance/cli",
3
- "version": "0.1.0",
3
+ "version": "0.2.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",
@@ -29,19 +29,17 @@
29
29
  },
30
30
  "files": [
31
31
  "dist/**/*.js",
32
- "dist/templates/**",
33
32
  "!dist/daemon/**",
34
33
  "!dist/dashboard/**",
35
34
  "LICENSE",
36
35
  "README.md"
37
36
  ],
38
37
  "scripts": {
39
- "build": "tsc && node -e \"const fs=require('fs');const path=require('path');function cpDir(s,d){fs.mkdirSync(d,{recursive:true});for(const f of fs.readdirSync(s)){const sp=path.join(s,f),dp=path.join(d,f);fs.statSync(sp).isDirectory()?cpDir(sp,dp):fs.copyFileSync(sp,dp)}}cpDir('src/templates','dist/templates')\""
38
+ "build": "tsc"
40
39
  },
41
40
  "dependencies": {
42
41
  "@modelcontextprotocol/sdk": "^1.26.0",
43
- "uuid": "^11.1.0",
44
- "yaml": "^2.6.1"
42
+ "uuid": "^11.1.0"
45
43
  },
46
44
  "devDependencies": {
47
45
  "@types/uuid": "^10.0.0",
@@ -1,22 +0,0 @@
1
- version: 1
2
-
3
- api_url: https://api.askance.app
4
- project_id: ""
5
-
6
- keep_alive:
7
- enabled: false
8
- duration: 0
9
- poll_interval: 0
10
- rules:
11
- - name: "Allow read-only tools"
12
- match: { tool: "^(Read|Glob|Grep)$" }
13
- action: allow
14
- risk: low
15
- - name: "Allow test commands"
16
- match: { tool: "^Bash$", command: "^(npm test|pytest|dotnet test|go test|cargo test)" }
17
- action: allow
18
- risk: low
19
- - name: "Deny everything else"
20
- match: { tool: ".*" }
21
- action: deny
22
- risk: high
@@ -1,30 +0,0 @@
1
- version: 1
2
-
3
- api_url: https://api.askance.app
4
- project_id: ""
5
-
6
- keep_alive:
7
- enabled: true
8
- duration: 3600
9
- poll_interval: 30
10
- rules:
11
- - name: "Allow read-only tools"
12
- match: { tool: "^(Read|Glob|Grep|WebSearch|WebFetch)$" }
13
- action: allow
14
- risk: low
15
- - name: "Gate all writes"
16
- match: { tool: "^(Write|Edit|NotebookEdit)$" }
17
- action: gate
18
- risk: medium
19
- - name: "Gate bash commands"
20
- match: { tool: "^Bash$" }
21
- action: gate
22
- risk: high
23
- - name: "Deny destructive commands"
24
- match: { tool: "^Bash$", command: "(rm -rf|drop table|format |shutdown|reboot)" }
25
- action: deny
26
- risk: high
27
- - name: "Default - gate everything"
28
- match: { tool: ".*" }
29
- action: gate
30
- risk: medium
@@ -1,11 +0,0 @@
1
- {
2
- "copilot.agent": {
3
- "hooks": {
4
- "preToolUse": {
5
- "command": "node",
6
- "args": ["node_modules/@askance/cli/dist/hook-handler/index.js"],
7
- "matchTools": [".*"]
8
- }
9
- }
10
- }
11
- }
@@ -1,18 +0,0 @@
1
- {
2
- "cursor.hooks": {
3
- "preToolUse": {
4
- "command": "node",
5
- "args": ["node_modules/@askance/cli/dist/hook-handler/index.js"],
6
- "matchTools": [".*"],
7
- "excludeTools": ["mcp__askance__.*"]
8
- }
9
- },
10
- "cursor.mcp": {
11
- "servers": {
12
- "askance": {
13
- "command": "node",
14
- "args": ["node_modules/@askance/cli/dist/mcp-server/index.js"]
15
- }
16
- }
17
- }
18
- }
@@ -1,34 +0,0 @@
1
- version: 1
2
-
3
- api_url: https://api.askance.app
4
- project_id: ""
5
-
6
- keep_alive:
7
- enabled: true
8
- duration: 3600
9
- poll_interval: 30
10
- rules:
11
- - name: "Allow read-only tools"
12
- match: { tool: "^(Read|Glob|Grep|WebSearch|WebFetch)$" }
13
- action: allow
14
- risk: low
15
- - name: "Allow safe writes"
16
- match: { tool: "^(Write|Edit|NotebookEdit)$" }
17
- action: allow
18
- risk: low
19
- - name: "Allow safe bash commands"
20
- match: { tool: "^Bash$", command: "^(npm test|npm run |npx tsc|git status|git log|git diff|ls |cat |echo |pwd)" }
21
- action: allow
22
- risk: low
23
- - name: "Gate other bash commands"
24
- match: { tool: "^Bash$" }
25
- action: gate
26
- risk: medium
27
- - name: "Deny destructive commands"
28
- match: { tool: "^Bash$", command: "(rm -rf|drop table|format |shutdown|reboot|git push --force)" }
29
- action: deny
30
- risk: high
31
- - name: "Default - gate everything"
32
- match: { tool: ".*" }
33
- action: gate
34
- risk: low
@@ -1,18 +0,0 @@
1
- version: 1
2
-
3
- api_url: https://api.askance.app
4
- project_id: ""
5
-
6
- keep_alive:
7
- enabled: true
8
- duration: 3600
9
- poll_interval: 30
10
- rules:
11
- - name: "Allow all tools"
12
- match: { tool: ".*" }
13
- action: allow
14
- risk: low
15
- - name: "Deny destructive commands"
16
- match: { tool: "^Bash$", command: "(rm -rf /|drop database|format c:|shutdown|reboot|git push --force.*main)" }
17
- action: deny
18
- risk: high