@askance/cli 0.1.0 → 0.2.1

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
  // ---------------------------------------------------------------------------
@@ -137,9 +105,13 @@ function initClaude() {
137
105
  const hookHandlerPath = path_1.default.join(pkgRoot, "dist", "hook-handler", "index.js").replace(/\\/g, "/");
138
106
  const stopHookPath = path_1.default.join(pkgRoot, "dist", "hook-handler", "stop-hook.js").replace(/\\/g, "/");
139
107
  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
- : {};
108
+ let settings = {};
109
+ if (fs_1.default.existsSync(settingsPath)) {
110
+ try {
111
+ settings = JSON.parse(fs_1.default.readFileSync(settingsPath, "utf-8"));
112
+ }
113
+ catch { }
114
+ }
143
115
  // Set hooks
144
116
  const hooks = (settings.hooks ?? {});
145
117
  hooks.PreToolUse = [
@@ -188,9 +160,13 @@ function initCursor() {
188
160
  const settingsPath = path_1.default.join(cursorDir, "settings.json");
189
161
  const hookHandlerPath = path_1.default.join(pkgRoot, "dist", "hook-handler", "index.js").replace(/\\/g, "/");
190
162
  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
- : {};
163
+ let settings = {};
164
+ if (fs_1.default.existsSync(settingsPath)) {
165
+ try {
166
+ settings = JSON.parse(fs_1.default.readFileSync(settingsPath, "utf-8"));
167
+ }
168
+ catch { }
169
+ }
194
170
  // Set Cursor hooks
195
171
  const cursorHooks = (settings["cursor.hooks"] ?? {});
196
172
  cursorHooks.preToolUse = {
@@ -222,9 +198,13 @@ function initCopilot() {
222
198
  }
223
199
  const configPath = path_1.default.join(githubDir, "copilot-config.json");
224
200
  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
- : {};
201
+ let config = {};
202
+ if (fs_1.default.existsSync(configPath)) {
203
+ try {
204
+ config = JSON.parse(fs_1.default.readFileSync(configPath, "utf-8"));
205
+ }
206
+ catch { }
207
+ }
228
208
  // Set Copilot agent hooks
229
209
  const copilotAgent = (config["copilot.agent"] ?? {});
230
210
  const hooks = (copilotAgent.hooks ?? {});
@@ -247,7 +227,7 @@ const CLAUDE_MD_SECTION = `
247
227
  This project uses [Askance](https://askance.app) for AI tool call interception and approval management.
248
228
 
249
229
  **How it works:**
250
- - All tool calls are intercepted by Askance hooks and evaluated against policy rules in \`.askance.yml\`
230
+ - All tool calls are intercepted by Askance hooks and evaluated against policy rules managed via the dashboard
251
231
  - Safe operations (reads, searches) are auto-approved; risky operations require human approval
252
232
  - When a tool call is gated for approval, use \`mcp__askance__wait\` to wait for the decision
253
233
  - Use \`mcp__askance__check_instructions\` periodically to check for operator instructions
@@ -275,8 +255,8 @@ function ensureClaudeMd() {
275
255
  }
276
256
  function init() {
277
257
  console.log("[askance] Initializing Askance in", projectDir);
278
- // 1. Always write .askance.yml + .gitignore
279
- ensurePolicy();
258
+ // 1. Always create .askance/config.json + update .gitignore
259
+ ensureConfig();
280
260
  ensureGitignore();
281
261
  // 2. Determine which IDE configs to set up
282
262
  const setupClaude = flagAll || (!flagCursor && !flagCopilot);
@@ -305,7 +285,139 @@ function init() {
305
285
  console.log(" 2. Copilot agent hooks are configured in .github/copilot-config.json");
306
286
  }
307
287
  console.log(" 3. Open the dashboard: https://app.askance.app");
308
- console.log(" 4. Edit .askance.yml to customize your policy rules");
288
+ console.log(" 4. Manage policy rules via the dashboard");
289
+ }
290
+ // ---------------------------------------------------------------------------
291
+ // Project setup helpers
292
+ // ---------------------------------------------------------------------------
293
+ function detectGitInfo() {
294
+ let repoUrl = "";
295
+ let branch = "main";
296
+ let repoName = path_1.default.basename(projectDir);
297
+ try {
298
+ repoUrl = (0, child_process_1.execSync)("git remote get-url origin", {
299
+ cwd: projectDir,
300
+ encoding: "utf-8",
301
+ stdio: ["pipe", "pipe", "pipe"],
302
+ }).trim();
303
+ }
304
+ catch { }
305
+ try {
306
+ branch = (0, child_process_1.execSync)("git branch --show-current", {
307
+ cwd: projectDir,
308
+ encoding: "utf-8",
309
+ stdio: ["pipe", "pipe", "pipe"],
310
+ }).trim() || "main";
311
+ }
312
+ catch { }
313
+ // Extract repo name from URL (e.g. git@github.com:user/my-repo.git → my-repo)
314
+ if (repoUrl) {
315
+ const match = repoUrl.match(/\/([^/]+?)(?:\.git)?$/) ?? repoUrl.match(/:([^/]+?)(?:\.git)?$/);
316
+ if (match)
317
+ repoName = match[1];
318
+ }
319
+ return { repoUrl, branch, repoName };
320
+ }
321
+ function promptSelection(message, max) {
322
+ const rl = readline_1.default.createInterface({ input: process.stdin, output: process.stdout });
323
+ return new Promise((resolve) => {
324
+ rl.question(message, (answer) => {
325
+ rl.close();
326
+ const num = parseInt(answer.trim(), 10);
327
+ if (isNaN(num) || num < 1 || num > max) {
328
+ resolve(-1);
329
+ }
330
+ else {
331
+ resolve(num);
332
+ }
333
+ });
334
+ });
335
+ }
336
+ async function setupProject() {
337
+ console.log("\n[askance] Setting up project...");
338
+ // Fetch existing projects
339
+ const projectsResult = await (0, api_client_1.apiGet)("/api/projects");
340
+ if (!projectsResult.ok || !projectsResult.data) {
341
+ console.log(" Could not fetch projects. You can link a project later via the dashboard.");
342
+ return;
343
+ }
344
+ const projects = projectsResult.data;
345
+ let selectedId = "";
346
+ let selectedName = "";
347
+ if (projects.length === 1) {
348
+ // Auto-select the only project
349
+ selectedId = projects[0].id;
350
+ selectedName = projects[0].name;
351
+ console.log(` Found 1 existing project: ${selectedName}`);
352
+ console.log(` Using project: ${selectedName} (${selectedId})`);
353
+ }
354
+ else if (projects.length > 1) {
355
+ // Prompt the user to select
356
+ console.log(` Found ${projects.length} projects:`);
357
+ for (let i = 0; i < projects.length; i++) {
358
+ console.log(` ${i + 1}. ${projects[i].name}`);
359
+ }
360
+ const choice = await promptSelection(` Select a project (1-${projects.length}): `, projects.length);
361
+ if (choice < 1) {
362
+ console.log(" Invalid selection. You can link a project later via the dashboard.");
363
+ return;
364
+ }
365
+ selectedId = projects[choice - 1].id;
366
+ selectedName = projects[choice - 1].name;
367
+ console.log(` Using project: ${selectedName} (${selectedId})`);
368
+ }
369
+ else {
370
+ // No projects — auto-create one
371
+ const git = detectGitInfo();
372
+ console.log(` No projects found. Creating "${git.repoName}" from ${git.repoUrl ? "git remote" : "directory name"}...`);
373
+ const createResult = await (0, api_client_1.apiPost)("/api/projects", {
374
+ Name: git.repoName,
375
+ RepoUrl: git.repoUrl,
376
+ Branch: git.branch,
377
+ });
378
+ if (!createResult.ok) {
379
+ if (createResult.status === 402) {
380
+ console.log("\n Your plan allows 1 project and you've reached the limit.");
381
+ console.log(" Upgrade at https://app.askance.app/settings");
382
+ console.log(" You're still logged in — link a project manually or upgrade first.");
383
+ }
384
+ else {
385
+ console.log(` Failed to create project: ${createResult.error}`);
386
+ console.log(" You can create a project later via the dashboard.");
387
+ }
388
+ return;
389
+ }
390
+ if (!createResult.data) {
391
+ console.log(" Failed to create project. You can create one later via the dashboard.");
392
+ return;
393
+ }
394
+ selectedId = createResult.data.id;
395
+ selectedName = createResult.data.name;
396
+ console.log(` Created project "${selectedName}"`);
397
+ console.log(` Project ID: ${selectedId}`);
398
+ }
399
+ // Save project ID to .askance/config.json
400
+ const configPath = path_1.default.join(projectDir, ".askance", "config.json");
401
+ if (fs_1.default.existsSync(configPath)) {
402
+ try {
403
+ const cfg = JSON.parse(fs_1.default.readFileSync(configPath, "utf-8"));
404
+ cfg.projectId = selectedId;
405
+ fs_1.default.writeFileSync(configPath, JSON.stringify(cfg, null, 2) + "\n");
406
+ }
407
+ catch { }
408
+ }
409
+ // Also save to credentials for backward compatibility
410
+ const credPath = path_1.default.join(projectDir, ".askance", "credentials");
411
+ if (fs_1.default.existsSync(credPath)) {
412
+ try {
413
+ const creds = JSON.parse(fs_1.default.readFileSync(credPath, "utf-8"));
414
+ creds.project_id = selectedId;
415
+ fs_1.default.writeFileSync(credPath, JSON.stringify(creds, null, 2) + "\n", { mode: 0o600 });
416
+ }
417
+ catch { }
418
+ }
419
+ (0, api_client_1.clearConfigCache)();
420
+ console.log(" Project linked successfully.");
309
421
  }
310
422
  // ---------------------------------------------------------------------------
311
423
  // Login — opens browser for auth, saves JWT to .askance/credentials
@@ -382,13 +494,16 @@ async function login() {
382
494
  project_id: pollResult.data.projectId ?? "",
383
495
  };
384
496
  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
497
+ // Update .askance/config.json project_id if we got one
386
498
  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);
499
+ const configPath = path_1.default.join(projectDir, ".askance", "config.json");
500
+ if (fs_1.default.existsSync(configPath)) {
501
+ try {
502
+ const cfg = JSON.parse(fs_1.default.readFileSync(configPath, "utf-8"));
503
+ cfg.projectId = pollResult.data.projectId;
504
+ fs_1.default.writeFileSync(configPath, JSON.stringify(cfg, null, 2) + "\n");
505
+ }
506
+ catch { }
392
507
  }
393
508
  }
394
509
  (0, api_client_1.clearConfigCache)();
@@ -397,6 +512,10 @@ async function login() {
397
512
  console.log(` Logged in as: ${pollResult.data.email}`);
398
513
  }
399
514
  console.log(" Credentials saved to .askance/credentials");
515
+ // If the server didn't provide a project_id, run interactive setup
516
+ if (!pollResult.data.projectId) {
517
+ await setupProject();
518
+ }
400
519
  return;
401
520
  }
402
521
  if (pollResult.data.status === "expired") {
@@ -408,8 +527,35 @@ async function login() {
408
527
  console.error("\n[askance] Login timed out. Please try again.");
409
528
  process.exit(1);
410
529
  }
411
- function template() {
412
- const TEMPLATES = ["conservative", "moderate", "permissive", "ci-safe"];
530
+ const TEMPLATE_RULES = {
531
+ conservative: [
532
+ { Name: "Allow read-only tools", ToolPattern: "^(Read|Glob|Grep|WebSearch|WebFetch)$", Action: "Allow", Risk: "Low" },
533
+ { Name: "Gate file writes", ToolPattern: "^(Edit|Write|NotebookEdit)$", Action: "Gate", Risk: "Medium" },
534
+ { Name: "Gate all bash commands", ToolPattern: "^Bash$", Action: "Gate", Risk: "Medium" },
535
+ { Name: "Deny destructive commands", ToolPattern: "^Bash$", CommandPattern: "(rm -rf|chmod 777|curl.*\\\\|.*bash)", Action: "Deny", Risk: "High" },
536
+ { Name: "Default — gate everything else", ToolPattern: ".*", Action: "Gate", Risk: "Low" },
537
+ ],
538
+ moderate: [
539
+ { Name: "Allow read-only tools", ToolPattern: "^(Read|Glob|Grep|WebSearch|WebFetch)$", Action: "Allow", Risk: "Low" },
540
+ { 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" },
541
+ { Name: "Gate file writes", ToolPattern: "^(Edit|Write|NotebookEdit)$", Action: "Gate", Risk: "Medium" },
542
+ { Name: "Gate package installs", ToolPattern: "^Bash$", CommandPattern: "(npm install|pip install|dotnet add)", Action: "Gate", Risk: "Medium" },
543
+ { Name: "Deny destructive commands", ToolPattern: "^Bash$", CommandPattern: "(rm -rf|chmod 777|curl.*\\\\|.*bash)", Action: "Deny", Risk: "High" },
544
+ { Name: "Default — gate everything else", ToolPattern: ".*", Action: "Gate", Risk: "Low" },
545
+ ],
546
+ permissive: [
547
+ { Name: "Deny destructive commands", ToolPattern: "^Bash$", CommandPattern: "(rm -rf|chmod 777|curl.*\\\\|.*bash)", Action: "Deny", Risk: "High" },
548
+ { Name: "Default — allow everything else", ToolPattern: ".*", Action: "Allow", Risk: "Low" },
549
+ ],
550
+ "ci-safe": [
551
+ { Name: "Allow read-only tools", ToolPattern: "^(Read|Glob|Grep)$", Action: "Allow", Risk: "Low" },
552
+ { Name: "Allow test and build commands", ToolPattern: "^Bash$", CommandPattern: "^(npm test|npm run lint|npm run build)", Action: "Allow", Risk: "Low" },
553
+ { Name: "Deny all bash", ToolPattern: "^Bash$", Action: "Deny", Risk: "High" },
554
+ { Name: "Default — deny everything else", ToolPattern: ".*", Action: "Deny", Risk: "Medium" },
555
+ ],
556
+ };
557
+ async function template() {
558
+ const TEMPLATES = Object.keys(TEMPLATE_RULES);
413
559
  const templateName = process.argv[3];
414
560
  if (!templateName) {
415
561
  console.log(`
@@ -417,7 +563,7 @@ function template() {
417
563
 
418
564
  Available templates:
419
565
  conservative Gate writes and bash, deny destructive commands
420
- moderate Allow safe writes and bash, gate the rest
566
+ moderate Allow safe writes and bash, gate the rest (default)
421
567
  permissive Allow everything, deny only destructive commands
422
568
  ci-safe Minimal permissions for CI/CD environments
423
569
 
@@ -434,26 +580,21 @@ Example:
434
580
  console.error(`[askance] Available templates: ${TEMPLATES.join(", ")}`);
435
581
  process.exit(1);
436
582
  }
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");
583
+ const config = (0, api_client_1.readConfig)();
584
+ if (!config.token || !config.projectId) {
585
+ console.error("[askance] You must be logged in and have a project linked.");
586
+ console.error(" Run: npx askance login");
447
587
  process.exit(1);
448
588
  }
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`);
589
+ const rules = TEMPLATE_RULES[templateName];
590
+ console.log(`[askance] Applying "${templateName}" template (${rules.length} rules)...`);
591
+ const result = await (0, api_client_1.apiPost)(`/api/projects/${config.projectId}/rules`, rules);
592
+ if (!result.ok) {
593
+ console.error(`[askance] Failed to apply template: ${result.error}`);
594
+ process.exit(1);
453
595
  }
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");
596
+ console.log(`[askance] Applied "${templateName}" template to project rules`);
597
+ console.log("[askance] Rules are active immediately — no restart needed");
457
598
  }
458
599
  function help() {
459
600
  console.log(`
@@ -464,7 +605,7 @@ Usage:
464
605
 
465
606
  Commands:
466
607
  init Set up Askance in the current project
467
- - Creates .askance.yml (policy rules)
608
+ - Creates .askance/config.json (settings)
468
609
  - Configures agent hooks and MCP servers
469
610
 
470
611
  Options:
@@ -477,7 +618,8 @@ Commands:
477
618
  - Opens browser for OAuth sign-in
478
619
  - Saves access token to .askance/credentials
479
620
 
480
- template Apply a pre-built policy template
621
+ template Apply a pre-built policy template to your project
622
+ - Pushes rules to the Askance API (requires login)
481
623
  - Available: conservative, moderate, permissive, ci-safe
482
624
  - Usage: npx askance template <name>
483
625
 
@@ -497,19 +639,25 @@ Dashboard: https://app.askance.app
497
639
  Docs: https://askance.app
498
640
  `);
499
641
  }
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;
642
+ async function main() {
643
+ switch (command) {
644
+ case "init":
645
+ init();
646
+ break;
647
+ case "login":
648
+ await login();
649
+ break;
650
+ case "template":
651
+ await template();
652
+ break;
653
+ case "help":
654
+ default:
655
+ help();
656
+ break;
657
+ }
514
658
  }
659
+ main().catch((err) => {
660
+ console.error("[askance] Error:", err.message);
661
+ process.exit(1);
662
+ });
515
663
  //# 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.1",
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