@kienntpixon/dieu-phoi 0.1.2 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/setup.cjs CHANGED
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const fs = require('fs');
4
+ const os = require('os');
4
5
  const path = require('path');
5
6
  const core = require('../hook.cjs');
6
7
 
@@ -70,10 +71,11 @@ function alreadyRegistered(settings, cmd) {
70
71
  if (h.command !== cmd) continue;
71
72
  if (event === 'PostToolUse' && g.matcher === 'Agent') good++;
72
73
  else if (event === 'SubagentStop') good++;
74
+ else if (event === 'SessionStart' && g.matcher === 'compact') good++;
73
75
  }
74
76
  }
75
77
  }
76
- return ours === 2 && good === 2;
78
+ return ours === 3 && good === 3;
77
79
  }
78
80
 
79
81
  function addOurHooks(settings, hookPath) {
@@ -84,6 +86,84 @@ function addOurHooks(settings, hookPath) {
84
86
  h.PostToolUse.push({ matcher: 'Agent', hooks: [{ type: 'command', command: cmd, timeout: 10 }] });
85
87
  if (!Array.isArray(h.SubagentStop)) h.SubagentStop = [];
86
88
  h.SubagentStop.push({ hooks: [{ type: 'command', command: cmd, timeout: 10 }] });
89
+ // after /compact or auto-compact: re-inject the orchestration state rebuilt from the log
90
+ if (!Array.isArray(h.SessionStart)) h.SessionStart = [];
91
+ h.SessionStart.push({ matcher: 'compact', hooks: [{ type: 'command', command: cmd, timeout: 10 }] });
92
+ }
93
+
94
+ // ------------------------------------------------------------------ run-dir permissions
95
+
96
+ /**
97
+ * Run directory for subagent reports: ~/.dieu-phoi/runs (override: DIEU_PHOI_RUNS_DIR, absolute).
98
+ * Deliberately OUTSIDE ~/.claude: `.claude` is a Claude Code protected path, where allow rules never
99
+ * pre-approve writes, so background agents could not write their reports there.
100
+ */
101
+ function runsDir() {
102
+ return core.runsDir();
103
+ }
104
+
105
+ /** Run dir used by 0.2.0 dev builds (<claudeDir>/dieu-phoi/runs); its rules are removed by setup/uninstall. */
106
+ function legacyRunsDirOf(claudeDir) {
107
+ return path.join(path.resolve(claudeDir), 'dieu-phoi', 'runs');
108
+ }
109
+
110
+ /**
111
+ * Permission entries that let background subagents (default permission mode) read and write reports in
112
+ * `runs`: Read(<p>/**) + Edit(<p>/**) allow rules (Edit rules cover every file-writing tool; Claude
113
+ * Code ignores path rules on Write and warns about them, so no Write(...) rule) and the dir itself in
114
+ * permissions.additionalDirectories. <p> = ~/<rel> under the home dir, else //<abs> (Windows: //c/...).
115
+ */
116
+ function permissionEntries(runs, homeDir) {
117
+ const home = path.resolve(homeDir || os.homedir());
118
+ const rel = path.relative(home, runs);
119
+ let pat;
120
+ if (rel && !rel.startsWith('..') && !path.isAbsolute(rel)) {
121
+ pat = `~/${toSlash(rel)}/**`;
122
+ } else {
123
+ const abs = toSlash(runs);
124
+ const m = /^([A-Za-z]):\/(.*)$/.exec(abs);
125
+ pat = m ? `//${m[1].toLowerCase()}/${m[2]}/**` : `/${abs}/**`;
126
+ }
127
+ return { allow: [`Read(${pat})`, `Edit(${pat})`], dir: toSlash(runs) };
128
+ }
129
+
130
+ /** Add our permission entries (idempotent). Returns true when settings changed. */
131
+ function addOurPermissions(settings, entries) {
132
+ let changed = false;
133
+ if (!settings.permissions || typeof settings.permissions !== 'object' || Array.isArray(settings.permissions)) {
134
+ settings.permissions = {};
135
+ changed = true;
136
+ }
137
+ const p = settings.permissions;
138
+ if (!Array.isArray(p.allow)) { p.allow = []; changed = true; }
139
+ for (const r of entries.allow) if (!p.allow.includes(r)) { p.allow.push(r); changed = true; }
140
+ if (!Array.isArray(p.additionalDirectories)) { p.additionalDirectories = []; changed = true; }
141
+ if (!p.additionalDirectories.includes(entries.dir)) { p.additionalDirectories.push(entries.dir); changed = true; }
142
+ return changed;
143
+ }
144
+
145
+ /** Remove exactly our permission entries; a list we emptied is deleted. Returns number removed. */
146
+ function stripOurPermissions(settings, entries) {
147
+ const p = settings.permissions;
148
+ if (!p || typeof p !== 'object' || Array.isArray(p)) return 0;
149
+ let removed = 0;
150
+ const strip = (key, items) => {
151
+ if (!Array.isArray(p[key])) return;
152
+ const kept = p[key].filter((x) => !items.includes(x));
153
+ if (kept.length === p[key].length) return;
154
+ removed += p[key].length - kept.length;
155
+ if (kept.length) p[key] = kept; else delete p[key];
156
+ };
157
+ strip('allow', entries.allow);
158
+ strip('additionalDirectories', [entries.dir]);
159
+ return removed;
160
+ }
161
+
162
+ function hasOurPermissions(settings, entries) {
163
+ const p = settings && settings.permissions;
164
+ if (!p || typeof p !== 'object') return false;
165
+ return entries.allow.every((r) => Array.isArray(p.allow) && p.allow.includes(r))
166
+ && Array.isArray(p.additionalDirectories) && p.additionalDirectories.includes(entries.dir);
87
167
  }
88
168
 
89
169
  function readSettings(file) {
@@ -204,11 +284,20 @@ function setup(opts) {
204
284
  const cmd = hookCommand(paths.hook);
205
285
  let removed = 0;
206
286
  let res = { changed: false, backup: null };
287
+ let dirty = false;
207
288
  if (!(before.exists && alreadyRegistered(obj, cmd))) {
208
289
  removed = stripOurHooks(obj);
209
290
  addOurHooks(obj, paths.hook);
210
- res = writeSettings(settingsFile, before, obj);
291
+ dirty = true;
211
292
  }
293
+ // 4. run dir + permissions so background subagents can write their reports there
294
+ const perms = permissionEntries(runsDir());
295
+ fs.mkdirSync(perms.dir, { recursive: true });
296
+ // rules a 0.2.0 dev build added for <claudeDir>/dieu-phoi/runs (protected path, never worked) go away
297
+ const legacyRemoved = stripOurPermissions(obj, permissionEntries(legacyRunsDirOf(claudeDir)));
298
+ if (legacyRemoved) dirty = true;
299
+ if (addOurPermissions(obj, perms)) dirty = true;
300
+ if (dirty) res = writeSettings(settingsFile, before, obj);
212
301
 
213
302
  return {
214
303
  home,
@@ -218,6 +307,9 @@ function setup(opts) {
218
307
  settings_changed: res.changed,
219
308
  backup: res.backup,
220
309
  replaced_entries: removed,
310
+ runs_dir: perms.dir,
311
+ permissions: perms,
312
+ legacy_permissions_removed: legacyRemoved,
221
313
  config: cfg,
222
314
  };
223
315
  }
@@ -229,29 +321,41 @@ function uninstall(opts) {
229
321
  const before = readSettings(settingsFile);
230
322
  const obj = JSON.parse(JSON.stringify(before.obj));
231
323
  const removed = stripOurHooks(obj);
324
+ const permsRemoved = stripOurPermissions(obj, permissionEntries(runsDir()))
325
+ + stripOurPermissions(obj, permissionEntries(legacyRunsDirOf(claudeDir)));
232
326
  let res = { changed: false, backup: null };
233
- if (removed) res = writeSettings(settingsFile, before, obj);
327
+ if (removed || permsRemoved) res = writeSettings(settingsFile, before, obj);
234
328
  const hookFile = core.pathsFor(home).hook;
235
329
  let hookRemoved = false;
236
330
  if (opts.purge && fs.existsSync(hookFile)) {
237
331
  fs.unlinkSync(hookFile);
238
332
  hookRemoved = true;
239
333
  }
240
- return { removed, settings: settingsFile, backup: res.backup, hook_removed: hookRemoved, data_kept: core.pathsFor(home).data };
334
+ return { removed, permissions_removed: permsRemoved, settings: settingsFile, backup: res.backup, hook_removed: hookRemoved, data_kept: core.pathsFor(home).data };
241
335
  }
242
336
 
243
337
  /** Inspect settings for our registration. */
244
338
  function registration(claudeDir) {
245
339
  const file = path.join(path.resolve(claudeDir), 'settings.json');
246
- const out = { file, PostToolUse: false, SubagentStop: false, commands: [] };
340
+ const perms = permissionEntries(runsDir());
341
+ const legacy = permissionEntries(legacyRunsDirOf(claudeDir));
342
+ const out = {
343
+ file, PostToolUse: false, SubagentStop: false, SessionStart: false, commands: [], permissions: false, permission_entries: perms,
344
+ runs_dir: perms.dir, runs_dir_exists: fs.existsSync(perms.dir), legacy_permissions: false,
345
+ };
247
346
  let obj;
248
347
  try { obj = readSettings(file).obj; } catch (_) { return out; }
348
+ out.permissions = hasOurPermissions(obj, perms);
349
+ const lp = obj.permissions || {};
350
+ out.legacy_permissions = legacy.allow.some((r) => Array.isArray(lp.allow) && lp.allow.includes(r))
351
+ || (Array.isArray(lp.additionalDirectories) && lp.additionalDirectories.includes(legacy.dir));
249
352
  const hooks = obj.hooks || {};
250
- for (const ev of ['PostToolUse', 'SubagentStop']) {
353
+ for (const ev of ['PostToolUse', 'SubagentStop', 'SessionStart']) {
251
354
  for (const g of Array.isArray(hooks[ev]) ? hooks[ev] : []) {
252
355
  for (const h of (g && Array.isArray(g.hooks) ? g.hooks : [])) {
253
356
  if (!isOurs(h)) continue;
254
357
  if (ev === 'PostToolUse' && g.matcher && !/\bAgent\b/.test(g.matcher)) continue;
358
+ if (ev === 'SessionStart' && g.matcher !== 'compact') continue;
255
359
  out[ev] = true;
256
360
  out.commands.push(h.command);
257
361
  }
@@ -262,5 +366,5 @@ function registration(claudeDir) {
262
366
 
263
367
  module.exports = {
264
368
  MARKER, setup, uninstall, registration, stripOurHooks, addOurHooks, alreadyRegistered, hookCommand,
265
- skillVersionOf, findSkillDir, tsStamp,
369
+ skillVersionOf, findSkillDir, tsStamp, runsDir, legacyRunsDirOf, permissionEntries, addOurPermissions, stripOurPermissions, hasOurPermissions,
266
370
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kienntpixon/dieu-phoi",
3
- "version": "0.1.2",
3
+ "version": "0.2.1",
4
4
  "description": "Hook + CLI ghi log subagent của Claude Code và chọn model/effort theo chi phí + thời gian tới lúc nghiệm thu.",
5
5
  "license": "MIT",
6
6
  "author": "kiennt <kiennt@pixon.games>",