aimessage-app 1.0.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/bin/aimessage.js +254 -0
- package/lib/check.js +213 -0
- package/package.json +22 -0
package/bin/aimessage.js
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { execSync } = require("child_process");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const fs = require("fs");
|
|
6
|
+
const os = require("os");
|
|
7
|
+
|
|
8
|
+
const cmd = process.argv[2];
|
|
9
|
+
|
|
10
|
+
if (!cmd || cmd === "setup") {
|
|
11
|
+
setup();
|
|
12
|
+
} else if (cmd === "status") {
|
|
13
|
+
status();
|
|
14
|
+
} else if (cmd === "proofs") {
|
|
15
|
+
proofs();
|
|
16
|
+
} else if (cmd === "uninstall") {
|
|
17
|
+
uninstall();
|
|
18
|
+
} else {
|
|
19
|
+
console.log(`
|
|
20
|
+
aimessage — Your AI asks before it acts.
|
|
21
|
+
|
|
22
|
+
Commands:
|
|
23
|
+
setup Install the Claude Code hook
|
|
24
|
+
status Check if hook is active
|
|
25
|
+
proofs Show recent proofs
|
|
26
|
+
uninstall Remove the hook
|
|
27
|
+
`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function setup() {
|
|
31
|
+
console.log("\n AiMessage Setup\n");
|
|
32
|
+
|
|
33
|
+
// Check macOS
|
|
34
|
+
if (process.platform !== "darwin") {
|
|
35
|
+
console.log(" ✗ AiMessage requires macOS (for iMessage).\n");
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Check Messages app access
|
|
40
|
+
try {
|
|
41
|
+
execSync('osascript -e \'tell application "Messages" to name\'', { stdio: "pipe" });
|
|
42
|
+
console.log(" ✓ Messages app accessible");
|
|
43
|
+
} catch {
|
|
44
|
+
console.log(" ✗ Can't access Messages app. Open Messages and sign in to iMessage first.\n");
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Get phone number
|
|
49
|
+
const readline = require("readline");
|
|
50
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
51
|
+
|
|
52
|
+
rl.question(" Your phone number (for iMessage replies): ", (phone) => {
|
|
53
|
+
phone = phone.trim();
|
|
54
|
+
if (!phone) {
|
|
55
|
+
console.log(" ✗ Phone number required.\n");
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Save config
|
|
60
|
+
const configDir = path.join(os.homedir(), ".aimessage");
|
|
61
|
+
if (!fs.existsSync(configDir)) fs.mkdirSync(configDir, { recursive: true });
|
|
62
|
+
|
|
63
|
+
fs.writeFileSync(path.join(configDir, "config.json"), JSON.stringify({
|
|
64
|
+
phone,
|
|
65
|
+
createdAt: new Date().toISOString(),
|
|
66
|
+
}, null, 2));
|
|
67
|
+
|
|
68
|
+
console.log(" ✓ Phone saved");
|
|
69
|
+
|
|
70
|
+
// Create proof database
|
|
71
|
+
initDb(configDir);
|
|
72
|
+
console.log(" ✓ Proof database created");
|
|
73
|
+
|
|
74
|
+
// Install hook into Claude Code settings
|
|
75
|
+
installHook(configDir);
|
|
76
|
+
console.log(" ✓ Claude Code hook installed");
|
|
77
|
+
|
|
78
|
+
// Test iMessage
|
|
79
|
+
console.log("\n Sending test message...");
|
|
80
|
+
try {
|
|
81
|
+
sendMessage(phone, 'AiMessage is set up. Reply "ok" to confirm.');
|
|
82
|
+
console.log(" ✓ Check your phone for the test message");
|
|
83
|
+
} catch (e) {
|
|
84
|
+
console.log(" ⚠ Couldn't send test message. Make sure Messages is open and signed in.");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
console.log("\n Done. Open Claude Code — every action will ask you first.\n");
|
|
88
|
+
rl.close();
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function installHook(configDir) {
|
|
93
|
+
const hookScript = path.join(configDir, "hook.sh");
|
|
94
|
+
const nodeScript = path.join(configDir, "check.js");
|
|
95
|
+
|
|
96
|
+
// Write the check script (Node.js — handles iMessage + proofs)
|
|
97
|
+
fs.writeFileSync(nodeScript, fs.readFileSync(path.join(__dirname, "..", "lib", "check.js"), "utf8"));
|
|
98
|
+
|
|
99
|
+
// Write the shell hook
|
|
100
|
+
fs.writeFileSync(hookScript, `#!/bin/bash
|
|
101
|
+
node "${nodeScript}"
|
|
102
|
+
`, { mode: 0o755 });
|
|
103
|
+
|
|
104
|
+
// Update Claude Code settings.json
|
|
105
|
+
const claudeDir = path.join(os.homedir(), ".claude");
|
|
106
|
+
if (!fs.existsSync(claudeDir)) fs.mkdirSync(claudeDir, { recursive: true });
|
|
107
|
+
|
|
108
|
+
const settingsPath = path.join(claudeDir, "settings.json");
|
|
109
|
+
let settings = {};
|
|
110
|
+
if (fs.existsSync(settingsPath)) {
|
|
111
|
+
try { settings = JSON.parse(fs.readFileSync(settingsPath, "utf8")); } catch {}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (!settings.hooks) settings.hooks = {};
|
|
115
|
+
if (!settings.hooks.PreToolUse) settings.hooks.PreToolUse = [];
|
|
116
|
+
|
|
117
|
+
// Remove existing aimessage hooks
|
|
118
|
+
settings.hooks.PreToolUse = settings.hooks.PreToolUse.filter(
|
|
119
|
+
h => !JSON.stringify(h).includes("aimessage")
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
// Add new hook
|
|
123
|
+
settings.hooks.PreToolUse.push({
|
|
124
|
+
matcher: "",
|
|
125
|
+
hooks: [{
|
|
126
|
+
type: "command",
|
|
127
|
+
command: hookScript,
|
|
128
|
+
}],
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function initDb(configDir) {
|
|
135
|
+
const dbPath = path.join(configDir, "proofs.db");
|
|
136
|
+
try {
|
|
137
|
+
const Database = require("better-sqlite3");
|
|
138
|
+
const db = new Database(dbPath);
|
|
139
|
+
db.exec(`
|
|
140
|
+
CREATE TABLE IF NOT EXISTS proofs (
|
|
141
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
142
|
+
tool TEXT NOT NULL,
|
|
143
|
+
args TEXT,
|
|
144
|
+
decision TEXT NOT NULL,
|
|
145
|
+
timestamp TEXT NOT NULL,
|
|
146
|
+
prev_hash TEXT,
|
|
147
|
+
hash TEXT
|
|
148
|
+
)
|
|
149
|
+
`);
|
|
150
|
+
db.close();
|
|
151
|
+
} catch {
|
|
152
|
+
// If better-sqlite3 isn't available, we'll create on first use
|
|
153
|
+
fs.writeFileSync(path.join(configDir, "proofs.json"), "[]");
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function sendMessage(phone, text) {
|
|
158
|
+
const escaped = text.replace(/"/g, '\\"');
|
|
159
|
+
execSync(`osascript -e '
|
|
160
|
+
tell application "Messages"
|
|
161
|
+
set targetBuddy to "${phone}"
|
|
162
|
+
set targetService to id of 1st account whose service type = iMessage
|
|
163
|
+
set theBuddy to participant targetBuddy of account id targetService
|
|
164
|
+
send "${escaped}" to theBuddy
|
|
165
|
+
end tell
|
|
166
|
+
'`, { stdio: "pipe" });
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function status() {
|
|
170
|
+
const configDir = path.join(os.homedir(), ".aimessage");
|
|
171
|
+
const configPath = path.join(configDir, "config.json");
|
|
172
|
+
|
|
173
|
+
if (!fs.existsSync(configPath)) {
|
|
174
|
+
console.log("\n AiMessage is not set up. Run: aimessage setup\n");
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
179
|
+
console.log(`\n AiMessage Status`);
|
|
180
|
+
console.log(` Phone: ${config.phone}`);
|
|
181
|
+
console.log(` Since: ${new Date(config.createdAt).toLocaleDateString()}`);
|
|
182
|
+
|
|
183
|
+
// Check hook
|
|
184
|
+
const settingsPath = path.join(os.homedir(), ".claude", "settings.json");
|
|
185
|
+
if (fs.existsSync(settingsPath)) {
|
|
186
|
+
const settings = JSON.parse(fs.readFileSync(settingsPath, "utf8"));
|
|
187
|
+
const hasHook = JSON.stringify(settings.hooks || {}).includes("aimessage");
|
|
188
|
+
console.log(` Hook: ${hasHook ? "active" : "not found"}`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
console.log("");
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function proofs() {
|
|
195
|
+
const configDir = path.join(os.homedir(), ".aimessage");
|
|
196
|
+
|
|
197
|
+
// Try JSON fallback
|
|
198
|
+
const jsonPath = path.join(configDir, "proofs.json");
|
|
199
|
+
if (fs.existsSync(jsonPath)) {
|
|
200
|
+
const proofs = JSON.parse(fs.readFileSync(jsonPath, "utf8"));
|
|
201
|
+
if (proofs.length === 0) {
|
|
202
|
+
console.log("\n No proofs yet.\n");
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
console.log(`\n Recent proofs (${proofs.length} total):\n`);
|
|
206
|
+
proofs.slice(-10).forEach(p => {
|
|
207
|
+
const icon = p.decision === "allow" ? "✓" : "✗";
|
|
208
|
+
console.log(` ${icon} ${p.tool} — ${p.decision} — ${new Date(p.timestamp).toLocaleString()}`);
|
|
209
|
+
});
|
|
210
|
+
console.log("");
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Try SQLite
|
|
215
|
+
try {
|
|
216
|
+
const Database = require("better-sqlite3");
|
|
217
|
+
const db = new Database(path.join(configDir, "proofs.db"));
|
|
218
|
+
const rows = db.prepare("SELECT * FROM proofs ORDER BY id DESC LIMIT 10").all();
|
|
219
|
+
db.close();
|
|
220
|
+
|
|
221
|
+
if (rows.length === 0) {
|
|
222
|
+
console.log("\n No proofs yet.\n");
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
console.log(`\n Recent proofs:\n`);
|
|
226
|
+
rows.reverse().forEach(r => {
|
|
227
|
+
const icon = r.decision === "allow" ? "✓" : "✗";
|
|
228
|
+
console.log(` ${icon} ${r.tool} — ${r.decision} — ${new Date(r.timestamp).toLocaleString()}`);
|
|
229
|
+
});
|
|
230
|
+
console.log("");
|
|
231
|
+
} catch {
|
|
232
|
+
console.log("\n No proofs found.\n");
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function uninstall() {
|
|
237
|
+
// Remove hook from settings
|
|
238
|
+
const settingsPath = path.join(os.homedir(), ".claude", "settings.json");
|
|
239
|
+
if (fs.existsSync(settingsPath)) {
|
|
240
|
+
try {
|
|
241
|
+
const settings = JSON.parse(fs.readFileSync(settingsPath, "utf8"));
|
|
242
|
+
if (settings.hooks?.PreToolUse) {
|
|
243
|
+
settings.hooks.PreToolUse = settings.hooks.PreToolUse.filter(
|
|
244
|
+
h => !JSON.stringify(h).includes("aimessage")
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
248
|
+
console.log("\n ✓ Hook removed from Claude Code");
|
|
249
|
+
} catch {}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
console.log(" ✓ AiMessage uninstalled\n");
|
|
253
|
+
console.log(" Your proofs are still in ~/.aimessage/\n");
|
|
254
|
+
}
|
package/lib/check.js
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// AiMessage — PreToolUse hook for Claude Code
|
|
4
|
+
// Reads tool call from stdin, sends iMessage, waits for reply, allows or denies.
|
|
5
|
+
|
|
6
|
+
const { execSync } = require("child_process");
|
|
7
|
+
const fs = require("fs");
|
|
8
|
+
const path = require("path");
|
|
9
|
+
const os = require("os");
|
|
10
|
+
const crypto = require("crypto");
|
|
11
|
+
|
|
12
|
+
const CONFIG_DIR = path.join(os.homedir(), ".aimessage");
|
|
13
|
+
const CONFIG_PATH = path.join(CONFIG_DIR, "config.json");
|
|
14
|
+
const PROOFS_PATH = path.join(CONFIG_DIR, "proofs.json");
|
|
15
|
+
const ALLOWED_PATH = path.join(CONFIG_DIR, "allowed.json");
|
|
16
|
+
|
|
17
|
+
// Read-only tools that don't need approval
|
|
18
|
+
const SKIP_TOOLS = new Set([
|
|
19
|
+
"Read", "Glob", "Grep", "WebSearch", "WebFetch",
|
|
20
|
+
"mcp__Claude_Preview__preview_screenshot",
|
|
21
|
+
"mcp__Claude_Preview__preview_snapshot",
|
|
22
|
+
"mcp__Claude_Preview__preview_inspect",
|
|
23
|
+
"mcp__Claude_Preview__preview_logs",
|
|
24
|
+
"mcp__Claude_Preview__preview_console_logs",
|
|
25
|
+
"mcp__Claude_Preview__preview_network",
|
|
26
|
+
"mcp__Claude_Preview__preview_list",
|
|
27
|
+
"TaskOutput",
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
async function main() {
|
|
31
|
+
// Read stdin (Claude Code sends JSON with tool info)
|
|
32
|
+
let input = "";
|
|
33
|
+
for await (const chunk of process.stdin) input += chunk;
|
|
34
|
+
|
|
35
|
+
let data;
|
|
36
|
+
try { data = JSON.parse(input); } catch { process.exit(0); } // Can't parse = allow
|
|
37
|
+
|
|
38
|
+
const tool = data.tool_name || data.tool || "unknown";
|
|
39
|
+
const args = data.tool_input || data.input || {};
|
|
40
|
+
|
|
41
|
+
// Skip read-only tools
|
|
42
|
+
if (SKIP_TOOLS.has(tool)) process.exit(0);
|
|
43
|
+
|
|
44
|
+
// Load config
|
|
45
|
+
if (!fs.existsSync(CONFIG_PATH)) {
|
|
46
|
+
process.stderr.write("AiMessage not set up. Run: aimessage setup\n");
|
|
47
|
+
process.exit(0); // Allow if not configured (don't block)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const config = JSON.parse(fs.readFileSync(CONFIG_PATH, "utf8"));
|
|
51
|
+
const phone = config.phone;
|
|
52
|
+
|
|
53
|
+
// Check if already allowed
|
|
54
|
+
const allowed = loadAllowed();
|
|
55
|
+
if (allowed.has(tool)) {
|
|
56
|
+
recordProof(tool, args, "allow", "previously approved");
|
|
57
|
+
process.exit(0);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Build human-readable message
|
|
61
|
+
const summary = buildSummary(tool, args);
|
|
62
|
+
const msg = `🔒 ${summary}\n\nReply: YES (always) / ONCE / NO`;
|
|
63
|
+
|
|
64
|
+
// Send iMessage
|
|
65
|
+
try {
|
|
66
|
+
sendMessage(phone, msg);
|
|
67
|
+
} catch (e) {
|
|
68
|
+
process.stderr.write(`AiMessage: couldn't send iMessage: ${e.message}\n`);
|
|
69
|
+
process.exit(0); // Allow if can't message (don't block work)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Wait for reply (poll Messages for up to 120 seconds)
|
|
73
|
+
const reply = await waitForReply(phone, 120);
|
|
74
|
+
|
|
75
|
+
if (!reply) {
|
|
76
|
+
// Timeout — deny
|
|
77
|
+
recordProof(tool, args, "deny", "timeout");
|
|
78
|
+
sendMessage(phone, "⏰ Timed out. Action denied.");
|
|
79
|
+
process.stderr.write("AiMessage: timed out waiting for reply. Denied.\n");
|
|
80
|
+
process.exit(2);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const answer = reply.trim().toUpperCase();
|
|
84
|
+
|
|
85
|
+
if (answer === "YES" || answer === "Y") {
|
|
86
|
+
// Always allow this tool
|
|
87
|
+
allowed.add(tool);
|
|
88
|
+
saveAllowed(allowed);
|
|
89
|
+
recordProof(tool, args, "allow", "always");
|
|
90
|
+
sendMessage(phone, "✅ Allowed (always).");
|
|
91
|
+
process.exit(0);
|
|
92
|
+
} else if (answer === "ONCE" || answer === "1") {
|
|
93
|
+
// Allow once
|
|
94
|
+
recordProof(tool, args, "allow", "once");
|
|
95
|
+
sendMessage(phone, "✅ Allowed (once).");
|
|
96
|
+
process.exit(0);
|
|
97
|
+
} else {
|
|
98
|
+
// Deny
|
|
99
|
+
recordProof(tool, args, "deny", "denied by user");
|
|
100
|
+
sendMessage(phone, "❌ Denied.");
|
|
101
|
+
process.stderr.write("AiMessage: denied by user.\n");
|
|
102
|
+
process.exit(2);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function buildSummary(tool, args) {
|
|
107
|
+
const name = tool.replace(/[_-]/g, " ");
|
|
108
|
+
|
|
109
|
+
if (args.file_path || args.path) return `${name}: ${args.file_path || args.path}`;
|
|
110
|
+
if (args.command) return `${name}: $ ${String(args.command).slice(0, 80)}`;
|
|
111
|
+
if (args.url) return `${name}: ${args.url}`;
|
|
112
|
+
if (args.query) return `${name}: "${String(args.query).slice(0, 60)}"`;
|
|
113
|
+
if (args.content && args.file_path) return `${name}: write to ${args.file_path}`;
|
|
114
|
+
|
|
115
|
+
return name;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function sendMessage(phone, text) {
|
|
119
|
+
const escaped = text.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
120
|
+
execSync(`osascript <<'SCPT'
|
|
121
|
+
tell application "Messages"
|
|
122
|
+
set targetService to id of 1st account whose service type = iMessage
|
|
123
|
+
set theBuddy to participant "${phone}" of account id targetService
|
|
124
|
+
send "${escaped}" to theBuddy
|
|
125
|
+
end tell
|
|
126
|
+
SCPT`, { stdio: "pipe", timeout: 10000 });
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function waitForReply(phone, timeoutSeconds) {
|
|
130
|
+
return new Promise((resolve) => {
|
|
131
|
+
const start = Date.now();
|
|
132
|
+
const deadline = start + timeoutSeconds * 1000;
|
|
133
|
+
|
|
134
|
+
// Get current message count to know baseline
|
|
135
|
+
const baseline = getLatestMessage(phone);
|
|
136
|
+
const baselineText = baseline || "";
|
|
137
|
+
const baselineTime = Date.now();
|
|
138
|
+
|
|
139
|
+
const poll = setInterval(() => {
|
|
140
|
+
if (Date.now() > deadline) {
|
|
141
|
+
clearInterval(poll);
|
|
142
|
+
resolve(null);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
const latest = getLatestMessage(phone);
|
|
148
|
+
// Check if it's a new message (different from baseline)
|
|
149
|
+
if (latest && latest !== baselineText) {
|
|
150
|
+
const upper = latest.trim().toUpperCase();
|
|
151
|
+
if (["YES", "Y", "NO", "N", "ONCE", "1"].includes(upper)) {
|
|
152
|
+
clearInterval(poll);
|
|
153
|
+
resolve(latest);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
} catch {}
|
|
158
|
+
}, 2000); // Poll every 2 seconds
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function getLatestMessage(phone) {
|
|
163
|
+
try {
|
|
164
|
+
// Query Messages database directly for the most recent incoming message
|
|
165
|
+
const dbPath = path.join(os.homedir(), "Library", "Messages", "chat.db");
|
|
166
|
+
const result = execSync(`sqlite3 "${dbPath}" "
|
|
167
|
+
SELECT m.text FROM message m
|
|
168
|
+
JOIN handle h ON m.handle_id = h.ROWID
|
|
169
|
+
WHERE h.id LIKE '%${phone.replace(/[^0-9+]/g, "")}%'
|
|
170
|
+
AND m.is_from_me = 0
|
|
171
|
+
ORDER BY m.date DESC LIMIT 1;
|
|
172
|
+
"`, { stdio: "pipe", timeout: 5000 }).toString().trim();
|
|
173
|
+
return result || null;
|
|
174
|
+
} catch {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function loadAllowed() {
|
|
180
|
+
if (!fs.existsSync(ALLOWED_PATH)) return new Set();
|
|
181
|
+
try {
|
|
182
|
+
return new Set(JSON.parse(fs.readFileSync(ALLOWED_PATH, "utf8")));
|
|
183
|
+
} catch { return new Set(); }
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function saveAllowed(set) {
|
|
187
|
+
fs.writeFileSync(ALLOWED_PATH, JSON.stringify([...set], null, 2));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function recordProof(tool, args, decision, reason) {
|
|
191
|
+
let proofs = [];
|
|
192
|
+
if (fs.existsSync(PROOFS_PATH)) {
|
|
193
|
+
try { proofs = JSON.parse(fs.readFileSync(PROOFS_PATH, "utf8")); } catch {}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const prevHash = proofs.length > 0 ? proofs[proofs.length - 1].hash : null;
|
|
197
|
+
const payload = JSON.stringify({ tool, decision, reason, timestamp: new Date().toISOString(), prevHash });
|
|
198
|
+
const hash = crypto.createHash("sha256").update(payload).digest("hex");
|
|
199
|
+
|
|
200
|
+
proofs.push({
|
|
201
|
+
tool,
|
|
202
|
+
args: typeof args === "object" ? JSON.stringify(args).slice(0, 500) : null,
|
|
203
|
+
decision,
|
|
204
|
+
reason,
|
|
205
|
+
timestamp: new Date().toISOString(),
|
|
206
|
+
prevHash,
|
|
207
|
+
hash,
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
fs.writeFileSync(PROOFS_PATH, JSON.stringify(proofs, null, 2));
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
main().catch(() => process.exit(0));
|
package/package.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "aimessage-app",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Your AI asks before it acts. Approve or deny AI actions via iMessage.",
|
|
5
|
+
"bin": {
|
|
6
|
+
"aimessage": "./bin/aimessage.js"
|
|
7
|
+
},
|
|
8
|
+
"files": ["bin/", "lib/", "README.md"],
|
|
9
|
+
"keywords": ["ai", "imessage", "claude", "occ", "approval", "governance"],
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"author": "Mike Argento",
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "https://github.com/mikeargento/occ"
|
|
15
|
+
},
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=18"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"better-sqlite3": "^11.0.0"
|
|
21
|
+
}
|
|
22
|
+
}
|