@hasna/coders 0.0.3 → 0.0.4

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/cli.mjs CHANGED
@@ -58429,7 +58429,7 @@ var init_client = __esm({
58429
58429
  "Content-Type": "application/json",
58430
58430
  "anthropic-version": "2023-06-01",
58431
58431
  "anthropic-beta": BETA_HEADERS.join(","),
58432
- "User-Agent": `coders/${"0.0.3"}`
58432
+ "User-Agent": `coders/${"0.0.4"}`
58433
58433
  };
58434
58434
  if (key.isOAuth) {
58435
58435
  headers["Authorization"] = `Bearer ${key.apiKey}`;
@@ -59167,6 +59167,270 @@ var init_permissions = __esm({
59167
59167
  }
59168
59168
  });
59169
59169
 
59170
+ // src/db/index.ts
59171
+ import { join as join2 } from "path";
59172
+ import { existsSync as existsSync4, mkdirSync as mkdirSync3 } from "fs";
59173
+ function getDbPath() {
59174
+ const dir = getConfigDir();
59175
+ if (!existsSync4(dir)) mkdirSync3(dir, { recursive: true });
59176
+ return join2(dir, "coders.db");
59177
+ }
59178
+ function getDb() {
59179
+ if (_db) return _db;
59180
+ const dbPath = getDbPath();
59181
+ try {
59182
+ const { Database } = __require("bun:sqlite");
59183
+ _db = new Database(dbPath);
59184
+ } catch {
59185
+ try {
59186
+ const BetterSqlite3 = __require("better-sqlite3");
59187
+ _db = new BetterSqlite3(dbPath);
59188
+ } catch {
59189
+ console.warn("[db] No SQLite available \u2014 using in-memory fallback");
59190
+ _db = createInMemoryDb();
59191
+ initSchema(_db);
59192
+ return _db;
59193
+ }
59194
+ }
59195
+ try {
59196
+ _db.exec("PRAGMA journal_mode=WAL");
59197
+ } catch {
59198
+ }
59199
+ try {
59200
+ _db.exec("PRAGMA foreign_keys=ON");
59201
+ } catch {
59202
+ }
59203
+ initSchema(_db);
59204
+ return _db;
59205
+ }
59206
+ function initSchema(db) {
59207
+ db.exec(`
59208
+ -- Sessions
59209
+ CREATE TABLE IF NOT EXISTS sessions (
59210
+ id TEXT PRIMARY KEY,
59211
+ device_id TEXT NOT NULL,
59212
+ project_dir TEXT,
59213
+ original_cwd TEXT,
59214
+ model TEXT,
59215
+ app_version TEXT,
59216
+ build_time TEXT,
59217
+ fingerprint TEXT, -- JSON
59218
+ metadata TEXT, -- JSON
59219
+ created_at TEXT DEFAULT (datetime('now')),
59220
+ updated_at TEXT DEFAULT (datetime('now'))
59221
+ );
59222
+
59223
+ -- Messages (conversation history)
59224
+ CREATE TABLE IF NOT EXISTS messages (
59225
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
59226
+ session_id TEXT NOT NULL REFERENCES sessions(id),
59227
+ role TEXT NOT NULL CHECK(role IN ('user','assistant','system')),
59228
+ content TEXT NOT NULL,
59229
+ tool_uses TEXT, -- JSON array of tool use displays
59230
+ thinking TEXT,
59231
+ duration_ms REAL,
59232
+ tokens_in INTEGER DEFAULT 0,
59233
+ tokens_out INTEGER DEFAULT 0,
59234
+ cost_usd REAL DEFAULT 0,
59235
+ created_at TEXT DEFAULT (datetime('now'))
59236
+ );
59237
+ CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id);
59238
+
59239
+ -- File history (tracks reads per session)
59240
+ CREATE TABLE IF NOT EXISTS file_history (
59241
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
59242
+ session_id TEXT NOT NULL,
59243
+ file_path TEXT NOT NULL,
59244
+ content_hash TEXT,
59245
+ byte_size INTEGER,
59246
+ line_count INTEGER,
59247
+ read_at TEXT DEFAULT (datetime('now')),
59248
+ UNIQUE(session_id, file_path)
59249
+ );
59250
+
59251
+ -- File checkpoints (for /rewind)
59252
+ CREATE TABLE IF NOT EXISTS checkpoints (
59253
+ id TEXT PRIMARY KEY,
59254
+ session_id TEXT NOT NULL,
59255
+ file_path TEXT NOT NULL,
59256
+ original_content TEXT NOT NULL,
59257
+ edit_operation TEXT, -- JSON {old_string, new_string}
59258
+ created_at TEXT DEFAULT (datetime('now'))
59259
+ );
59260
+ CREATE INDEX IF NOT EXISTS idx_checkpoints_session_file ON checkpoints(session_id, file_path);
59261
+
59262
+ -- Tasks (replaces in-memory fallback for @hasna/todos)
59263
+ CREATE TABLE IF NOT EXISTS tasks (
59264
+ id TEXT PRIMARY KEY,
59265
+ subject TEXT NOT NULL,
59266
+ description TEXT DEFAULT '',
59267
+ status TEXT DEFAULT 'pending' CHECK(status IN ('pending','in_progress','completed','failed','cancelled')),
59268
+ active_form TEXT,
59269
+ owner TEXT,
59270
+ blocks TEXT DEFAULT '[]', -- JSON array of task IDs
59271
+ blocked_by TEXT DEFAULT '[]', -- JSON array of task IDs
59272
+ metadata TEXT DEFAULT '{}', -- JSON
59273
+ created_at TEXT DEFAULT (datetime('now')),
59274
+ updated_at TEXT DEFAULT (datetime('now'))
59275
+ );
59276
+
59277
+ -- Config (replaces JSON files)
59278
+ CREATE TABLE IF NOT EXISTS config (
59279
+ key TEXT PRIMARY KEY,
59280
+ value TEXT, -- JSON
59281
+ scope TEXT DEFAULT 'user' CHECK(scope IN ('user','project','local','global')),
59282
+ updated_at TEXT DEFAULT (datetime('now'))
59283
+ );
59284
+
59285
+ -- Memories (replaces in-memory fallback for @hasna/mementos)
59286
+ CREATE TABLE IF NOT EXISTS memories (
59287
+ id TEXT PRIMARY KEY,
59288
+ key TEXT UNIQUE NOT NULL,
59289
+ value TEXT NOT NULL,
59290
+ scope TEXT DEFAULT 'shared' CHECK(scope IN ('global','shared','private')),
59291
+ category TEXT DEFAULT 'knowledge',
59292
+ importance INTEGER DEFAULT 5 CHECK(importance BETWEEN 1 AND 10),
59293
+ tags TEXT DEFAULT '[]', -- JSON array
59294
+ version INTEGER DEFAULT 1,
59295
+ created_at TEXT DEFAULT (datetime('now')),
59296
+ updated_at TEXT DEFAULT (datetime('now'))
59297
+ );
59298
+
59299
+ -- Teams
59300
+ CREATE TABLE IF NOT EXISTS teams (
59301
+ name TEXT PRIMARY KEY,
59302
+ description TEXT,
59303
+ task_list_id TEXT,
59304
+ created_at TEXT DEFAULT (datetime('now'))
59305
+ );
59306
+
59307
+ -- Team members
59308
+ CREATE TABLE IF NOT EXISTS team_members (
59309
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
59310
+ team_name TEXT NOT NULL REFERENCES teams(name),
59311
+ agent_name TEXT NOT NULL,
59312
+ role TEXT,
59313
+ status TEXT DEFAULT 'idle',
59314
+ current_task TEXT,
59315
+ UNIQUE(team_name, agent_name)
59316
+ );
59317
+
59318
+ -- Team messages
59319
+ CREATE TABLE IF NOT EXISTS team_messages (
59320
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
59321
+ from_agent TEXT NOT NULL,
59322
+ to_agent TEXT NOT NULL,
59323
+ team_name TEXT,
59324
+ content TEXT NOT NULL,
59325
+ is_read INTEGER DEFAULT 0,
59326
+ is_blocking INTEGER DEFAULT 0,
59327
+ created_at TEXT DEFAULT (datetime('now'))
59328
+ );
59329
+ CREATE INDEX IF NOT EXISTS idx_team_msgs_to ON team_messages(to_agent, is_read);
59330
+
59331
+ -- Permissions (persisted allow/deny rules)
59332
+ CREATE TABLE IF NOT EXISTS permissions (
59333
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
59334
+ tool_name TEXT,
59335
+ command_pattern TEXT,
59336
+ path_pattern TEXT,
59337
+ behavior TEXT NOT NULL CHECK(behavior IN ('allow','deny')),
59338
+ scope TEXT DEFAULT 'session',
59339
+ created_at TEXT DEFAULT (datetime('now'))
59340
+ );
59341
+
59342
+ -- MCP servers
59343
+ CREATE TABLE IF NOT EXISTS mcp_servers (
59344
+ name TEXT PRIMARY KEY,
59345
+ command TEXT,
59346
+ args TEXT, -- JSON array
59347
+ env TEXT, -- JSON object
59348
+ url TEXT,
59349
+ transport TEXT DEFAULT 'stdio',
59350
+ scope TEXT DEFAULT 'user',
59351
+ enabled INTEGER DEFAULT 1,
59352
+ created_at TEXT DEFAULT (datetime('now'))
59353
+ );
59354
+
59355
+ -- Metrics (per-turn tracking)
59356
+ CREATE TABLE IF NOT EXISTS metrics (
59357
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
59358
+ session_id TEXT NOT NULL,
59359
+ turn_index INTEGER,
59360
+ tokens_in INTEGER DEFAULT 0,
59361
+ tokens_out INTEGER DEFAULT 0,
59362
+ cost_usd REAL DEFAULT 0,
59363
+ api_duration_ms REAL DEFAULT 0,
59364
+ tool_duration_ms REAL DEFAULT 0,
59365
+ hook_duration_ms REAL DEFAULT 0,
59366
+ tool_count INTEGER DEFAULT 0,
59367
+ model TEXT,
59368
+ created_at TEXT DEFAULT (datetime('now'))
59369
+ );
59370
+
59371
+ -- Audit log (for security \u2014 tracks all tool executions)
59372
+ CREATE TABLE IF NOT EXISTS audit_log (
59373
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
59374
+ session_id TEXT,
59375
+ tool_name TEXT NOT NULL,
59376
+ input_summary TEXT,
59377
+ result_summary TEXT,
59378
+ exit_code INTEGER,
59379
+ duration_ms REAL,
59380
+ was_allowed INTEGER DEFAULT 1,
59381
+ created_at TEXT DEFAULT (datetime('now'))
59382
+ );
59383
+ `);
59384
+ }
59385
+ function dbRun(sql, params = []) {
59386
+ const db = getDb();
59387
+ try {
59388
+ const stmt = db.prepare(sql);
59389
+ return stmt.run(...params);
59390
+ } catch (e) {
59391
+ try {
59392
+ return db.run(sql, params);
59393
+ } catch {
59394
+ throw e;
59395
+ }
59396
+ }
59397
+ }
59398
+ function createInMemoryDb() {
59399
+ const tables = /* @__PURE__ */ new Map();
59400
+ return {
59401
+ exec(sql) {
59402
+ const matches = sql.matchAll(/CREATE TABLE IF NOT EXISTS (\w+)/g);
59403
+ for (const m of matches) {
59404
+ if (!tables.has(m[1])) tables.set(m[1], /* @__PURE__ */ new Map());
59405
+ }
59406
+ },
59407
+ prepare(sql) {
59408
+ return {
59409
+ run(...params) {
59410
+ return { changes: 0 };
59411
+ },
59412
+ get(...params) {
59413
+ return void 0;
59414
+ },
59415
+ all(...params) {
59416
+ return [];
59417
+ }
59418
+ };
59419
+ },
59420
+ close() {
59421
+ tables.clear();
59422
+ }
59423
+ };
59424
+ }
59425
+ var _db;
59426
+ var init_db = __esm({
59427
+ "src/db/index.ts"() {
59428
+ "use strict";
59429
+ init_paths();
59430
+ _db = null;
59431
+ }
59432
+ });
59433
+
59170
59434
  // src/core/constants.ts
59171
59435
  var BASH_TOOL, READ_TOOL, EDIT_TOOL, WRITE_TOOL, GLOB_TOOL, GREP_TOOL, DEFAULT_BASH_TIMEOUT_MS, MAX_BASH_TIMEOUT_MS, DEFAULT_READ_LINE_LIMIT, DEFAULT_MAX_RESULT_SIZE_CHARS;
59172
59436
  var init_constants = __esm({
@@ -59187,6 +59451,23 @@ var init_constants = __esm({
59187
59451
 
59188
59452
  // src/tools/builtin/bash.ts
59189
59453
  import { spawn } from "child_process";
59454
+ function isDangerousCommand(command) {
59455
+ for (const { pattern, reason } of DANGEROUS_PATTERNS) {
59456
+ if (pattern.test(command)) {
59457
+ return { dangerous: true, reason };
59458
+ }
59459
+ }
59460
+ return { dangerous: false };
59461
+ }
59462
+ function auditLog(command, exitCode, durationMs, sessionId) {
59463
+ try {
59464
+ dbRun(
59465
+ "INSERT INTO audit_log (session_id, tool_name, input_summary, result_summary, exit_code, duration_ms, was_allowed) VALUES (?, 'Bash', ?, ?, ?, ?, 1)",
59466
+ [sessionId ?? null, command.slice(0, 500), exitCode === 0 ? "success" : `exit ${exitCode}`, exitCode, durationMs]
59467
+ );
59468
+ } catch {
59469
+ }
59470
+ }
59190
59471
  function isReadOnlyCommand(command) {
59191
59472
  const trimmed = command.trim();
59192
59473
  if (!trimmed) return false;
@@ -59282,12 +59563,28 @@ function truncateOutput(output, maxChars = DEFAULT_MAX_RESULT_SIZE_CHARS) {
59282
59563
 
59283
59564
  ` + output.slice(-half);
59284
59565
  }
59285
- var BashInputSchema, BashOutputSchema, READ_ONLY_COMMANDS, READ_ONLY_KEYWORDS, GIT_READ_ONLY_SUBCOMMANDS, NPM_READ_ONLY_SUBCOMMANDS, DOCKER_READ_ONLY_SUBCOMMANDS, backgroundTasks, nextBgId, bashTool, BASH_PROMPT;
59566
+ var DANGEROUS_PATTERNS, BashInputSchema, BashOutputSchema, READ_ONLY_COMMANDS, READ_ONLY_KEYWORDS, GIT_READ_ONLY_SUBCOMMANDS, NPM_READ_ONLY_SUBCOMMANDS, DOCKER_READ_ONLY_SUBCOMMANDS, backgroundTasks, nextBgId, bashTool, BASH_PROMPT;
59286
59567
  var init_bash = __esm({
59287
59568
  "src/tools/builtin/bash.ts"() {
59288
59569
  "use strict";
59289
59570
  init_zod();
59290
59571
  init_constants();
59572
+ init_db();
59573
+ DANGEROUS_PATTERNS = [
59574
+ { pattern: /\brm\s+-rf\s+\/\s*$/, reason: "Recursive delete of root filesystem" },
59575
+ { pattern: /\brm\s+-rf\s+~\//, reason: "Recursive delete of home directory" },
59576
+ { pattern: /\brm\s+-rf\s+\*/, reason: "Recursive delete with wildcard" },
59577
+ { pattern: /\bmkfs\b/, reason: "Format filesystem" },
59578
+ { pattern: /\bdd\s+.*of=\/dev\//, reason: "Direct write to device" },
59579
+ { pattern: />\s*\/dev\/sd[a-z]/, reason: "Redirect to disk device" },
59580
+ { pattern: /\b:(){ :\|:& };:/, reason: "Fork bomb" },
59581
+ { pattern: /\bchmod\s+-R\s+777\s+\//, reason: "Recursive chmod 777 on root" },
59582
+ { pattern: /\bchown\s+-R\s+.*\s+\//, reason: "Recursive chown on root" },
59583
+ { pattern: /\bcurl\s.*\|\s*sh\b/, reason: "Pipe remote script to shell" },
59584
+ { pattern: /\bwget\s.*\|\s*sh\b/, reason: "Pipe remote script to shell" },
59585
+ { pattern: /\b\/etc\/passwd\b/, reason: "Access to passwd file" },
59586
+ { pattern: /\b\/etc\/shadow\b/, reason: "Access to shadow file" }
59587
+ ];
59291
59588
  BashInputSchema = external_exports.strictObject({
59292
59589
  command: external_exports.string().describe("The command to execute"),
59293
59590
  description: external_exports.string().optional().describe("Description of what the command does"),
@@ -59507,6 +59804,14 @@ var init_bash = __esm({
59507
59804
  if (input.timeout !== void 0 && input.timeout > MAX_BASH_TIMEOUT_MS) {
59508
59805
  return { result: false, message: `Timeout exceeds maximum of ${MAX_BASH_TIMEOUT_MS}ms`, errorCode: 2 };
59509
59806
  }
59807
+ const danger = isDangerousCommand(input.command);
59808
+ if (danger.dangerous) {
59809
+ try {
59810
+ dbRun("INSERT INTO audit_log (tool_name, input_summary, was_allowed) VALUES ('Bash', ?, 0)", [input.command.slice(0, 200)]);
59811
+ } catch {
59812
+ }
59813
+ return { result: false, message: `Blocked dangerous command: ${danger.reason}`, errorCode: 3 };
59814
+ }
59510
59815
  return { result: true };
59511
59816
  },
59512
59817
  async checkPermissions(input, context) {
@@ -59587,6 +59892,7 @@ var init_bash = __esm({
59587
59892
  }
59588
59893
  stdout = truncateOutput(stdout);
59589
59894
  stderr = truncateOutput(stderr);
59895
+ auditLog(command, code, durationMs);
59590
59896
  resolve7({
59591
59897
  data: {
59592
59898
  stdout,
@@ -59654,7 +59960,7 @@ Instructions:
59654
59960
  });
59655
59961
 
59656
59962
  // src/tools/builtin/read.ts
59657
- import { readFileSync as readFileSync3, statSync, existsSync as existsSync4 } from "fs";
59963
+ import { readFileSync as readFileSync3, statSync, existsSync as existsSync5 } from "fs";
59658
59964
  import { extname, isAbsolute, resolve as resolve2 } from "path";
59659
59965
  function hasFileBeenRead(filePath) {
59660
59966
  return readFiles.has(resolve2(filePath));
@@ -59835,7 +60141,7 @@ var init_read = __esm({
59835
60141
  return { result: false, message: "file_path is required", errorCode: 1 };
59836
60142
  }
59837
60143
  const resolved = resolvePath(input.file_path);
59838
- if (!existsSync4(resolved)) {
60144
+ if (!existsSync5(resolved)) {
59839
60145
  return { result: false, message: `File does not exist: ${input.file_path}`, errorCode: 2 };
59840
60146
  }
59841
60147
  try {
@@ -59889,8 +60195,9 @@ Usage:
59889
60195
  });
59890
60196
 
59891
60197
  // src/tools/builtin/edit.ts
59892
- import { readFileSync as readFileSync4, writeFileSync as writeFileSync2, existsSync as existsSync5 } from "fs";
60198
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync2, existsSync as existsSync6 } from "fs";
59893
60199
  import { resolve as resolve3, isAbsolute as isAbsolute2 } from "path";
60200
+ import { randomUUID } from "crypto";
59894
60201
  function resolvePath2(filePath) {
59895
60202
  if (isAbsolute2(filePath)) return filePath;
59896
60203
  return resolve3(process.cwd(), filePath);
@@ -59904,36 +60211,64 @@ function countOccurrences(text, search) {
59904
60211
  }
59905
60212
  return count;
59906
60213
  }
59907
- function generateSimpleDiff(oldContent, newContent, filePath) {
60214
+ function generateUnifiedDiff(oldContent, newContent, filePath) {
59908
60215
  const oldLines = oldContent.split("\n");
59909
60216
  const newLines = newContent.split("\n");
59910
- const lines = [`--- a/${filePath}`, `+++ b/${filePath}`];
59911
- let i = 0;
59912
- let j = 0;
59913
- while (i < oldLines.length || j < newLines.length) {
59914
- if (i < oldLines.length && j < newLines.length && oldLines[i] === newLines[j]) {
59915
- i++;
59916
- j++;
59917
- } else {
59918
- const contextStart = Math.max(0, i - 2);
59919
- lines.push(`@@ -${contextStart + 1} @@`);
59920
- while (i < oldLines.length && (j >= newLines.length || oldLines[i] !== newLines[j])) {
59921
- lines.push(`-${oldLines[i]}`);
59922
- i++;
59923
- if (lines.length > 50) break;
59924
- }
59925
- while (j < newLines.length && (i >= oldLines.length || oldLines[i] !== newLines[j])) {
59926
- lines.push(`+${newLines[j]}`);
59927
- j++;
59928
- if (lines.length > 100) break;
59929
- }
59930
- if (lines.length > 100) {
59931
- lines.push("... (diff truncated)");
59932
- break;
60217
+ const CONTEXT = 3;
60218
+ const result = [`--- a/${filePath}`, `+++ b/${filePath}`];
60219
+ const hunks = findDiffHunks(oldLines, newLines, CONTEXT);
60220
+ for (const hunk of hunks) {
60221
+ const { oldStart, oldCount, newStart, newCount, lines } = hunk;
60222
+ result.push(`@@ -${oldStart + 1},${oldCount} +${newStart + 1},${newCount} @@`);
60223
+ result.push(...lines);
60224
+ }
60225
+ return result.length > 2 ? result.join("\n") : "";
60226
+ }
60227
+ function findDiffHunks(oldLines, newLines, context) {
60228
+ const hunks = [];
60229
+ let oi = 0, ni = 0;
60230
+ while (oi < oldLines.length || ni < newLines.length) {
60231
+ if (oi < oldLines.length && ni < newLines.length && oldLines[oi] === newLines[ni]) {
60232
+ oi++;
60233
+ ni++;
60234
+ continue;
60235
+ }
60236
+ const changeOldStart = Math.max(0, oi - context);
60237
+ const changeNewStart = Math.max(0, ni - context);
60238
+ const hunkLines = [];
60239
+ for (let c = Math.max(0, oi - context); c < oi; c++) {
60240
+ hunkLines.push(` ${oldLines[c]}`);
60241
+ }
60242
+ const removedStart = oi;
60243
+ while (oi < oldLines.length && (ni >= newLines.length || oldLines[oi] !== newLines[ni])) {
60244
+ hunkLines.push(`-${oldLines[oi]}`);
60245
+ oi++;
60246
+ if (hunkLines.length > 100) break;
60247
+ }
60248
+ const addedStart = ni;
60249
+ while (ni < newLines.length && (oi >= oldLines.length || oldLines[oi] !== newLines[ni])) {
60250
+ hunkLines.push(`+${newLines[ni]}`);
60251
+ ni++;
60252
+ if (hunkLines.length > 200) break;
60253
+ }
60254
+ const afterEnd = Math.min(oi + context, oldLines.length);
60255
+ for (let c = oi; c < afterEnd && c < oldLines.length && ni + (c - oi) < newLines.length; c++) {
60256
+ if (oldLines[c] === newLines[ni + (c - oi)]) {
60257
+ hunkLines.push(` ${oldLines[c]}`);
59933
60258
  }
59934
60259
  }
60260
+ const oldCount = oi - changeOldStart + Math.min(context, oldLines.length - oi);
60261
+ const newCount = ni - changeNewStart + Math.min(context, newLines.length - ni);
60262
+ hunks.push({
60263
+ oldStart: changeOldStart,
60264
+ oldCount: Math.max(oldCount, 1),
60265
+ newStart: changeNewStart,
60266
+ newCount: Math.max(newCount, 1),
60267
+ lines: hunkLines
60268
+ });
60269
+ if (hunks.length > 20) break;
59935
60270
  }
59936
- return lines.length > 2 ? lines.join("\n") : "";
60271
+ return hunks;
59937
60272
  }
59938
60273
  var EditInputSchema, EditOutputSchema, editTool, EDIT_PROMPT;
59939
60274
  var init_edit = __esm({
@@ -59942,6 +60277,7 @@ var init_edit = __esm({
59942
60277
  init_zod();
59943
60278
  init_constants();
59944
60279
  init_read();
60280
+ init_db();
59945
60281
  EditInputSchema = external_exports.strictObject({
59946
60282
  file_path: external_exports.string().describe("The absolute path to the file to modify"),
59947
60283
  old_string: external_exports.string().describe("The text to replace"),
@@ -59999,7 +60335,7 @@ var init_edit = __esm({
59999
60335
  return { result: false, message: "old_string and new_string must be different", errorCode: 2 };
60000
60336
  }
60001
60337
  const resolved = resolvePath2(input.file_path);
60002
- if (!existsSync5(resolved)) {
60338
+ if (!existsSync6(resolved)) {
60003
60339
  return { result: false, message: `File does not exist: ${input.file_path}`, errorCode: 3 };
60004
60340
  }
60005
60341
  if (!hasFileBeenRead(resolved)) {
@@ -60055,9 +60391,17 @@ var init_edit = __esm({
60055
60391
  newContent = originalContent.slice(0, idx) + input.new_string + originalContent.slice(idx + input.old_string.length);
60056
60392
  replacements = 1;
60057
60393
  }
60394
+ try {
60395
+ const cpId = randomUUID().slice(0, 8);
60396
+ dbRun(
60397
+ "INSERT INTO checkpoints (id, session_id, file_path, original_content, edit_operation) VALUES (?, ?, ?, ?, ?)",
60398
+ [cpId, "current", resolved, originalContent, JSON.stringify({ old_string: input.old_string, new_string: input.new_string })]
60399
+ );
60400
+ } catch {
60401
+ }
60058
60402
  writeFileSync2(resolved, newContent, "utf-8");
60059
60403
  markFileAsRead(resolved);
60060
- const gitDiff = generateSimpleDiff(originalContent, newContent, input.file_path);
60404
+ const gitDiff = generateUnifiedDiff(originalContent, newContent, input.file_path);
60061
60405
  return {
60062
60406
  data: {
60063
60407
  filePath: resolved,
@@ -60093,7 +60437,7 @@ Usage:
60093
60437
  });
60094
60438
 
60095
60439
  // src/tools/builtin/write.ts
60096
- import { writeFileSync as writeFileSync3, existsSync as existsSync6, mkdirSync as mkdirSync3 } from "fs";
60440
+ import { writeFileSync as writeFileSync3, existsSync as existsSync7, mkdirSync as mkdirSync4 } from "fs";
60097
60441
  import { dirname as dirname2, resolve as resolve4, isAbsolute as isAbsolute3 } from "path";
60098
60442
  function resolvePath3(filePath) {
60099
60443
  if (isAbsolute3(filePath)) return filePath;
@@ -60155,7 +60499,7 @@ var init_write = __esm({
60155
60499
  return { result: false, message: "file_path is required", errorCode: 1 };
60156
60500
  }
60157
60501
  const resolved = resolvePath3(input.file_path);
60158
- if (existsSync6(resolved) && !hasFileBeenRead(resolved)) {
60502
+ if (existsSync7(resolved) && !hasFileBeenRead(resolved)) {
60159
60503
  return {
60160
60504
  result: false,
60161
60505
  message: "This file already exists. You must Read it first before overwriting. Use the Read tool, then retry.",
@@ -60169,10 +60513,10 @@ var init_write = __esm({
60169
60513
  },
60170
60514
  async call(input, context) {
60171
60515
  const resolved = resolvePath3(input.file_path);
60172
- const created = !existsSync6(resolved);
60516
+ const created = !existsSync7(resolved);
60173
60517
  const dir = dirname2(resolved);
60174
- if (!existsSync6(dir)) {
60175
- mkdirSync3(dir, { recursive: true });
60518
+ if (!existsSync7(dir)) {
60519
+ mkdirSync4(dir, { recursive: true });
60176
60520
  }
60177
60521
  writeFileSync3(resolved, input.content, "utf-8");
60178
60522
  markFileAsRead(resolved);
@@ -67042,10 +67386,11 @@ function useSpinner(active) {
67042
67386
  }, [active]);
67043
67387
  return active ? FRAMES[i] : " ";
67044
67388
  }
67045
- function createToolHandlers(onToolStart, onToolEnd) {
67389
+ function createToolHandlers(onToolStart, onToolEnd, requestPermission) {
67046
67390
  const tools = [bashTool, readTool, editTool, writeTool, globTool, grepTool];
67047
67391
  const permCtx = createDefaultPermissionContext();
67048
67392
  const appState = { toolPermissionContext: permCtx, verbose: false };
67393
+ const alwaysAllowed = /* @__PURE__ */ new Set();
67049
67394
  return tools.map((tool) => ({
67050
67395
  name: tool.name,
67051
67396
  description: typeof tool.description === "function" ? `Tool: ${tool.name}` : String(tool.description),
@@ -67055,6 +67400,25 @@ function createToolHandlers(onToolStart, onToolEnd) {
67055
67400
  call: async (input, ctx) => {
67056
67401
  const summary = toolSummary(tool.name, input);
67057
67402
  const toolId = `tool-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
67403
+ if (!tool.isReadOnly() && !alwaysAllowed.has(tool.name)) {
67404
+ const decision = await requestPermission(tool.name, summary);
67405
+ if (decision === "deny") {
67406
+ onToolStart(toolId, tool.name, summary);
67407
+ onToolEnd(toolId, "Permission denied by user", "Permission denied", 0);
67408
+ try {
67409
+ dbRun("INSERT INTO audit_log (tool_name, input_summary, was_allowed) VALUES (?, ?, 0)", [tool.name, summary]);
67410
+ } catch {
67411
+ }
67412
+ return { error: "Permission denied by user", isError: true };
67413
+ }
67414
+ if (decision === "always") {
67415
+ alwaysAllowed.add(tool.name);
67416
+ try {
67417
+ dbRun("INSERT OR REPLACE INTO permissions (tool_name, behavior) VALUES (?, 'allow')", [tool.name]);
67418
+ } catch {
67419
+ }
67420
+ }
67421
+ }
67058
67422
  onToolStart(toolId, tool.name, summary);
67059
67423
  const t0 = performance.now();
67060
67424
  try {
@@ -67068,11 +67432,19 @@ function createToolHandlers(onToolStart, onToolEnd) {
67068
67432
  const block = tool.mapToolResultToToolResultBlockParam(result.data, toolId);
67069
67433
  const dur = performance.now() - t0;
67070
67434
  onToolEnd(toolId, block.content.slice(0, 500), block.is_error ? block.content : void 0, dur);
67435
+ try {
67436
+ dbRun("INSERT INTO audit_log (tool_name, input_summary, result_summary, duration_ms, was_allowed) VALUES (?, ?, ?, ?, 1)", [tool.name, summary, block.content.slice(0, 200), dur]);
67437
+ } catch {
67438
+ }
67071
67439
  return { data: block.content };
67072
67440
  } catch (err) {
67073
67441
  const dur = performance.now() - t0;
67074
67442
  const errMsg = err instanceof Error ? err.message : String(err);
67075
67443
  onToolEnd(toolId, "", errMsg, dur);
67444
+ try {
67445
+ dbRun("INSERT INTO audit_log (tool_name, input_summary, result_summary, duration_ms, was_allowed) VALUES (?, ?, ?, ?, 1)", [tool.name, summary, errMsg.slice(0, 200), dur]);
67446
+ } catch {
67447
+ }
67076
67448
  return { error: errMsg, isError: true };
67077
67449
  }
67078
67450
  }
@@ -67192,6 +67564,25 @@ function App2({ model, mode, initialPrompt }) {
67192
67564
  const [history, setHistory] = (0, import_react22.useState)([]);
67193
67565
  const [activeTools, setActiveTools] = (0, import_react22.useState)([]);
67194
67566
  const rows = stdout?.rows ?? 24;
67567
+ const [permissionPending, setPermissionPending] = (0, import_react22.useState)(null);
67568
+ const requestPermission = (0, import_react22.useCallback)((toolName, summary) => {
67569
+ return new Promise((resolve7) => {
67570
+ setPermissionPending({ toolName, summary, resolve: resolve7 });
67571
+ });
67572
+ }, []);
67573
+ use_input_default((ch) => {
67574
+ if (!permissionPending) return;
67575
+ if (ch === "y" || ch === "Y") {
67576
+ permissionPending.resolve("allow");
67577
+ setPermissionPending(null);
67578
+ } else if (ch === "n" || ch === "N") {
67579
+ permissionPending.resolve("deny");
67580
+ setPermissionPending(null);
67581
+ } else if (ch === "a" || ch === "A") {
67582
+ permissionPending.resolve("always");
67583
+ setPermissionPending(null);
67584
+ }
67585
+ }, { isActive: !!permissionPending });
67195
67586
  const onToolStart = (0, import_react22.useCallback)((id, name, summary) => {
67196
67587
  setActiveTools((prev) => [...prev, { id, name, summary, status: "running" }]);
67197
67588
  }, []);
@@ -67222,7 +67613,7 @@ function App2({ model, mode, initialPrompt }) {
67222
67613
  setActiveTools([]);
67223
67614
  const t0 = performance.now();
67224
67615
  try {
67225
- const toolHandlers = createToolHandlers(onToolStart, onToolEnd);
67616
+ const toolHandlers = createToolHandlers(onToolStart, onToolEnd, requestPermission);
67226
67617
  const permCtx = createDefaultPermissionContext();
67227
67618
  const result = await runAgentLoop(
67228
67619
  newHistory,
@@ -67284,7 +67675,7 @@ function App2({ model, mode, initialPrompt }) {
67284
67675
  } finally {
67285
67676
  setBusy(false);
67286
67677
  }
67287
- }, [busy, history, model, exit, onToolStart, onToolEnd, streaming, activeTools]);
67678
+ }, [busy, history, model, exit, onToolStart, onToolEnd, requestPermission, streaming, activeTools]);
67288
67679
  (0, import_react22.useEffect)(() => {
67289
67680
  if (initialPrompt) submit(initialPrompt);
67290
67681
  }, []);
@@ -67316,12 +67707,34 @@ function App2({ model, mode, initialPrompt }) {
67316
67707
  visible.map((m) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(MessageView, { msg: m }, m.id)),
67317
67708
  busy && activeTools.filter((t) => t.status === "running").map((t) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ToolItem, { tool: t }, t.id)),
67318
67709
  busy && streaming && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box_default, { marginTop: 1, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { children: streaming }) }),
67319
- busy && !streaming && activeTools.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box_default, { marginTop: 1, children: [
67710
+ busy && !streaming && activeTools.length === 0 && !permissionPending && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box_default, { marginTop: 1, children: [
67320
67711
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, { color: "cyan", children: [
67321
67712
  FRAMES[0],
67322
67713
  " "
67323
67714
  ] }),
67324
67715
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { dimColor: true, children: "Thinking..." })
67716
+ ] }),
67717
+ permissionPending && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box_default, { flexDirection: "column", marginTop: 1, children: [
67718
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box_default, { children: [
67719
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { color: "yellow", bold: true, children: "? " }),
67720
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { children: "Allow " }),
67721
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { bold: true, children: permissionPending.toolName }),
67722
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, { dimColor: true, children: [
67723
+ ": ",
67724
+ permissionPending.summary.slice(0, 80)
67725
+ ] })
67726
+ ] }),
67727
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box_default, { children: [
67728
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { dimColor: true, children: " " }),
67729
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { color: "green", bold: true, children: "(y)" }),
67730
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { color: "green", children: "es" }),
67731
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { children: " " }),
67732
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { color: "red", bold: true, children: "(n)" }),
67733
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { color: "red", children: "o" }),
67734
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { children: " " }),
67735
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { color: "blue", bold: true, children: "(a)" }),
67736
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { color: "blue", children: "lways allow" })
67737
+ ] })
67325
67738
  ] })
67326
67739
  ] }),
67327
67740
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box_default, { children: [
@@ -67392,6 +67805,7 @@ var init_app = __esm({
67392
67805
  init_streaming();
67393
67806
  init_agent_loop();
67394
67807
  init_permissions();
67808
+ init_db();
67395
67809
  init_bash();
67396
67810
  init_read();
67397
67811
  init_edit();
@@ -67619,8 +68033,8 @@ async function bootstrap() {
67619
68033
  var VERSION, BUILD_TIME, PACKAGE_NAME2, ISSUES_URL2, startupTimestamps, originalCwd, RESET_TERMINAL, cleanupHandlers, earlyInput, earlyInputCapturing;
67620
68034
  var init_index = __esm({
67621
68035
  "src/cli/index.ts"() {
67622
- VERSION = "0.0.3";
67623
- BUILD_TIME = "2026-03-20T06:29:11.511Z";
68036
+ VERSION = "0.0.4";
68037
+ BUILD_TIME = "2026-03-20T07:32:07.258Z";
67624
68038
  PACKAGE_NAME2 = "@hasna/coders";
67625
68039
  ISSUES_URL2 = "https://github.com/hasnaxyz/open-coders/issues";
67626
68040
  startupTimestamps = {};