soulguard 0.2.1 → 0.2.3

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.
Files changed (2) hide show
  1. package/dist/index.js +386 -206
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -4096,7 +4096,8 @@ var ownershipSchema = exports_external.object({
4096
4096
  mode: exports_external.string()
4097
4097
  });
4098
4098
  var daemonConfigSchema = exports_external.object({
4099
- channel: exports_external.string()
4099
+ channel: exports_external.string().optional(),
4100
+ syncIntervalSecs: exports_external.number().int().nonnegative().optional()
4100
4101
  }).passthrough();
4101
4102
  var soulguardConfigSchema = exports_external.object({
4102
4103
  version: exports_external.literal(1),
@@ -6153,6 +6154,7 @@ ${approvalMessage}`;
6153
6154
  }
6154
6155
 
6155
6156
  // ../core/src/sdk/sync.ts
6157
+ import { dirname as dirname2 } from "node:path";
6156
6158
  async function sync(options) {
6157
6159
  const { tree, ops, config } = options;
6158
6160
  const drifts = tree.driftedEntities();
@@ -6182,8 +6184,38 @@ async function sync(options) {
6182
6184
  }
6183
6185
  }
6184
6186
  }
6187
+ let stagingCopiesCreated = 0;
6188
+ const protectFiles = tree.flatFiles().filter((f) => f.configTier === "protect");
6189
+ for (const file of protectFiles) {
6190
+ if (file.canonicalHash === null)
6191
+ continue;
6192
+ if (file.stagedHash !== null || file.status === "deleted")
6193
+ continue;
6194
+ const stagePath = stagingPath(file.path);
6195
+ const parentDir = dirname2(stagePath);
6196
+ if (parentDir !== "." && parentDir !== "/" && parentDir !== STAGING_DIR) {
6197
+ await ops.mkdir(parentDir);
6198
+ const defaultOwnership = config.defaultOwnership;
6199
+ if (defaultOwnership) {
6200
+ await ops.chown(parentDir, { user: defaultOwnership.user, group: defaultOwnership.group });
6201
+ await ops.chmod(parentDir, "755");
6202
+ }
6203
+ }
6204
+ const readResult = await ops.readFile(file.path);
6205
+ if (!readResult.ok)
6206
+ continue;
6207
+ const writeResult = await ops.writeFile(stagePath, readResult.value);
6208
+ if (writeResult.ok) {
6209
+ const defaultOwnership = config.defaultOwnership;
6210
+ if (defaultOwnership) {
6211
+ await ops.chown(stagePath, { user: defaultOwnership.user, group: defaultOwnership.group });
6212
+ await ops.chmod(stagePath, defaultOwnership.mode);
6213
+ }
6214
+ stagingCopiesCreated++;
6215
+ }
6216
+ }
6185
6217
  if (errors2.length > 0) {
6186
- return ok({ drifts, errors: errors2, git: undefined });
6218
+ return ok({ drifts, errors: errors2, git: undefined, stagingCopiesCreated });
6187
6219
  }
6188
6220
  let git;
6189
6221
  if (await isGitEnabled(ops, config)) {
@@ -6195,7 +6227,7 @@ async function sync(options) {
6195
6227
  }
6196
6228
  }
6197
6229
  }
6198
- return ok({ drifts, errors: errors2, git });
6230
+ return ok({ drifts, errors: errors2, git, stagingCopiesCreated });
6199
6231
  }
6200
6232
  // ../core/src/util/console-live.ts
6201
6233
  var import_picocolors = __toESM(require_picocolors(), 1);
@@ -6280,11 +6312,11 @@ function validateSelfProtection(pendingContents, deletedFiles = []) {
6280
6312
  return ok(undefined);
6281
6313
  }
6282
6314
  // ../core/src/sdk/apply.ts
6283
- import { dirname as dirname2 } from "node:path";
6315
+ import { dirname as dirname3 } from "node:path";
6284
6316
  function collectParentDirs(prefix, paths) {
6285
6317
  const dirs = new Set;
6286
6318
  for (const p of paths) {
6287
- const parent = dirname2(`${prefix}/${p}`);
6319
+ const parent = dirname3(`${prefix}/${p}`);
6288
6320
  if (parent !== prefix) {
6289
6321
  dirs.add(parent);
6290
6322
  }
@@ -6384,7 +6416,7 @@ async function apply(options) {
6384
6416
  }
6385
6417
  } else {
6386
6418
  if (file.status === "created") {
6387
- const parentDir = dirname2(file.path);
6419
+ const parentDir = dirname3(file.path);
6388
6420
  if (parentDir !== ".") {
6389
6421
  await ops.mkdir(parentDir);
6390
6422
  }
@@ -6432,7 +6464,7 @@ async function apply(options) {
6432
6464
  const defaultOwnership = tree.config.defaultOwnership;
6433
6465
  for (const file of nonDeletedFiles) {
6434
6466
  const stagePath = stagingPath(file.path);
6435
- const stageParent = dirname2(stagePath);
6467
+ const stageParent = dirname3(stagePath);
6436
6468
  if (stageParent !== STAGING_DIR) {
6437
6469
  await ops.mkdir(stageParent);
6438
6470
  }
@@ -6613,10 +6645,10 @@ class SyncCommand {
6613
6645
  const result = await sync(this.opts);
6614
6646
  if (!result.ok)
6615
6647
  return 1;
6616
- const { drifts, errors: errors2, git } = result.value;
6648
+ const { drifts, errors: errors2, git, stagingCopiesCreated } = result.value;
6617
6649
  this.out.heading(`Soulguard Sync — ${this.opts.ops.workspace}`);
6618
6650
  this.out.write("");
6619
- if (drifts.length === 0 && errors2.length === 0) {
6651
+ if (drifts.length === 0 && errors2.length === 0 && stagingCopiesCreated === 0) {
6620
6652
  this.out.success("Nothing to fix — all files ok.");
6621
6653
  this.reportGit(git);
6622
6654
  return 0;
@@ -6639,6 +6671,10 @@ class SyncCommand {
6639
6671
  this.out.write("");
6640
6672
  return 1;
6641
6673
  }
6674
+ if (stagingCopiesCreated > 0) {
6675
+ this.out.success(` Refreshed ${stagingCopiesCreated} staging ${stagingCopiesCreated === 1 ? "copy" : "copies"}.`);
6676
+ this.out.write("");
6677
+ }
6642
6678
  this.out.success("All files now ok.");
6643
6679
  this.reportGit(git);
6644
6680
  return 0;
@@ -6815,6 +6851,54 @@ class ResetCommand {
6815
6851
  return 0;
6816
6852
  }
6817
6853
  }
6854
+ // ../core/src/sdk/staging-ops.ts
6855
+ import { dirname as dirname4 } from "node:path";
6856
+ function isInProtectTier(path, protectPatterns2) {
6857
+ for (const pattern of protectPatterns2) {
6858
+ const normalized = pattern.endsWith("/") ? pattern.slice(0, -1) : pattern;
6859
+ if (path === normalized || path === pattern)
6860
+ return true;
6861
+ if (path.startsWith(normalized + "/"))
6862
+ return true;
6863
+ }
6864
+ return false;
6865
+ }
6866
+ async function ensureStagingParentDir(ops, canonicalPath, ownership) {
6867
+ const stagePath = stagingPath(canonicalPath);
6868
+ const parentDir = dirname4(stagePath);
6869
+ if (parentDir === "." || parentDir === "/" || parentDir === STAGING_DIR) {
6870
+ return ok(undefined);
6871
+ }
6872
+ const mkdirResult = await ops.mkdir(parentDir);
6873
+ if (!mkdirResult.ok) {
6874
+ return err(`Cannot create parent directory ${parentDir}: ${mkdirResult.error.kind}`);
6875
+ }
6876
+ if (ownership) {
6877
+ await ops.chown(parentDir, { user: ownership.user, group: ownership.group });
6878
+ await ops.chmod(parentDir, "755");
6879
+ }
6880
+ return ok(undefined);
6881
+ }
6882
+ async function createStagingCopy(ops, path, ownership) {
6883
+ const stagePath = stagingPath(path);
6884
+ const parentResult = await ensureStagingParentDir(ops, path, ownership);
6885
+ if (!parentResult.ok)
6886
+ return parentResult;
6887
+ const readResult = await ops.readFile(path);
6888
+ if (!readResult.ok) {
6889
+ return err(`Cannot read ${path}: ${readResult.error.kind}`);
6890
+ }
6891
+ const writeResult = await ops.writeFile(stagePath, readResult.value);
6892
+ if (!writeResult.ok) {
6893
+ return err(`Cannot write staging copy ${stagePath}: ${writeResult.error.kind}`);
6894
+ }
6895
+ if (ownership) {
6896
+ await ops.chown(stagePath, { user: ownership.user, group: ownership.group });
6897
+ await ops.chmod(stagePath, ownership.mode);
6898
+ }
6899
+ return ok(undefined);
6900
+ }
6901
+
6818
6902
  // ../core/src/cli/tier-command.ts
6819
6903
  function formatSummary(paths) {
6820
6904
  const dirs = paths.filter((p) => p.endsWith("/")).length;
@@ -6981,6 +7065,34 @@ class TierCommand {
6981
7065
  return 1;
6982
7066
  }
6983
7067
  }
7068
+ const defaultOwnership = configResult.value.defaultOwnership;
7069
+ await ops.mkdir(".soulguard-staging");
7070
+ if (defaultOwnership) {
7071
+ await ops.chown(".soulguard-staging", {
7072
+ user: defaultOwnership.user,
7073
+ group: defaultOwnership.group
7074
+ });
7075
+ await ops.chmod(".soulguard-staging", "755");
7076
+ }
7077
+ for (const file of changedPaths) {
7078
+ const isDir = await isDirectory(ops, file);
7079
+ if (isDir) {
7080
+ const listResult = await ops.listDir(file);
7081
+ if (listResult.ok) {
7082
+ for (const childPath of listResult.value) {
7083
+ const copyResult = await createStagingCopy(ops, childPath, defaultOwnership ?? undefined);
7084
+ if (!copyResult.ok) {
7085
+ this.out.warn(` Warning: staging copy failed for ${childPath}: ${copyResult.error}`);
7086
+ }
7087
+ }
7088
+ }
7089
+ } else {
7090
+ const copyResult = await createStagingCopy(ops, file, defaultOwnership ?? undefined);
7091
+ if (!copyResult.ok) {
7092
+ this.out.warn(` Warning: staging copy failed for ${file}: ${copyResult.error}`);
7093
+ }
7094
+ }
7095
+ }
6984
7096
  } else if (action.kind === "set" && action.tier === "watch") {
6985
7097
  const defaultOwnership = configResult.value.defaultOwnership;
6986
7098
  for (const file of changedPaths) {
@@ -7018,153 +7130,114 @@ class TierCommand {
7018
7130
  return 0;
7019
7131
  }
7020
7132
  }
7021
- // ../core/src/sdk/stage.ts
7022
- import { dirname as dirname3 } from "node:path";
7023
- function isInProtectTier(path, protectPatterns2) {
7024
- for (const pattern of protectPatterns2) {
7025
- const normalized = pattern.endsWith("/") ? pattern.slice(0, -1) : pattern;
7026
- if (path === normalized || path === pattern)
7027
- return true;
7028
- if (path.startsWith(normalized + "/"))
7029
- return true;
7030
- }
7031
- return false;
7032
- }
7033
- async function ensureParentDir(ops, path) {
7034
- const parentDir = dirname3(path);
7035
- if (parentDir === "." || parentDir === "/") {
7036
- return ok(undefined);
7133
+ // ../core/src/sdk/create.ts
7134
+ async function create(options) {
7135
+ const { ops, config, path } = options;
7136
+ if (!isInProtectTier(path, protectPatterns(config))) {
7137
+ return err({ kind: "not_in_protect_tier", path });
7037
7138
  }
7038
- const mkdirResult = await ops.mkdir(parentDir);
7039
- if (!mkdirResult.ok) {
7040
- return err(`Cannot create parent directory ${parentDir}: ${mkdirResult.error.kind}`);
7139
+ const exists = await ops.exists(path);
7140
+ if (exists.ok && exists.value) {
7141
+ return ok({ path, alreadyExisted: true });
7041
7142
  }
7042
- return ok(undefined);
7043
- }
7044
- async function stage(options) {
7045
- const { ops, config, path, delete: isDelete } = options;
7046
- const protectFiles = protectPatterns(config);
7047
- if (!isInProtectTier(path, protectFiles)) {
7048
- return err({ kind: "not_in_protect_tier", path });
7143
+ const stagePath = stagingPath(path);
7144
+ const parentResult = await ensureStagingParentDir(ops, path);
7145
+ if (!parentResult.ok) {
7146
+ return err({ kind: "create_failed", path, message: parentResult.error });
7049
7147
  }
7050
- const stagedFiles = [];
7051
- const pathStat = await ops.stat(path);
7052
- if (!pathStat.ok && pathStat.error.kind !== "not_found") {
7148
+ const writeResult = await ops.writeFile(stagePath, "");
7149
+ if (!writeResult.ok) {
7053
7150
  return err({
7054
- kind: "stage_failed",
7151
+ kind: "create_failed",
7055
7152
  path,
7056
- message: `Cannot stat path: ${pathStat.error.kind}`
7153
+ message: `Cannot create staging file: ${writeResult.error.kind}`
7057
7154
  });
7058
7155
  }
7059
- const isDirectory2 = pathStat.ok && pathStat.value.isDirectory;
7060
- if (isDirectory2 && isDelete) {
7061
- const stagePath = stagingPath(path);
7062
- const existsResult = await ops.exists(stagePath);
7063
- if (existsResult.ok && existsResult.value) {
7064
- const readResult = await ops.readFile(stagePath);
7065
- if (readResult.ok && isDeleteSentinel(readResult.value)) {
7066
- return ok({ stagedFiles: [] });
7067
- }
7068
- }
7069
- const parentResult = await ensureParentDir(ops, stagePath);
7070
- if (!parentResult.ok) {
7071
- return err({ kind: "stage_failed", path, message: parentResult.error });
7072
- }
7073
- const writeResult = await ops.writeFile(stagePath, JSON.stringify(DELETE_SENTINEL, null, 2));
7074
- if (!writeResult.ok) {
7075
- return err({
7076
- kind: "stage_failed",
7077
- path,
7078
- message: `Cannot write delete sentinel: ${writeResult.error.kind}`
7079
- });
7080
- }
7081
- stagedFiles.push({ path, action: "delete" });
7082
- } else if (isDirectory2 && !isDelete) {
7083
- const listResult = await ops.listDir(path);
7084
- if (!listResult.ok) {
7085
- return err({
7086
- kind: "stage_failed",
7087
- path,
7088
- message: `Cannot list directory: ${listResult.error.kind}`
7089
- });
7156
+ return ok({ path, alreadyExisted: false });
7157
+ }
7158
+
7159
+ // ../core/src/cli/create-command.ts
7160
+ class CreateCommand {
7161
+ opts;
7162
+ out;
7163
+ constructor(opts, out) {
7164
+ this.opts = opts;
7165
+ this.out = out;
7166
+ }
7167
+ async execute() {
7168
+ const { ops, config, paths } = this.opts;
7169
+ if (paths.length === 0) {
7170
+ this.out.error("No paths specified.");
7171
+ return 1;
7090
7172
  }
7091
- for (const filePath of listResult.value) {
7092
- const result = await stage({ ops, config, path: filePath, delete: isDelete });
7173
+ let created = 0;
7174
+ for (const path of paths) {
7175
+ const result = await create({ ops, config, path });
7093
7176
  if (!result.ok) {
7094
- return result;
7095
- }
7096
- stagedFiles.push(...result.value.stagedFiles);
7097
- }
7098
- } else if (!isDirectory2 && isDelete) {
7099
- const stagePath = stagingPath(path);
7100
- const existsResult = await ops.exists(stagePath);
7101
- if (existsResult.ok && existsResult.value) {
7102
- const readResult = await ops.readFile(stagePath);
7103
- if (readResult.ok && isDeleteSentinel(readResult.value)) {
7104
- return ok({ stagedFiles: [] });
7177
+ switch (result.error.kind) {
7178
+ case "not_in_protect_tier":
7179
+ this.out.error(`${result.error.path} is not in the protect tier.`);
7180
+ return 1;
7181
+ case "create_failed":
7182
+ this.out.error(`Failed to create ${result.error.path}: ${result.error.message}`);
7183
+ return 1;
7184
+ }
7105
7185
  }
7106
- }
7107
- const parentResult = await ensureParentDir(ops, stagePath);
7108
- if (!parentResult.ok) {
7109
- return err({ kind: "stage_failed", path, message: parentResult.error });
7110
- }
7111
- const writeResult = await ops.writeFile(stagePath, JSON.stringify(DELETE_SENTINEL, null, 2));
7112
- if (!writeResult.ok) {
7113
- return err({
7114
- kind: "stage_failed",
7115
- path,
7116
- message: `Cannot write delete sentinel: ${writeResult.error.kind}`
7117
- });
7118
- }
7119
- stagedFiles.push({ path, action: "delete" });
7120
- } else if (!isDirectory2 && !isDelete) {
7121
- const stagePath = stagingPath(path);
7122
- const existsResult = await ops.exists(stagePath);
7123
- if (existsResult.ok && existsResult.value) {
7124
- const readResult = await ops.readFile(stagePath);
7125
- if (readResult.ok && isDeleteSentinel(readResult.value)) {} else {
7126
- return ok({ stagedFiles: [] });
7186
+ if (result.value.alreadyExisted) {
7187
+ this.out.warn(` · ${path} (already exists staging copy was created automatically)`);
7188
+ continue;
7127
7189
  }
7190
+ this.out.success(` + ${path} → ${stagingPath(path)}`);
7191
+ created++;
7128
7192
  }
7129
- const parentResult = await ensureParentDir(ops, stagePath);
7130
- if (!parentResult.ok) {
7131
- return err({ kind: "stage_failed", path, message: parentResult.error });
7132
- }
7133
- const sourceExists = await ops.exists(path);
7134
- if (sourceExists.ok && sourceExists.value) {
7135
- const readResult = await ops.readFile(path);
7136
- if (!readResult.ok) {
7137
- return err({
7138
- kind: "stage_failed",
7139
- path,
7140
- message: `Cannot read file: ${readResult.error.kind}`
7141
- });
7142
- }
7143
- const writeResult = await ops.writeFile(stagePath, readResult.value);
7144
- if (!writeResult.ok) {
7145
- return err({
7146
- kind: "stage_failed",
7147
- path,
7148
- message: `Cannot write staging copy: ${writeResult.error.kind}`
7149
- });
7150
- }
7193
+ if (created === 0) {
7194
+ this.out.info("Nothing to create.");
7151
7195
  } else {
7152
- const writeResult = await ops.writeFile(stagePath, "");
7153
- if (!writeResult.ok) {
7154
- return err({
7155
- kind: "stage_failed",
7156
- path,
7157
- message: `Cannot create empty staging file: ${writeResult.error.kind}`
7158
- });
7159
- }
7196
+ this.out.write("");
7197
+ this.out.success(`Created ${created} staging ${created === 1 ? "entry" : "entries"}.`);
7198
+ }
7199
+ return 0;
7200
+ }
7201
+ }
7202
+ // ../core/src/sdk/delete.ts
7203
+ async function deleteStagingEntry(options) {
7204
+ const { ops, config, path } = options;
7205
+ if (!isInProtectTier(path, protectPatterns(config))) {
7206
+ return err({ kind: "not_in_protect_tier", path });
7207
+ }
7208
+ const diskExists = await ops.exists(path);
7209
+ const stagePath = stagingPath(path);
7210
+ const stagingExists = await ops.exists(stagePath);
7211
+ if ((!diskExists.ok || !diskExists.value) && (!stagingExists.ok || !stagingExists.value)) {
7212
+ return err({ kind: "not_found", path });
7213
+ }
7214
+ if (stagingExists.ok && stagingExists.value) {
7215
+ const readResult = await ops.readFile(stagePath);
7216
+ if (readResult.ok && isDeleteSentinel(readResult.value)) {
7217
+ return err({ kind: "already_deleted", path });
7160
7218
  }
7161
- stagedFiles.push({ path, action: "edit" });
7162
7219
  }
7163
- return ok({ stagedFiles });
7220
+ const stageExists = await ops.stat(stagePath);
7221
+ if (stageExists.ok && stageExists.value.isDirectory) {
7222
+ await ops.exec("rm", ["-rf", stagePath]);
7223
+ }
7224
+ const parentResult = await ensureStagingParentDir(ops, path);
7225
+ if (!parentResult.ok) {
7226
+ return err({ kind: "delete_failed", path, message: parentResult.error });
7227
+ }
7228
+ const writeResult = await ops.writeFile(stagePath, JSON.stringify(DELETE_SENTINEL, null, 2));
7229
+ if (!writeResult.ok) {
7230
+ return err({
7231
+ kind: "delete_failed",
7232
+ path,
7233
+ message: `Cannot write delete sentinel: ${writeResult.error.kind}`
7234
+ });
7235
+ }
7236
+ return ok({ path });
7164
7237
  }
7165
7238
 
7166
- // ../core/src/cli/stage-command.ts
7167
- class StageCommand {
7239
+ // ../core/src/cli/delete-command.ts
7240
+ class DeleteCommand {
7168
7241
  opts;
7169
7242
  out;
7170
7243
  constructor(opts, out) {
@@ -7172,46 +7245,38 @@ class StageCommand {
7172
7245
  this.out = out;
7173
7246
  }
7174
7247
  async execute() {
7175
- const { ops, config, paths, delete: isDelete } = this.opts;
7248
+ const { ops, config, paths } = this.opts;
7176
7249
  if (paths.length === 0) {
7177
7250
  this.out.error("No paths specified.");
7178
7251
  return 1;
7179
7252
  }
7180
- const allStagedFiles = [];
7181
- const alreadyStaged = [];
7253
+ let deleted = 0;
7182
7254
  for (const path of paths) {
7183
- const result = await stage({ ops, config, path, delete: isDelete });
7255
+ const result = await deleteStagingEntry({ ops, config, path });
7184
7256
  if (!result.ok) {
7185
7257
  switch (result.error.kind) {
7186
7258
  case "not_in_protect_tier":
7187
7259
  this.out.error(`${result.error.path} is not in the protect tier.`);
7188
7260
  return 1;
7189
- case "stage_failed":
7190
- this.out.error(`Failed to stage ${result.error.path}: ${result.error.message}`);
7261
+ case "not_found":
7262
+ this.out.error(`${result.error.path} does not exist.`);
7263
+ return 1;
7264
+ case "already_deleted":
7265
+ this.out.info(` · ${result.error.path} (already staged for deletion)`);
7266
+ continue;
7267
+ case "delete_failed":
7268
+ this.out.error(`Failed to delete ${result.error.path}: ${result.error.message}`);
7191
7269
  return 1;
7192
7270
  }
7193
7271
  }
7194
- if (result.value.stagedFiles.length === 0) {
7195
- alreadyStaged.push(path);
7196
- } else {
7197
- allStagedFiles.push(...result.value.stagedFiles);
7198
- }
7199
- }
7200
- for (const file of alreadyStaged) {
7201
- this.out.info(` · ${file} (already staged)`);
7202
- }
7203
- for (const { path, action } of allStagedFiles) {
7204
- if (action === "delete") {
7205
- this.out.success(` \uD83D\uDDD1️ ${path} (staged for deletion)`);
7206
- } else {
7207
- this.out.success(` \uD83D\uDCDD ${path} (staged for editing)`);
7208
- }
7272
+ this.out.success(` \uD83D\uDDD1️ ${path} (staged for deletion)`);
7273
+ deleted++;
7209
7274
  }
7210
- if (allStagedFiles.length === 0) {
7211
- this.out.info("Nothing to stage.");
7275
+ if (deleted === 0) {
7276
+ this.out.info("Nothing to do.");
7212
7277
  } else {
7213
7278
  this.out.write("");
7214
- this.out.success(`Staged ${allStagedFiles.length} file(s).`);
7279
+ this.out.success(`Staged ${deleted} ${deleted === 1 ? "path" : "paths"} for deletion.`);
7215
7280
  }
7216
7281
  return 0;
7217
7282
  }
@@ -7405,6 +7470,9 @@ class ProposalManager extends EventEmitter {
7405
7470
  this._abortController = null;
7406
7471
  }
7407
7472
  }
7473
+ // ../core/src/daemon/daemon.ts
7474
+ import { EventEmitter as EventEmitter2 } from "node:events";
7475
+
7408
7476
  // ../core/src/daemon/channel-registry.ts
7409
7477
  var REGISTRY_KEY = "__soulguard_channel_registry__";
7410
7478
  function getRegistry() {
@@ -7422,13 +7490,16 @@ function getChannel(name) {
7422
7490
  }
7423
7491
 
7424
7492
  // ../core/src/daemon/daemon.ts
7425
- class SoulguardDaemon {
7493
+ class SoulguardDaemon extends EventEmitter2 {
7426
7494
  _ops;
7427
7495
  _config;
7428
7496
  _channel = null;
7429
7497
  _proposalManager = null;
7498
+ _syncTimer = null;
7499
+ _syncRunning = false;
7430
7500
  _running = false;
7431
7501
  constructor(options) {
7502
+ super();
7432
7503
  this._ops = options.ops;
7433
7504
  this._config = options.config;
7434
7505
  }
@@ -7446,28 +7517,40 @@ class SoulguardDaemon {
7446
7517
  throw new Error("Daemon configuration missing. Add a 'daemon' section to soulguard.json.");
7447
7518
  }
7448
7519
  const channelName = daemonConfig.channel;
7449
- console.log(`[daemon] channel: "${channelName}", config keys: ${JSON.stringify(Object.keys(daemonConfig))}`);
7450
- const createChannelFn = getChannel(channelName);
7451
- if (!createChannelFn) {
7452
- throw new Error(`No channel registered for "${channelName}". Register it with registerChannel() before starting the daemon.`);
7453
- }
7454
- const channelConfig = daemonConfig[channelName];
7455
- console.log(`[daemon] channelConfig present: ${!!channelConfig}, keys: ${channelConfig ? JSON.stringify(Object.keys(channelConfig)) : "n/a"}`);
7456
- this._channel = createChannelFn(channelConfig);
7457
- console.log(`[daemon] channel created: ${this._channel.name}`);
7458
- this._proposalManager = new ProposalManager({
7459
- ops: this._ops,
7460
- config: this._config,
7461
- channel: this._channel
7462
- });
7463
- console.log(`[daemon] starting proposal manager`);
7464
- this._proposalManager.start();
7520
+ if (channelName) {
7521
+ const createChannelFn = getChannel(channelName);
7522
+ if (!createChannelFn) {
7523
+ throw new Error(`No channel registered for "${channelName}". Register it with registerChannel() before starting the daemon.`);
7524
+ }
7525
+ const channelConfig = daemonConfig[channelName];
7526
+ this._channel = createChannelFn(channelConfig);
7527
+ this._proposalManager = new ProposalManager({
7528
+ ops: this._ops,
7529
+ config: this._config,
7530
+ channel: this._channel
7531
+ });
7532
+ this._proposalManager.on("proposed", (...args) => this.emit("proposed", ...args));
7533
+ this._proposalManager.on("applied", (...args) => this.emit("applied", ...args));
7534
+ this._proposalManager.on("rejected", (...args) => this.emit("rejected", ...args));
7535
+ this._proposalManager.on("superseded", (...args) => this.emit("superseded", ...args));
7536
+ this._proposalManager.on("error", (...args) => this.emit("proposal:error", ...args));
7537
+ this._proposalManager.start();
7538
+ }
7539
+ const syncIntervalSecs = daemonConfig.syncIntervalSecs ?? 60;
7540
+ if (syncIntervalSecs > 0) {
7541
+ this._runSync();
7542
+ this._syncTimer = setInterval(() => this._runSync(), syncIntervalSecs * 1000);
7543
+ }
7465
7544
  this._running = true;
7466
7545
  }
7467
7546
  async stop() {
7468
7547
  if (!this._running)
7469
7548
  return;
7470
7549
  this._running = false;
7550
+ if (this._syncTimer) {
7551
+ clearInterval(this._syncTimer);
7552
+ this._syncTimer = null;
7553
+ }
7471
7554
  if (this._proposalManager) {
7472
7555
  await this._proposalManager.stop();
7473
7556
  this._proposalManager = null;
@@ -7477,6 +7560,35 @@ class SoulguardDaemon {
7477
7560
  this._channel = null;
7478
7561
  }
7479
7562
  }
7563
+ async _runSync() {
7564
+ if (this._syncRunning)
7565
+ return;
7566
+ this._syncRunning = true;
7567
+ try {
7568
+ const treeResult = await StateTree.build({
7569
+ ops: this._ops,
7570
+ config: this._config
7571
+ });
7572
+ if (!treeResult.ok) {
7573
+ this.emit("sync:error", new Error(`StateTree.build failed: ${treeResult.error.message}`));
7574
+ return;
7575
+ }
7576
+ const syncResult = await sync({
7577
+ tree: treeResult.value,
7578
+ ops: this._ops,
7579
+ config: this._config
7580
+ });
7581
+ if (!syncResult.ok) {
7582
+ this.emit("sync:error", new Error(`sync failed: ${syncResult.error.message}`));
7583
+ return;
7584
+ }
7585
+ this.emit("synced", syncResult.value);
7586
+ } catch (e) {
7587
+ this.emit("sync:error", e instanceof Error ? e : new Error(String(e)));
7588
+ } finally {
7589
+ this._syncRunning = false;
7590
+ }
7591
+ }
7480
7592
  }
7481
7593
  // ../core/src/daemon/service.ts
7482
7594
  function generateServiceFile(options) {
@@ -7578,10 +7690,9 @@ var templates = {
7578
7690
  "workspace/HEARTBEAT.md",
7579
7691
  "workspace/BOOTSTRAP.md",
7580
7692
  "openclaw.json",
7581
- "cron/",
7582
7693
  "extensions/"
7583
7694
  ],
7584
- watch: ["workspace/MEMORY.md", "workspace/memory/", "workspace/skills/"],
7695
+ watch: ["workspace/MEMORY.md", "workspace/memory/", "workspace/skills/", "cron/"],
7585
7696
  release: ["workspace/sessions/"]
7586
7697
  },
7587
7698
  paranoid: {
@@ -7629,8 +7740,9 @@ var templates = {
7629
7740
  };
7630
7741
  // ../openclaw/src/plugin.ts
7631
7742
  import { readFileSync } from "node:fs";
7743
+ import { access as access2, chmod, copyFile, mkdir } from "node:fs/promises";
7632
7744
  import os from "node:os";
7633
- import { join as join2 } from "node:path";
7745
+ import { dirname as dirname5, join as join2 } from "node:path";
7634
7746
 
7635
7747
  // ../openclaw/src/guard.ts
7636
7748
  import path from "node:path";
@@ -7638,40 +7750,57 @@ var WRITE_TOOLS = new Set(["write", "edit"]);
7638
7750
  var PATH_KEYS = ["file_path", "path", "file"];
7639
7751
  function guardToolCall(toolName, params, options) {
7640
7752
  if (!WRITE_TOOLS.has(toolName.toLowerCase())) {
7641
- return { blocked: false };
7753
+ return { action: "allow" };
7642
7754
  }
7643
- let targetPath;
7755
+ let pathKey;
7756
+ let rawPath;
7644
7757
  for (const key of PATH_KEYS) {
7645
7758
  const v = params[key];
7646
7759
  if (typeof v === "string" && v.length > 0) {
7647
- targetPath = v;
7760
+ pathKey = key;
7761
+ rawPath = v;
7648
7762
  break;
7649
7763
  }
7650
7764
  }
7651
- if (targetPath && path.isAbsolute(targetPath)) {
7652
- targetPath = path.relative(options.stateDir, targetPath);
7653
- }
7654
- if (!targetPath)
7655
- return { blocked: false };
7656
- if (isStagingPath(targetPath))
7657
- return { blocked: false };
7658
- if (!isProtectedFile(options.protectFiles, targetPath))
7659
- return { blocked: false };
7765
+ if (!pathKey || !rawPath)
7766
+ return { action: "allow" };
7767
+ const isAbsolute = path.isAbsolute(rawPath);
7768
+ const relativePath = isAbsolute ? path.relative(options.stateDir, rawPath) : rawPath;
7769
+ if (isStagingPath(relativePath))
7770
+ return { action: "allow" };
7771
+ if (!isProtectedFile(options.protectFiles, relativePath))
7772
+ return { action: "allow" };
7773
+ const relativeStaging = stagingPath(relativePath);
7774
+ const redirectedPath = isAbsolute ? path.join(options.stateDir, relativeStaging) : relativeStaging;
7660
7775
  return {
7661
- blocked: true,
7662
- reason: [
7663
- `${targetPath} is protected by soulguard.`,
7664
- `To propose changes, run \`soulguard stage ${targetPath}\` to create a working copy,`,
7665
- `then edit the staged file at ${stagingPath(targetPath)}.`,
7666
- `Run \`soulguard diff\` to review your changes.`,
7667
- `Your owner will review and apply the changes.`
7668
- ].join(" ")
7776
+ action: "redirect",
7777
+ pathKey,
7778
+ originalPath: relativePath,
7779
+ redirectedPath
7669
7780
  };
