@askmesh/mcp 0.10.7 → 0.11.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/dist/agent/askmeshignore.d.ts +11 -0
- package/dist/agent/askmeshignore.js +97 -0
- package/dist/agent/auto_responder.d.ts +6 -0
- package/dist/agent/auto_responder.js +43 -5
- package/dist/agent/context_reader.js +12 -3
- package/dist/agent/redaction.d.ts +16 -0
- package/dist/agent/redaction.js +101 -0
- package/dist/index.js +1 -1
- package/dist/tools/askmesh.js +85 -3
- package/package.json +1 -1
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare const DEFAULT_PATTERNS: string[];
|
|
2
|
+
export interface IgnoreMatcher {
|
|
3
|
+
patterns: string[];
|
|
4
|
+
shouldIgnore: (filePath: string) => boolean;
|
|
5
|
+
describe: () => string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Load .askmeshignore from the current working directory and merge with defaults.
|
|
9
|
+
* Returns a matcher with shouldIgnore() and a describe() helper for prompts.
|
|
10
|
+
*/
|
|
11
|
+
export declare function loadIgnore(cwd?: string): IgnoreMatcher;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
2
|
+
import { join, basename } from 'node:path';
|
|
3
|
+
// Default exclusions — applied even when no .askmeshignore file exists.
|
|
4
|
+
// These are patterns that almost no project should ever share via the mesh.
|
|
5
|
+
export const DEFAULT_PATTERNS = [
|
|
6
|
+
// Environment files
|
|
7
|
+
'.env',
|
|
8
|
+
'.env.*',
|
|
9
|
+
'*.env',
|
|
10
|
+
// Secrets folders
|
|
11
|
+
'secrets/',
|
|
12
|
+
'secret/',
|
|
13
|
+
'private/',
|
|
14
|
+
'.secrets/',
|
|
15
|
+
// Keys and certificates
|
|
16
|
+
'*.key',
|
|
17
|
+
'*.pem',
|
|
18
|
+
'*.p12',
|
|
19
|
+
'*.pfx',
|
|
20
|
+
'*.crt',
|
|
21
|
+
'*.csr',
|
|
22
|
+
'*.der',
|
|
23
|
+
'id_rsa',
|
|
24
|
+
'id_rsa.*',
|
|
25
|
+
'id_dsa',
|
|
26
|
+
'id_ecdsa',
|
|
27
|
+
'id_ed25519',
|
|
28
|
+
// Credentials
|
|
29
|
+
'credentials',
|
|
30
|
+
'credentials.*',
|
|
31
|
+
'.npmrc',
|
|
32
|
+
'.pypirc',
|
|
33
|
+
'.netrc',
|
|
34
|
+
'.aws/',
|
|
35
|
+
'.ssh/',
|
|
36
|
+
'.gnupg/',
|
|
37
|
+
// Cloud
|
|
38
|
+
'service-account.json',
|
|
39
|
+
'service-account-*.json',
|
|
40
|
+
'gcp-key.json',
|
|
41
|
+
'firebase-adminsdk-*.json',
|
|
42
|
+
// Common build/dep dirs
|
|
43
|
+
'node_modules/',
|
|
44
|
+
'.git/',
|
|
45
|
+
];
|
|
46
|
+
/**
|
|
47
|
+
* Load .askmeshignore from the current working directory and merge with defaults.
|
|
48
|
+
* Returns a matcher with shouldIgnore() and a describe() helper for prompts.
|
|
49
|
+
*/
|
|
50
|
+
export function loadIgnore(cwd = process.cwd()) {
|
|
51
|
+
const patterns = [...DEFAULT_PATTERNS];
|
|
52
|
+
const file = join(cwd, '.askmeshignore');
|
|
53
|
+
if (existsSync(file)) {
|
|
54
|
+
try {
|
|
55
|
+
const content = readFileSync(file, 'utf-8');
|
|
56
|
+
for (const rawLine of content.split('\n')) {
|
|
57
|
+
const line = rawLine.trim();
|
|
58
|
+
if (!line || line.startsWith('#'))
|
|
59
|
+
continue;
|
|
60
|
+
patterns.push(line);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch { }
|
|
64
|
+
}
|
|
65
|
+
const compiled = patterns.map(compile);
|
|
66
|
+
const shouldIgnore = (filePath) => {
|
|
67
|
+
const name = basename(filePath);
|
|
68
|
+
return compiled.some((re) => re.test(filePath) || re.test(name));
|
|
69
|
+
};
|
|
70
|
+
const describe = () => {
|
|
71
|
+
return patterns.join(', ');
|
|
72
|
+
};
|
|
73
|
+
return { patterns, shouldIgnore, describe };
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Convert a glob-like pattern to a RegExp.
|
|
77
|
+
* Supports: *, **, trailing / (folder), leading * (suffix match)
|
|
78
|
+
*/
|
|
79
|
+
function compile(pattern) {
|
|
80
|
+
let p = pattern.trim();
|
|
81
|
+
// Folder pattern (trailing /)
|
|
82
|
+
const isFolder = p.endsWith('/');
|
|
83
|
+
if (isFolder)
|
|
84
|
+
p = p.slice(0, -1);
|
|
85
|
+
// Escape regex special chars except * and ?
|
|
86
|
+
let re = p.replace(/[.+^${}()|[\]\\]/g, '\\$&');
|
|
87
|
+
// ** matches anything (including /)
|
|
88
|
+
re = re.replace(/\*\*/g, '§§');
|
|
89
|
+
// * matches anything except /
|
|
90
|
+
re = re.replace(/\*/g, '[^/]*');
|
|
91
|
+
re = re.replace(/§§/g, '.*');
|
|
92
|
+
re = re.replace(/\?/g, '.');
|
|
93
|
+
if (isFolder) {
|
|
94
|
+
return new RegExp(`(^|/)${re}(/|$)`);
|
|
95
|
+
}
|
|
96
|
+
return new RegExp(`(^|/)${re}$`);
|
|
97
|
+
}
|
|
@@ -6,6 +6,12 @@ export declare class AutoResponder {
|
|
|
6
6
|
private mcpServer;
|
|
7
7
|
constructor(client: AskMeshClient);
|
|
8
8
|
setServer(server: Server): void;
|
|
9
|
+
/**
|
|
10
|
+
* Send a reply through the AskMesh client with a pre-send redaction scan.
|
|
11
|
+
* If secrets are detected, the reply is blocked and a desktop notification +
|
|
12
|
+
* status line update inform the owner. Returns true if the reply was sent.
|
|
13
|
+
*/
|
|
14
|
+
private safeReply;
|
|
9
15
|
handleRequest(request: IncomingRequest): Promise<void>;
|
|
10
16
|
private callAnthropicAPI;
|
|
11
17
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readLocalContext } from './context_reader.js';
|
|
2
2
|
import { sendDesktopNotification } from './notifier.js';
|
|
3
|
+
import { scanForSecrets, formatBlockedMessage } from './redaction.js';
|
|
3
4
|
const SYSTEM_PROMPT = `You are an AI coding agent responding on behalf of a developer through AskMesh.
|
|
4
5
|
You have access to the developer's project context below.
|
|
5
6
|
Use this context to answer accurately and concisely.
|
|
@@ -12,8 +13,12 @@ SECURITY RULES:
|
|
|
12
13
|
- READ-ONLY: Never suggest or execute destructive operations (DELETE, DROP, rm, reset --hard, etc.)
|
|
13
14
|
- NO SECRETS: Never include passwords, API keys, tokens, or credentials in your responses
|
|
14
15
|
- NO SENSITIVE DATA: Never expose personal data, database connection strings, or internal URLs
|
|
16
|
+
- HONOR .askmeshignore: Never read or share files listed by the project's privacy policy
|
|
15
17
|
- If asked for sensitive information, respond: "I can't share this information via AskMesh for security reasons."
|
|
16
|
-
- You may share: code patterns, architecture decisions, file structures, conventions, public configs
|
|
18
|
+
- You may share: code patterns, architecture decisions, file structures, conventions, public configs
|
|
19
|
+
|
|
20
|
+
A pre-send filter will block your response if it contains detectable secrets (API keys, JWTs, private keys, DB URLs, etc.).
|
|
21
|
+
Avoid quoting raw config values — describe them instead.`;
|
|
17
22
|
export class AutoResponder {
|
|
18
23
|
client;
|
|
19
24
|
mcpServer = null;
|
|
@@ -23,6 +28,37 @@ export class AutoResponder {
|
|
|
23
28
|
setServer(server) {
|
|
24
29
|
this.mcpServer = server;
|
|
25
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* Send a reply through the AskMesh client with a pre-send redaction scan.
|
|
33
|
+
* If secrets are detected, the reply is blocked and a desktop notification +
|
|
34
|
+
* status line update inform the owner. Returns true if the reply was sent.
|
|
35
|
+
*/
|
|
36
|
+
async safeReply(requestId, answer, source) {
|
|
37
|
+
const scan = scanForSecrets(answer);
|
|
38
|
+
if (!scan.safe) {
|
|
39
|
+
const samples = scan.hits.map((h) => `${h.pattern} (${h.sample})`).join(', ');
|
|
40
|
+
console.error(`[AskMesh] 🚫 BLOCKED reply #${requestId} (${source}) — secrets detected: ${samples}`);
|
|
41
|
+
// Notify the owner via desktop and via the MCP logging channel
|
|
42
|
+
sendDesktopNotification('AskMesh — réponse bloquée', `Secrets détectés: ${scan.hits.map((h) => h.pattern).join(', ')}`);
|
|
43
|
+
if (this.mcpServer) {
|
|
44
|
+
try {
|
|
45
|
+
await this.mcpServer.sendLoggingMessage({
|
|
46
|
+
level: 'warning',
|
|
47
|
+
data: formatBlockedMessage(scan),
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
catch { }
|
|
51
|
+
}
|
|
52
|
+
// Revert thread status to pending so the human can take over
|
|
53
|
+
try {
|
|
54
|
+
await this.client.updateThreadStatus(requestId, 'pending');
|
|
55
|
+
}
|
|
56
|
+
catch { }
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
await this.client.replyToThread(requestId, answer);
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
26
62
|
async handleRequest(request) {
|
|
27
63
|
console.error(`[AskMesh] Question from @${request.fromUsername}: "${request.question}"`);
|
|
28
64
|
// Desktop notification
|
|
@@ -67,8 +103,9 @@ export class AutoResponder {
|
|
|
67
103
|
}, {}));
|
|
68
104
|
const answer = result?.content?.text || result?.content?.[0]?.text;
|
|
69
105
|
if (answer) {
|
|
70
|
-
await this.
|
|
71
|
-
|
|
106
|
+
const sent = await this.safeReply(request.id, answer, 'Claude Code');
|
|
107
|
+
if (sent)
|
|
108
|
+
console.error(`[AskMesh] Responded to #${request.id} via Claude Code`);
|
|
72
109
|
return;
|
|
73
110
|
}
|
|
74
111
|
}
|
|
@@ -83,8 +120,9 @@ export class AutoResponder {
|
|
|
83
120
|
const context = readLocalContext();
|
|
84
121
|
const answer = await this.callAnthropicAPI(apiKey, request, context);
|
|
85
122
|
if (answer) {
|
|
86
|
-
await this.
|
|
87
|
-
|
|
123
|
+
const sent = await this.safeReply(request.id, answer, 'Anthropic API');
|
|
124
|
+
if (sent)
|
|
125
|
+
console.error(`[AskMesh] Responded to #${request.id} via Anthropic API`);
|
|
88
126
|
return;
|
|
89
127
|
}
|
|
90
128
|
}
|
|
@@ -1,18 +1,22 @@
|
|
|
1
1
|
import { readFileSync, readdirSync, existsSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
|
+
import { loadIgnore } from './askmeshignore.js';
|
|
4
5
|
export function readLocalContext() {
|
|
5
6
|
const parts = [];
|
|
7
|
+
const ignore = loadIgnore();
|
|
6
8
|
// 1. CLAUDE.md from current working directory
|
|
7
9
|
const claudeMdPath = join(process.cwd(), 'CLAUDE.md');
|
|
8
|
-
if (existsSync(claudeMdPath)) {
|
|
10
|
+
if (existsSync(claudeMdPath) && !ignore.shouldIgnore('CLAUDE.md')) {
|
|
9
11
|
parts.push('=== CLAUDE.md ===');
|
|
10
12
|
parts.push(readSafe(claudeMdPath));
|
|
11
13
|
}
|
|
12
14
|
// 2. Global Claude Code memories
|
|
13
15
|
const globalMemDir = join(homedir(), '.claude', 'memory');
|
|
14
16
|
if (existsSync(globalMemDir)) {
|
|
15
|
-
const files = safeReadDir(globalMemDir)
|
|
17
|
+
const files = safeReadDir(globalMemDir)
|
|
18
|
+
.filter((f) => f.endsWith('.md'))
|
|
19
|
+
.filter((f) => !ignore.shouldIgnore(f));
|
|
16
20
|
if (files.length > 0) {
|
|
17
21
|
parts.push('=== Global memories ===');
|
|
18
22
|
for (const file of files.slice(0, 10)) {
|
|
@@ -32,7 +36,9 @@ export function readLocalContext() {
|
|
|
32
36
|
if (dir.includes(cwdKey) || cwdKey.includes(dir)) {
|
|
33
37
|
const memDir = join(projectsDir, dir, 'memory');
|
|
34
38
|
if (existsSync(memDir)) {
|
|
35
|
-
const files = safeReadDir(memDir)
|
|
39
|
+
const files = safeReadDir(memDir)
|
|
40
|
+
.filter((f) => f.endsWith('.md'))
|
|
41
|
+
.filter((f) => !ignore.shouldIgnore(f));
|
|
36
42
|
if (files.length > 0) {
|
|
37
43
|
parts.push('=== Project memories ===');
|
|
38
44
|
for (const file of files.slice(0, 10)) {
|
|
@@ -45,6 +51,9 @@ export function readLocalContext() {
|
|
|
45
51
|
}
|
|
46
52
|
}
|
|
47
53
|
}
|
|
54
|
+
// 4. Append the active ignore policy so the agent knows what NOT to read
|
|
55
|
+
parts.push('=== Privacy policy (.askmeshignore) ===');
|
|
56
|
+
parts.push(`Never read or share contents of files matching these patterns: ${ignore.describe()}`);
|
|
48
57
|
return parts.join('\n\n') || 'No local context available.';
|
|
49
58
|
}
|
|
50
59
|
function readSafe(path) {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface RedactionResult {
|
|
2
|
+
safe: boolean;
|
|
3
|
+
hits: Array<{
|
|
4
|
+
pattern: string;
|
|
5
|
+
sample: string;
|
|
6
|
+
}>;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Scan a reply for known secret patterns. Returns safe=false if any are found.
|
|
10
|
+
* The hits include the pattern name and a truncated sample for logging/audit.
|
|
11
|
+
*/
|
|
12
|
+
export declare function scanForSecrets(text: string): RedactionResult;
|
|
13
|
+
/**
|
|
14
|
+
* Format a user-facing block message when redaction triggers.
|
|
15
|
+
*/
|
|
16
|
+
export declare function formatBlockedMessage(result: RedactionResult): string;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// Pre-send redaction — scans outgoing replies for common secret patterns
|
|
2
|
+
// and blocks them before they're sent through the mesh.
|
|
3
|
+
//
|
|
4
|
+
// This is a defense-in-depth layer. The agent is also instructed not to share
|
|
5
|
+
// secrets, but we don't trust the LLM to never slip up.
|
|
6
|
+
const PATTERNS = [
|
|
7
|
+
{
|
|
8
|
+
name: 'AWS Access Key',
|
|
9
|
+
detect: (t) => match(t, /\bAKIA[0-9A-Z]{16}\b/g),
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
name: 'AWS Secret Key (assignment)',
|
|
13
|
+
detect: (t) => match(t, /aws_secret_access_key\s*[=:]\s*['"][^'"]+['"]/gi),
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
name: 'OpenAI API Key',
|
|
17
|
+
detect: (t) => match(t, /\bsk-(?:proj-)?[A-Za-z0-9_\-]{32,}\b/g),
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
name: 'Anthropic API Key',
|
|
21
|
+
detect: (t) => match(t, /\bsk-ant-[A-Za-z0-9_\-]{20,}\b/g),
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
name: 'GitHub Token',
|
|
25
|
+
detect: (t) => match(t, /\bgh[poursa]_[A-Za-z0-9_]{36,}\b/g),
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
name: 'Slack Token',
|
|
29
|
+
detect: (t) => match(t, /\bxox[bpoars]-[A-Za-z0-9-]{10,}\b/g),
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
name: 'Stripe Key',
|
|
33
|
+
detect: (t) => match(t, /\b(?:sk|pk|rk)_(?:test|live)_[A-Za-z0-9]{20,}\b/g),
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: 'JWT',
|
|
37
|
+
detect: (t) => match(t, /\beyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/g),
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: 'Private Key (PEM)',
|
|
41
|
+
detect: (t) => match(t, /-----BEGIN (?:RSA |EC |OPENSSH |DSA |PGP )?PRIVATE KEY-----/g),
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: 'Database URL',
|
|
45
|
+
detect: (t) => match(t, /\b(?:postgres(?:ql)?|mysql|mongodb(?:\+srv)?|redis|amqp):\/\/[^\s'"]*:[^\s'"@]+@[^\s'"]+/gi),
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: 'Generic API Key Assignment',
|
|
49
|
+
detect: (t) => match(t, /\b(?:api[_-]?key|apikey|secret(?:[_-]?key)?|access[_-]?token|auth[_-]?token|password)\s*[:=]\s*['"][A-Za-z0-9_\-+/=]{16,}['"]/gi),
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: 'Google API Key',
|
|
53
|
+
detect: (t) => match(t, /\bAIza[0-9A-Za-z\-_]{35}\b/g),
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
name: 'Square Token',
|
|
57
|
+
detect: (t) => match(t, /\bsq0(?:atp|csp|idp)-[A-Za-z0-9_-]{22,}\b/g),
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: 'Twilio API Key',
|
|
61
|
+
detect: (t) => match(t, /\bSK[a-f0-9]{32}\b/g),
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
name: 'SendGrid API Key',
|
|
65
|
+
detect: (t) => match(t, /\bSG\.[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}\b/g),
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: 'Mailgun API Key',
|
|
69
|
+
detect: (t) => match(t, /\bkey-[a-f0-9]{32}\b/g),
|
|
70
|
+
},
|
|
71
|
+
];
|
|
72
|
+
function match(text, re) {
|
|
73
|
+
const found = text.match(re);
|
|
74
|
+
return found && found.length > 0 ? found : null;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Scan a reply for known secret patterns. Returns safe=false if any are found.
|
|
78
|
+
* The hits include the pattern name and a truncated sample for logging/audit.
|
|
79
|
+
*/
|
|
80
|
+
export function scanForSecrets(text) {
|
|
81
|
+
const hits = [];
|
|
82
|
+
for (const p of PATTERNS) {
|
|
83
|
+
const found = p.detect(text);
|
|
84
|
+
if (found) {
|
|
85
|
+
for (const f of found) {
|
|
86
|
+
hits.push({
|
|
87
|
+
pattern: p.name,
|
|
88
|
+
sample: f.length > 24 ? `${f.slice(0, 8)}…${f.slice(-4)}` : f,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return { safe: hits.length === 0, hits };
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Format a user-facing block message when redaction triggers.
|
|
97
|
+
*/
|
|
98
|
+
export function formatBlockedMessage(result) {
|
|
99
|
+
const list = result.hits.map((h) => ` - ${h.pattern}: ${h.sample}`).join('\n');
|
|
100
|
+
return `🚫 Réponse bloquée par le filtre AskMesh — secrets détectés :\n${list}\n\nReformule ta réponse sans les valeurs sensibles.`;
|
|
101
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -11,7 +11,7 @@ const TOKEN = process.env.ASKMESH_TOKEN;
|
|
|
11
11
|
const URL = process.env.ASKMESH_URL || 'https://api.askmesh.dev';
|
|
12
12
|
const server = new McpServer({
|
|
13
13
|
name: 'askmesh',
|
|
14
|
-
version: '0.
|
|
14
|
+
version: '0.11.1',
|
|
15
15
|
});
|
|
16
16
|
if (!TOKEN) {
|
|
17
17
|
// No token — start in setup-only mode
|
package/dist/tools/askmesh.js
CHANGED
|
@@ -5,6 +5,7 @@ import { homedir } from 'os';
|
|
|
5
5
|
import { createHash } from 'crypto';
|
|
6
6
|
import { fileURLToPath } from 'url';
|
|
7
7
|
import * as statusCache from '../statusline/cache.js';
|
|
8
|
+
import { DEFAULT_PATTERNS, loadIgnore } from '../agent/askmeshignore.js';
|
|
8
9
|
export function registerAskMesh(server, client) {
|
|
9
10
|
server.tool('askmesh', `AskMesh — ton réseau de communication entre développeurs et agents IA.
|
|
10
11
|
Utilise cet outil pour envoyer et recevoir des messages, vérifier qui est connecté/online,
|
|
@@ -37,8 +38,9 @@ Actions disponibles :
|
|
|
37
38
|
- "setup" : installer les slash commands (/ask-inbox, /ask-broadcast, etc.) et la status line dans le projet courant
|
|
38
39
|
- "update" : vérifier les mises à jour du MCP, mettre à jour les skills et la status line
|
|
39
40
|
- "loop-start" : marquer le loop comme actif (utilisé par /ask-loop)
|
|
40
|
-
- "loop-stop" : arrêter le loop (utilisé par /ask-loop-stop)
|
|
41
|
-
|
|
41
|
+
- "loop-stop" : arrêter le loop (utilisé par /ask-loop-stop)
|
|
42
|
+
- "privacy" : afficher la politique .askmeshignore active (defaults + fichier projet)`, {
|
|
43
|
+
action: z.enum(['ask', 'list', 'status', 'pending', 'inbox', 'answer', 'reply', 'thread', 'close', 'my-threads', 'board', 'progress', 'broadcast', 'context', 'setup', 'update', 'loop-start', 'loop-stop', 'privacy']).describe('Action à effectuer'),
|
|
42
44
|
username: z.string().optional().describe("Username de l'agent cible (pour ask/status)"),
|
|
43
45
|
question: z.string().optional().describe('Question à poser (pour ask)'),
|
|
44
46
|
requestId: z.number().optional().describe('ID de la requête (pour answer/reply/thread/close/progress)'),
|
|
@@ -246,6 +248,9 @@ Actions disponibles :
|
|
|
246
248
|
statusCache.setLoopActive(false);
|
|
247
249
|
return text('Loop arrêté.');
|
|
248
250
|
}
|
|
251
|
+
case 'privacy': {
|
|
252
|
+
return showPrivacyPolicy();
|
|
253
|
+
}
|
|
249
254
|
default:
|
|
250
255
|
return text('Action inconnue.');
|
|
251
256
|
}
|
|
@@ -277,7 +282,7 @@ Si l'utilisateur n'a pas encore de compte, dirige-le vers https://askmesh.dev po
|
|
|
277
282
|
return text('Action inconnue.');
|
|
278
283
|
});
|
|
279
284
|
}
|
|
280
|
-
const CURRENT_VERSION = '0.
|
|
285
|
+
const CURRENT_VERSION = '0.11.1';
|
|
281
286
|
async function checkUpdateAndSetup() {
|
|
282
287
|
const lines = [];
|
|
283
288
|
lines.push(`Version actuelle : ${CURRENT_VERSION}`);
|
|
@@ -306,6 +311,55 @@ async function checkUpdateAndSetup() {
|
|
|
306
311
|
lines.push(setupText);
|
|
307
312
|
return text(lines.join('\n'));
|
|
308
313
|
}
|
|
314
|
+
function showPrivacyPolicy() {
|
|
315
|
+
const cwd = process.cwd();
|
|
316
|
+
const filePath = join(cwd, '.askmeshignore');
|
|
317
|
+
const fileExists = existsSync(filePath);
|
|
318
|
+
let userPatterns = [];
|
|
319
|
+
if (fileExists) {
|
|
320
|
+
try {
|
|
321
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
322
|
+
userPatterns = content
|
|
323
|
+
.split('\n')
|
|
324
|
+
.map((l) => l.trim())
|
|
325
|
+
.filter((l) => l && !l.startsWith('#'));
|
|
326
|
+
}
|
|
327
|
+
catch { }
|
|
328
|
+
}
|
|
329
|
+
const matcher = loadIgnore(cwd);
|
|
330
|
+
const lines = [];
|
|
331
|
+
lines.push('🔒 AskMesh Privacy Policy');
|
|
332
|
+
lines.push('');
|
|
333
|
+
lines.push('Your agent will NEVER read or share files matching these patterns.');
|
|
334
|
+
lines.push('');
|
|
335
|
+
lines.push(`📋 Default patterns (${DEFAULT_PATTERNS.length}, baked into the MCP):`);
|
|
336
|
+
for (const p of DEFAULT_PATTERNS) {
|
|
337
|
+
lines.push(` • ${p}`);
|
|
338
|
+
}
|
|
339
|
+
lines.push('');
|
|
340
|
+
if (fileExists) {
|
|
341
|
+
lines.push(`📁 .askmeshignore (${filePath}) — ${userPatterns.length} custom pattern(s):`);
|
|
342
|
+
if (userPatterns.length > 0) {
|
|
343
|
+
for (const p of userPatterns) {
|
|
344
|
+
lines.push(` • ${p}`);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
else {
|
|
348
|
+
lines.push(' (no custom patterns yet — file is empty or only comments)');
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
else {
|
|
352
|
+
lines.push(`📁 .askmeshignore — not found in this project`);
|
|
353
|
+
lines.push(` Create it to add custom patterns:`);
|
|
354
|
+
lines.push(` echo "private-docs/" > ${filePath}`);
|
|
355
|
+
}
|
|
356
|
+
lines.push('');
|
|
357
|
+
lines.push(`🛡 Total active patterns: ${matcher.patterns.length}`);
|
|
358
|
+
lines.push('');
|
|
359
|
+
lines.push('🚫 Pre-send redaction filter is also active — replies containing API keys,');
|
|
360
|
+
lines.push(' JWTs, private keys, DB connection strings, etc. will be blocked automatically.');
|
|
361
|
+
return text(lines.join('\n'));
|
|
362
|
+
}
|
|
309
363
|
function text(t) {
|
|
310
364
|
return { content: [{ type: 'text', text: t }] };
|
|
311
365
|
}
|
|
@@ -334,6 +388,7 @@ function setupSkillsAndStatusLine(token, pollInterval, url) {
|
|
|
334
388
|
'ask-update.md': `Utilise l'outil MCP askmesh avec l'action "update" pour vérifier les mises à jour et mettre à jour les skills et la status line.\n\nCela va :\n- Vérifier si une nouvelle version du MCP est disponible sur npm\n- Mettre à jour les slash commands\n- Mettre à jour le script de status line\n\nSi une mise à jour est disponible, indique à l'utilisateur comment l'installer (relancer Claude Code ou npx clear-npx-cache).`,
|
|
335
389
|
'ask-loop.md': `Lance une boucle de vérification périodique des messages AskMesh.\n\nÉtapes :\n1. D'abord, utilise l'outil MCP askmesh avec l'action "loop-start" pour vérifier qu'aucun loop n'est déjà actif et marquer le loop comme actif\n2. Si le loop-start réussit, lance : /loop <intervalle ou 2m> /ask-inbox\n\nArguments optionnels : $ARGUMENTS (intervalle, ex: "2m", "5m", "30s")\nPar défaut : 2m\n\nSi un loop est déjà actif, préviens l'utilisateur et propose /ask-loop-stop.`,
|
|
336
390
|
'ask-loop-stop.md': `Arrête la boucle de vérification périodique des messages AskMesh.\n\nÉtapes :\n1. Utilise l'outil MCP askmesh avec l'action "loop-stop" pour marquer le loop comme inactif\n2. Indique à l'utilisateur que le loop est arrêté\n\nNote : cela met à jour la status line pour retirer l'indicateur "loop".`,
|
|
391
|
+
'ask-privacy.md': `Utilise l'outil MCP askmesh avec l'action "privacy" pour afficher la politique de confidentialité active du projet.\n\nCela montre :\n- Les patterns par défaut bakés dans le MCP (jamais lus ni partagés via le mesh)\n- Les patterns custom du fichier .askmeshignore du projet (s'il existe)\n- Le nombre total de patterns actifs\n- Un rappel que le filtre de redaction pré-envoi est actif\n\nC'est l'outil pour répondre aux questions du type "qu'est-ce qui peut sortir de mon projet via askmesh ?".`,
|
|
337
392
|
};
|
|
338
393
|
// Create commands directory
|
|
339
394
|
try {
|
|
@@ -419,6 +474,32 @@ function setupSkillsAndStatusLine(token, pollInterval, url) {
|
|
|
419
474
|
gitignoreStatus = '.gitignore : .env ajouté';
|
|
420
475
|
}
|
|
421
476
|
}
|
|
477
|
+
// Create a .askmeshignore template if it doesn't exist
|
|
478
|
+
const askmeshIgnorePath = join(cwd, '.askmeshignore');
|
|
479
|
+
let askmeshIgnoreStatus = '';
|
|
480
|
+
if (!existsSync(askmeshIgnorePath)) {
|
|
481
|
+
const template = `# AskMesh privacy policy
|
|
482
|
+
# Patterns listed here will NEVER be read or shared via the mesh.
|
|
483
|
+
#
|
|
484
|
+
# The MCP also has built-in defaults that are ALWAYS active, even
|
|
485
|
+
# without this file. Run /ask-privacy to see the full active policy.
|
|
486
|
+
#
|
|
487
|
+
# Built-in defaults include: .env, .env.*, secrets/, *.key, *.pem,
|
|
488
|
+
# id_rsa*, .aws/, .ssh/, service-account.json, node_modules/, .git/
|
|
489
|
+
#
|
|
490
|
+
# Use this file to add project-specific exclusions:
|
|
491
|
+
|
|
492
|
+
# private-docs/
|
|
493
|
+
# config/production.yml
|
|
494
|
+
# *.dump
|
|
495
|
+
# customers/
|
|
496
|
+
`;
|
|
497
|
+
writeFileSync(askmeshIgnorePath, template);
|
|
498
|
+
askmeshIgnoreStatus = '.askmeshignore template créé — édite-le pour ajouter tes patterns custom';
|
|
499
|
+
}
|
|
500
|
+
else {
|
|
501
|
+
askmeshIgnoreStatus = '.askmeshignore : présent (run /ask-privacy pour voir la policy active)';
|
|
502
|
+
}
|
|
422
503
|
// Auto-configure status line — per-project, not global
|
|
423
504
|
// Copy statusline.sh to ~/.claude/ (stable location), configure per-project with agent hash
|
|
424
505
|
const mcpDir = dirname(fileURLToPath(import.meta.url));
|
|
@@ -486,6 +567,7 @@ function setupSkillsAndStatusLine(token, pollInterval, url) {
|
|
|
486
567
|
lines.push(envStatus);
|
|
487
568
|
if (gitignoreStatus)
|
|
488
569
|
lines.push(gitignoreStatus);
|
|
570
|
+
lines.push(askmeshIgnoreStatus);
|
|
489
571
|
lines.push(statusLineStatus);
|
|
490
572
|
if (token) {
|
|
491
573
|
lines.push('');
|