7670
7781
  }
7671
7782
 
7672
7783
  // ../openclaw/src/plugin.ts
7673
7784
  var PKG_VERSION = typeof SOULGUARD_VERSION !== "undefined" ? SOULGUARD_VERSION : "0.0.0-dev";
7674
7785
  var PLUGIN_DESCRIPTION = "Identity protection for AI agents";
7786
+ var MAX_TRACKED_REDIRECTS = 1024;
7787
+ var redirectMap = new Map;
7788
+ async function ensureStagingCopy(originalAbsPath, stagingAbsPath) {
7789
+ try {
7790
+ await access2(stagingAbsPath);
7791
+ return;
7792
+ } catch {}
7793
+ await mkdir(dirname5(stagingAbsPath), { recursive: true });
7794
+ try {
7795
+ await copyFile(originalAbsPath, stagingAbsPath);
7796
+ await chmod(stagingAbsPath, 420);
7797
+ } catch {}
7798
+ }
7799
+ function buildRedirectWarning(info) {
7800
+ return `
7801
+
7802
+ [Soulguard] This edit was redirected to the staging copy at ${info.redirectedPath}. ` + `The original file ${info.originalPath} is protected. ` + "Run `soulguard diff` to review your changes. " + "Your owner will review and apply them.";
7803
+ }
7675
7804
  function createSoulguardPlugin(options) {
7676
7805
  return {
7677
7806
  id: "soulguard",
@@ -7692,7 +7821,7 @@ function createSoulguardPlugin(options) {
7692
7821
  }
7693
7822
  if (protectFiles.length === 0)
7694
7823
  return;
7695
- api.on("before_tool_call", (...args) => {
7824
+ api.on("before_tool_call", async (...args) => {
7696
7825
  const event = args[0];
7697
7826
  if (!event || typeof event !== "object" || !("toolName" in event)) {
7698
7827
  return;
@@ -7702,11 +7831,57 @@ function createSoulguardPlugin(options) {
7702
7831
  protectFiles,
7703
7832
  stateDir
7704
7833
  });
7705
- if (result.blocked) {
7834
+ if (result.action === "block") {
7706
7835
  return { block: true, blockReason: result.reason };
7707
7836
  }
7837
+ if (result.action === "redirect") {
7838
+ const originalAbsPath = join2(stateDir, result.originalPath);
7839
+ const stagingAbsPath = result.redirectedPath.startsWith("/") ? result.redirectedPath : join2(stateDir, result.redirectedPath);
7840
+ await ensureStagingCopy(originalAbsPath, stagingAbsPath);
7841
+ const toolCallId = e.toolCallId;
7842
+ if (toolCallId) {
7843
+ if (redirectMap.size >= MAX_TRACKED_REDIRECTS) {
7844
+ const oldest = redirectMap.keys().next().value;
7845
+ if (oldest)
7846
+ redirectMap.delete(oldest);
7847
+ }
7848
+ redirectMap.set(toolCallId, {
7849
+ originalPath: result.originalPath,
7850
+ redirectedPath: result.redirectedPath
7851
+ });
7852
+ }
7853
+ return {
7854
+ params: { [result.pathKey]: result.redirectedPath }
7855
+ };
7856
+ }
7708
7857
  return;
7709
7858
  });
7859
+ api.on("tool_result_persist", (...args) => {
7860
+ const event = args[0];
7861
+ if (!event?.toolCallId || !event.message)
7862
+ return;
7863
+ const info = redirectMap.get(event.toolCallId);
7864
+ if (!info)
7865
+ return;
7866
+ redirectMap.delete(event.toolCallId);
7867
+ const warning = buildRedirectWarning(info);
7868
+ const msg = event.message;
7869
+ const content = Array.isArray(msg.content) ? [...msg.content] : [];
7870
+ let lastText;
7871
+ for (let i = content.length - 1;i >= 0; i--) {
7872
+ const block = content[i];
7873
+ if (block && block.type === "text") {
7874
+ lastText = block;
7875
+ break;
7876
+ }
7877
+ }
7878
+ if (lastText && lastText.text != null) {
7879
+ lastText.text += warning;
7880
+ } else {
7881
+ content.push({ type: "text", text: warning.trimStart() });
7882
+ }
7883
+ return { message: { ...msg, content } };
7884
+ });
7710
7885
  }
7711
7886
  };
7712
7887
  }
@@ -7752,6 +7927,7 @@ export {
7752
7927
  makeDefaultConfig,
7753
7928
  isStagingPath,
7754
7929
  isProtectedFile,
7930
+ isInProtectTier,
7755
7931
  isDeleteSentinel,
7756
7932
  guardianName,
7757
7933
  getProtectOwnership,
@@ -7762,13 +7938,15 @@ export {
7762
7938
  evaluatePolicies,
7763
7939
  err,
7764
7940
  diff,
7941
+ deleteStagingEntry,
7942
+ createStagingCopy,
7943
+ create,
7765
7944
  apply,
7766
7945
  TierCommand,
7767
7946
  SyncCommand,
7768
7947
  StatusCommand,
7769
7948
  StateTreeBuildError,
7770
7949
  StateTree,
7771
- StageCommand,
7772
7950
  SoulguardDaemon,
7773
7951
  STAGING_DIR,
7774
7952
  SOULGUARD_GROUP,
@@ -7778,6 +7956,8 @@ export {
7778
7956
  MockSystemOps,
7779
7957
  LiveConsoleOutput,
7780
7958
  DiffCommand,
7959
+ DeleteCommand,
7781
7960
  DELETE_SENTINEL,
7961
+ CreateCommand,
7782
7962
  ApplyCommand
7783
7963
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "soulguard",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "Identity protection for AI agents",
5
5
  "homepage": "https://soulguard.ai",
6
6
  "license": "MIT",