@rubytech/create-maxy-code 0.1.61 → 0.1.63
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/__tests__/installer-specialist-registration.test.js +112 -0
- package/dist/index.js +22 -0
- package/dist/specialist-registration.js +78 -0
- package/package.json +1 -1
- package/payload/platform/plugins/admin/PLUGIN.md +22 -0
- package/payload/platform/plugins/admin/hooks/__tests__/turn-completed-graph-write.test.sh +50 -13
- package/payload/platform/plugins/admin/hooks/turn-completed-graph-write.sh +22 -21
- package/payload/platform/plugins/contacts/PLUGIN.md +9 -0
- package/payload/platform/plugins/docs/references/admin-session.md +55 -7
- package/payload/platform/plugins/email/PLUGIN.md +9 -0
- package/payload/platform/plugins/epc/PLUGIN.md +5 -0
- package/payload/platform/plugins/loop/PLUGIN.md +27 -0
- package/payload/platform/plugins/memory/PLUGIN.md +37 -0
- package/payload/platform/plugins/memory/mcp/dist/index.js +3 -3
- package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/live-schema-source.test.js +137 -75
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/live-schema-source.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/live-schema-source.d.ts +0 -17
- package/payload/platform/plugins/memory/mcp/dist/lib/live-schema-source.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/live-schema-source.js +32 -4
- package/payload/platform/plugins/memory/mcp/dist/lib/live-schema-source.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/log-ingest.d.ts +65 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/log-ingest.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/log-ingest.js +182 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/log-ingest.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/conversation-archive-enrich-rejection.test.js +8 -8
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/conversation-archive-enrich-rejection.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/log-ingest.test.d.ts +2 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/log-ingest.test.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/log-ingest.test.js +241 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/log-ingest.test.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/conversation-archive-derive-insights.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/conversation-archive-derive-insights.js +9 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/conversation-archive-derive-insights.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/conversation-archive-enrich-rejection.d.ts +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/conversation-archive-enrich-rejection.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/conversation-archive-enrich-rejection.js +9 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/conversation-archive-enrich-rejection.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/conversation-memory-expunge.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/conversation-memory-expunge.js +10 -2
- package/payload/platform/plugins/memory/mcp/dist/tools/conversation-memory-expunge.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/graph-prune-denylist-add.d.ts +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/graph-prune-denylist-add.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/graph-prune-denylist-add.js +5 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/graph-prune-denylist-add.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/graph-prune-denylist-remove.d.ts +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/graph-prune-denylist-remove.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/graph-prune-denylist-remove.js +5 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/graph-prune-denylist-remove.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-archive-write.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-archive-write.js +4 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-archive-write.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-classify.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-classify.js +4 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-classify.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-delete.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-delete.js +4 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-delete.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-edit-attachment.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-edit-attachment.js +4 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-edit-attachment.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-empty-trash.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-empty-trash.js +4 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-empty-trash.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest-extract.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest-extract.js +4 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest-extract.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest-web.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest-web.js +4 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest-web.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.js +5 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-reindex.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-reindex.js +4 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-reindex.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-rename-attachment.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-rename-attachment.js +4 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-rename-attachment.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-restore.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-restore.js +4 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-restore.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.js +5 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.js +6 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/profile-delete.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/profile-delete.js +4 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/profile-delete.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/profile-update.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/profile-update.js +4 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/profile-update.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/vitest.config.ts +10 -3
- package/payload/platform/plugins/outlook/PLUGIN.md +7 -0
- package/payload/platform/plugins/preval/.claude-plugin/plugin.json +1 -1
- package/payload/platform/plugins/preval/PLUGIN.md +22 -12
- package/payload/platform/plugins/preval/hooks/preval-prompt-required.sh +140 -0
- package/payload/platform/plugins/preval/skills/property-preval/SKILL.md +77 -19
- package/payload/platform/plugins/preval/skills/property-preval/references/__tests__/adherence.test.py +373 -0
- package/payload/platform/plugins/preval/skills/property-preval/references/render.py +390 -47
- package/payload/platform/plugins/preval/skills/property-preval/references/template-inputs.schema.json +32 -21
- package/payload/platform/plugins/preval/skills/property-preval/references/template.html +0 -1
- package/payload/platform/plugins/property-data/PLUGIN.md +22 -0
- package/payload/platform/plugins/replicate/PLUGIN.md +3 -0
- package/payload/platform/plugins/scheduling/PLUGIN.md +8 -0
- package/payload/platform/plugins/tasks/PLUGIN.md +14 -0
- package/payload/platform/plugins/telegram/PLUGIN.md +3 -0
- package/payload/platform/plugins/waitlist/PLUGIN.md +7 -0
- package/payload/platform/plugins/whatsapp/PLUGIN.md +12 -0
- package/payload/platform/plugins/workflows/PLUGIN.md +8 -0
- package/payload/platform/services/claude-session-manager/dist/index.js +39 -1
- package/payload/platform/services/claude-session-manager/dist/index.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts +21 -0
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.js +148 -41
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/specialist-drift.d.ts +23 -0
- package/payload/platform/services/claude-session-manager/dist/specialist-drift.d.ts.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/specialist-drift.js +141 -0
- package/payload/platform/services/claude-session-manager/dist/specialist-drift.js.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/tool-surface.d.ts +13 -2
- package/payload/platform/services/claude-session-manager/dist/tool-surface.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/tool-surface.js +135 -37
- package/payload/platform/services/claude-session-manager/dist/tool-surface.js.map +1 -1
- package/payload/premium-plugins/real-agent/agents/compliance.md +1 -1
- package/payload/premium-plugins/real-agent/plugins/epc/PLUGIN.md +5 -0
- package/payload/premium-plugins/real-agent/plugins/loop/PLUGIN.md +27 -0
- package/payload/premium-plugins/real-agent/plugins/preval/.claude-plugin/plugin.json +1 -1
- package/payload/premium-plugins/real-agent/plugins/preval/PLUGIN.md +22 -12
- package/payload/premium-plugins/real-agent/plugins/preval/hooks/preval-prompt-required.sh +140 -0
- package/payload/premium-plugins/real-agent/plugins/preval/skills/property-preval/SKILL.md +77 -19
- package/payload/premium-plugins/real-agent/plugins/preval/skills/property-preval/references/__tests__/adherence.test.py +373 -0
- package/payload/premium-plugins/real-agent/plugins/preval/skills/property-preval/references/render.py +390 -47
- package/payload/premium-plugins/real-agent/plugins/preval/skills/property-preval/references/template-inputs.schema.json +32 -21
- package/payload/premium-plugins/real-agent/plugins/preval/skills/property-preval/references/template.html +0 -1
- package/payload/premium-plugins/real-agent/plugins/property-data/PLUGIN.md +22 -0
- package/payload/server/public/assets/{admin-DhN3G8W7.js → admin-ymReS1mN.js} +1 -1
- package/payload/server/public/assets/{data-B2ZVXOcE.js → data-D6U2e85B.js} +1 -1
- package/payload/server/public/assets/{graph-BAMGPHrK.js → graph-C274PN27.js} +1 -1
- package/payload/server/public/assets/{graph-labels-D9eBbvxo.js → graph-labels-DQFzd1FY.js} +1 -1
- package/payload/server/public/assets/{page-DjdVMWCz.js → page-D-OLjort.js} +1 -1
- package/payload/server/public/assets/{page-CV27Al6Z.js → page-DFFt9_Q0.js} +1 -1
- package/payload/server/public/data.html +3 -3
- package/payload/server/public/graph.html +3 -3
- package/payload/server/public/index.html +4 -4
- package/payload/server/server.js +1 -1
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// Task 165 — installer must register every bundled specialist subagent
|
|
2
|
+
// at $CLAUDE_CONFIG_DIR/agents/<name>.md so Claude Code's `--agent <name>`
|
|
3
|
+
// discovery resolves. Pre-165 the installer left those names unreachable;
|
|
4
|
+
// every recorder spawn silently fell back to the admin agent.
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
import assert from "node:assert/strict";
|
|
7
|
+
import { mkdtempSync, mkdirSync, writeFileSync, readlinkSync, lstatSync, rmSync, symlinkSync, readdirSync } from "node:fs";
|
|
8
|
+
import { tmpdir } from "node:os";
|
|
9
|
+
import { join, resolve } from "node:path";
|
|
10
|
+
import { registerSpecialistAgentsAt, SpecialistSymlinkCollision } from "../specialist-registration.js";
|
|
11
|
+
function makeTemplates(dir, names) {
|
|
12
|
+
mkdirSync(dir, { recursive: true });
|
|
13
|
+
for (const n of names)
|
|
14
|
+
writeFileSync(join(dir, `${n}.md`), `---\nname: ${n}\ndescription: "x"\ntools: mcp__x__y\n---\n# ${n}\n`);
|
|
15
|
+
}
|
|
16
|
+
test("registers every bundled specialist as a symlink under agentsDir", () => {
|
|
17
|
+
const templatesDir = mkdtempSync(join(tmpdir(), "t165-tpl-"));
|
|
18
|
+
const agentsDir = mkdtempSync(join(tmpdir(), "t165-agents-"));
|
|
19
|
+
try {
|
|
20
|
+
const names = ["content-producer", "database-operator", "librarian", "personal-assistant", "project-manager", "research-assistant"];
|
|
21
|
+
makeTemplates(templatesDir, names);
|
|
22
|
+
const result = registerSpecialistAgentsAt(templatesDir, agentsDir);
|
|
23
|
+
assert.equal(result.skipped, null);
|
|
24
|
+
assert.deepEqual(result.registered.sort(), [...names].sort());
|
|
25
|
+
for (const n of names) {
|
|
26
|
+
const linkPath = join(agentsDir, `${n}.md`);
|
|
27
|
+
const stat = lstatSync(linkPath);
|
|
28
|
+
assert.ok(stat.isSymbolicLink(), `${n}.md must be a symlink`);
|
|
29
|
+
const target = resolve(agentsDir, readlinkSync(linkPath));
|
|
30
|
+
assert.equal(target, resolve(templatesDir, `${n}.md`));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
finally {
|
|
34
|
+
rmSync(templatesDir, { recursive: true, force: true });
|
|
35
|
+
rmSync(agentsDir, { recursive: true, force: true });
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
test("re-run refreshes a stale symlink pointing at the wrong target", () => {
|
|
39
|
+
const templatesDir = mkdtempSync(join(tmpdir(), "t165-tpl-"));
|
|
40
|
+
const agentsDir = mkdtempSync(join(tmpdir(), "t165-agents-"));
|
|
41
|
+
try {
|
|
42
|
+
makeTemplates(templatesDir, ["database-operator"]);
|
|
43
|
+
// Seed a stale symlink at the destination pointing somewhere else.
|
|
44
|
+
const stalePath = join(templatesDir, "_stale.md");
|
|
45
|
+
writeFileSync(stalePath, "stale\n");
|
|
46
|
+
const linkPath = join(agentsDir, "database-operator.md");
|
|
47
|
+
symlinkSync(stalePath, linkPath);
|
|
48
|
+
const beforeTarget = resolve(agentsDir, readlinkSync(linkPath));
|
|
49
|
+
assert.equal(beforeTarget, resolve(stalePath));
|
|
50
|
+
registerSpecialistAgentsAt(templatesDir, agentsDir);
|
|
51
|
+
const afterTarget = resolve(agentsDir, readlinkSync(linkPath));
|
|
52
|
+
assert.equal(afterTarget, resolve(templatesDir, "database-operator.md"));
|
|
53
|
+
}
|
|
54
|
+
finally {
|
|
55
|
+
rmSync(templatesDir, { recursive: true, force: true });
|
|
56
|
+
rmSync(agentsDir, { recursive: true, force: true });
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
test("idempotent — second run leaves the same symlink", () => {
|
|
60
|
+
const templatesDir = mkdtempSync(join(tmpdir(), "t165-tpl-"));
|
|
61
|
+
const agentsDir = mkdtempSync(join(tmpdir(), "t165-agents-"));
|
|
62
|
+
try {
|
|
63
|
+
makeTemplates(templatesDir, ["database-operator"]);
|
|
64
|
+
registerSpecialistAgentsAt(templatesDir, agentsDir);
|
|
65
|
+
const linkPath = join(agentsDir, "database-operator.md");
|
|
66
|
+
const firstStat = lstatSync(linkPath);
|
|
67
|
+
const firstTarget = readlinkSync(linkPath);
|
|
68
|
+
registerSpecialistAgentsAt(templatesDir, agentsDir);
|
|
69
|
+
const secondStat = lstatSync(linkPath);
|
|
70
|
+
const secondTarget = readlinkSync(linkPath);
|
|
71
|
+
assert.equal(firstTarget, secondTarget);
|
|
72
|
+
assert.ok(secondStat.isSymbolicLink());
|
|
73
|
+
// The number of entries in agentsDir hasn't grown.
|
|
74
|
+
assert.deepEqual(readdirSync(agentsDir).sort(), ["database-operator.md"]);
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
rmSync(templatesDir, { recursive: true, force: true });
|
|
78
|
+
rmSync(agentsDir, { recursive: true, force: true });
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
test("skips with reason=no-templates-dir when templates dir is absent", () => {
|
|
82
|
+
const agentsDir = mkdtempSync(join(tmpdir(), "t165-agents-"));
|
|
83
|
+
const missingDir = join(agentsDir, "_definitely-not-here");
|
|
84
|
+
try {
|
|
85
|
+
const r = registerSpecialistAgentsAt(missingDir, agentsDir);
|
|
86
|
+
assert.equal(r.skipped, "no-templates-dir");
|
|
87
|
+
assert.deepEqual(r.registered, []);
|
|
88
|
+
}
|
|
89
|
+
finally {
|
|
90
|
+
rmSync(agentsDir, { recursive: true, force: true });
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
test("collides loudly on a regular file at the destination (no silent overwrite)", () => {
|
|
94
|
+
const templatesDir = mkdtempSync(join(tmpdir(), "t165-tpl-"));
|
|
95
|
+
const agentsDir = mkdtempSync(join(tmpdir(), "t165-agents-"));
|
|
96
|
+
try {
|
|
97
|
+
makeTemplates(templatesDir, ["database-operator"]);
|
|
98
|
+
// Drop a regular file at the destination — the installer must
|
|
99
|
+
// never clobber it. The function under test throws
|
|
100
|
+
// SpecialistSymlinkCollision; index.ts maps that to process.exit(1).
|
|
101
|
+
const linkPath = join(agentsDir, "database-operator.md");
|
|
102
|
+
writeFileSync(linkPath, "operator-owned\n");
|
|
103
|
+
assert.throws(() => registerSpecialistAgentsAt(templatesDir, agentsDir), (err) => err instanceof SpecialistSymlinkCollision && err.linkPath === linkPath);
|
|
104
|
+
// The regular file was not unlinked.
|
|
105
|
+
const stat = lstatSync(linkPath);
|
|
106
|
+
assert.ok(!stat.isSymbolicLink(), "regular file must remain untouched after collision");
|
|
107
|
+
}
|
|
108
|
+
finally {
|
|
109
|
+
rmSync(templatesDir, { recursive: true, force: true });
|
|
110
|
+
rmSync(agentsDir, { recursive: true, force: true });
|
|
111
|
+
}
|
|
112
|
+
});
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { execFileSync, spawn, spawnSync } from "node:child_process";
|
|
3
3
|
import { existsSync, mkdirSync, writeFileSync, cpSync, readFileSync, rmSync, readdirSync, appendFileSync, openSync, closeSync, chmodSync, symlinkSync, unlinkSync, lstatSync, statSync, readlinkSync, realpathSync, accessSync, constants as fsConstants } from "node:fs";
|
|
4
|
+
import { registerSpecialistAgentsAt, SpecialistSymlinkCollision } from "./specialist-registration.js";
|
|
4
5
|
import { resolve, join, dirname } from "node:path";
|
|
5
6
|
import { randomBytes } from "node:crypto";
|
|
6
7
|
import { resolveInstallPortFromFs, buildMaxyUnitFile, buildClaudeSessionManagerUnitFile } from "./port-resolution.js";
|
|
@@ -2308,6 +2309,14 @@ function setupAccount() {
|
|
|
2308
2309
|
logFile(` [neo4j] passing NEO4J_URI=${neo4jUri} to seed`);
|
|
2309
2310
|
shell("bash", [seedScript], { cwd: INSTALL_DIR, env: neo4jEnv });
|
|
2310
2311
|
}
|
|
2312
|
+
// Task 165 — register every bundled specialist subagent at
|
|
2313
|
+
// $CLAUDE_CONFIG_DIR/agents/<name>.md. Claude Code's `--agent <name>`
|
|
2314
|
+
// discovery only resolves under this path; without these symlinks,
|
|
2315
|
+
// every `--agent database-operator` etc. silently runs as the admin
|
|
2316
|
+
// agent on admin IDENTITY/SOUL scaffolding. Symlinks (not copies) so
|
|
2317
|
+
// a `npx -y @rubytech/create-maxy-code` upgrade picks up template
|
|
2318
|
+
// changes without a second seed run.
|
|
2319
|
+
registerSpecialistAgents();
|
|
2311
2320
|
// install-time defaults. Stamp `account.json` with the
|
|
2312
2321
|
// brand's default plugin set + default outputStyle / thinkingView, and
|
|
2313
2322
|
// write a minimal admin SOUL.md. The agent boots into a working state
|
|
@@ -2409,6 +2418,19 @@ function writeInstallDefaults(accountId) {
|
|
|
2409
2418
|
// correct brand regardless of who owns
|
|
2410
2419
|
// the shortcut symlink.
|
|
2411
2420
|
// ---------------------------------------------------------------------------
|
|
2421
|
+
function registerSpecialistAgents() {
|
|
2422
|
+
try {
|
|
2423
|
+
registerSpecialistAgentsAt(join(INSTALL_DIR, "platform/templates/specialists/agents"), join(CLAUDE_CONFIG_DIR, "agents"), (line) => { console.log(line); logFile(line); });
|
|
2424
|
+
}
|
|
2425
|
+
catch (err) {
|
|
2426
|
+
if (err instanceof SpecialistSymlinkCollision) {
|
|
2427
|
+
console.error(`Setup failed: ${err.message}`);
|
|
2428
|
+
console.error(` Remove the file and re-run.`);
|
|
2429
|
+
process.exit(1);
|
|
2430
|
+
}
|
|
2431
|
+
throw err;
|
|
2432
|
+
}
|
|
2433
|
+
}
|
|
2412
2434
|
function createTunnelSymlink(linkPath, target) {
|
|
2413
2435
|
const targetAbs = resolve(target);
|
|
2414
2436
|
let lstat = null;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// Task 165 — bundled-specialist registration.
|
|
2
|
+
//
|
|
3
|
+
// Claude Code resolves `--agent <name>` by reading
|
|
4
|
+
// `$CLAUDE_CONFIG_DIR/agents/<name>.md`. The installer ships every
|
|
5
|
+
// bundled specialist as a file under
|
|
6
|
+
// `<INSTALL_DIR>/platform/templates/specialists/agents/*.md`; this
|
|
7
|
+
// module symlinks each into the per-brand `$CLAUDE_CONFIG_DIR/agents/`
|
|
8
|
+
// so `claude --agent database-operator` resolves on first call.
|
|
9
|
+
//
|
|
10
|
+
// Idempotent. Re-running the installer after a template change
|
|
11
|
+
// refreshes stale symlinks. A regular file at the destination is a
|
|
12
|
+
// collision — never silently overwritten; the operator must remove it
|
|
13
|
+
// explicitly (we throw and the caller decides whether to exit).
|
|
14
|
+
//
|
|
15
|
+
// Lives outside index.ts so unit tests can import the function without
|
|
16
|
+
// triggering index.ts top-level installer code (which fatals on missing
|
|
17
|
+
// brand.json in a test environment).
|
|
18
|
+
import { existsSync, mkdirSync, readdirSync, lstatSync, readlinkSync, symlinkSync, unlinkSync } from "node:fs";
|
|
19
|
+
import { dirname, join, resolve } from "node:path";
|
|
20
|
+
export class SpecialistSymlinkCollision extends Error {
|
|
21
|
+
linkPath;
|
|
22
|
+
constructor(linkPath) {
|
|
23
|
+
super(`${linkPath} collision (regular file) — remove and re-run`);
|
|
24
|
+
this.linkPath = linkPath;
|
|
25
|
+
this.name = "SpecialistSymlinkCollision";
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export function registerSpecialistAgentsAt(templatesDir, agentsDir, log = () => { }) {
|
|
29
|
+
if (!existsSync(templatesDir)) {
|
|
30
|
+
log(` [specialist-registration] SKIPPED reason=templates-dir-missing path=${templatesDir}`);
|
|
31
|
+
return { registered: [], skipped: 'no-templates-dir' };
|
|
32
|
+
}
|
|
33
|
+
mkdirSync(agentsDir, { recursive: true });
|
|
34
|
+
const entries = readdirSync(templatesDir).filter((n) => n.endsWith(".md"));
|
|
35
|
+
if (entries.length === 0) {
|
|
36
|
+
log(` [specialist-registration] SKIPPED reason=no-bundled-specialists path=${templatesDir}`);
|
|
37
|
+
return { registered: [], skipped: 'no-bundled-specialists' };
|
|
38
|
+
}
|
|
39
|
+
const registered = [];
|
|
40
|
+
for (const file of entries.sort()) {
|
|
41
|
+
const targetAbs = join(templatesDir, file);
|
|
42
|
+
const linkPath = join(agentsDir, file);
|
|
43
|
+
const name = file.replace(/\.md$/, "");
|
|
44
|
+
replaceSpecialistSymlink(linkPath, targetAbs);
|
|
45
|
+
registered.push(name);
|
|
46
|
+
log(` [specialist-registration] installer-specialist-registered name=${name} link=${targetAbs}`);
|
|
47
|
+
}
|
|
48
|
+
return { registered, skipped: null };
|
|
49
|
+
}
|
|
50
|
+
export function replaceSpecialistSymlink(linkPath, targetAbs) {
|
|
51
|
+
let lstat = null;
|
|
52
|
+
try {
|
|
53
|
+
lstat = lstatSync(linkPath);
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
const code = err.code;
|
|
57
|
+
if (code !== "ENOENT")
|
|
58
|
+
throw err;
|
|
59
|
+
}
|
|
60
|
+
if (lstat === null) {
|
|
61
|
+
symlinkSync(targetAbs, linkPath);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (!lstat.isSymbolicLink()) {
|
|
65
|
+
throw new SpecialistSymlinkCollision(linkPath);
|
|
66
|
+
}
|
|
67
|
+
let resolvedTarget;
|
|
68
|
+
try {
|
|
69
|
+
resolvedTarget = resolve(dirname(linkPath), readlinkSync(linkPath));
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
resolvedTarget = "<unreadable>";
|
|
73
|
+
}
|
|
74
|
+
if (resolvedTarget === targetAbs)
|
|
75
|
+
return;
|
|
76
|
+
unlinkSync(linkPath);
|
|
77
|
+
symlinkSync(targetAbs, linkPath);
|
|
78
|
+
}
|
package/package.json
CHANGED
|
@@ -4,48 +4,70 @@ description: "Platform administration plugin. Provides system-status, public-hos
|
|
|
4
4
|
tools:
|
|
5
5
|
- name: system-status
|
|
6
6
|
publicAllowlist: false
|
|
7
|
+
adminAllowlist: false
|
|
7
8
|
- name: public-hostname
|
|
8
9
|
publicAllowlist: false
|
|
10
|
+
adminAllowlist: false
|
|
9
11
|
- name: brand-settings
|
|
10
12
|
publicAllowlist: false
|
|
13
|
+
adminAllowlist: false
|
|
11
14
|
- name: account-manage
|
|
12
15
|
publicAllowlist: false
|
|
16
|
+
adminAllowlist: false
|
|
13
17
|
- name: account-update
|
|
14
18
|
publicAllowlist: false
|
|
19
|
+
adminAllowlist: false
|
|
15
20
|
- name: admin-add
|
|
16
21
|
publicAllowlist: false
|
|
22
|
+
adminAllowlist: true
|
|
17
23
|
- name: admin-remove
|
|
18
24
|
publicAllowlist: false
|
|
25
|
+
adminAllowlist: true
|
|
19
26
|
- name: admin-list
|
|
20
27
|
publicAllowlist: false
|
|
28
|
+
adminAllowlist: true
|
|
21
29
|
- name: admin-update-pin
|
|
22
30
|
publicAllowlist: false
|
|
31
|
+
adminAllowlist: true
|
|
23
32
|
- name: agent-list
|
|
24
33
|
publicAllowlist: false
|
|
34
|
+
adminAllowlist: true
|
|
25
35
|
- name: agent-config-read
|
|
26
36
|
publicAllowlist: false
|
|
37
|
+
adminAllowlist: true
|
|
27
38
|
- name: logs-read
|
|
28
39
|
publicAllowlist: false
|
|
40
|
+
adminAllowlist: false
|
|
29
41
|
- name: plugin-read
|
|
30
42
|
publicAllowlist: false
|
|
43
|
+
adminAllowlist: false
|
|
31
44
|
- name: skill-load
|
|
32
45
|
publicAllowlist: false
|
|
46
|
+
adminAllowlist: true
|
|
33
47
|
- name: store-skill
|
|
34
48
|
publicAllowlist: false
|
|
49
|
+
adminAllowlist: true
|
|
35
50
|
- name: session-reset
|
|
36
51
|
publicAllowlist: false
|
|
52
|
+
adminAllowlist: true
|
|
37
53
|
- name: session-resume
|
|
38
54
|
publicAllowlist: false
|
|
55
|
+
adminAllowlist: true
|
|
39
56
|
- name: file-attach
|
|
40
57
|
publicAllowlist: false
|
|
58
|
+
adminAllowlist: false
|
|
41
59
|
- name: action-pending
|
|
42
60
|
publicAllowlist: false
|
|
61
|
+
adminAllowlist: true
|
|
43
62
|
- name: action-approve
|
|
44
63
|
publicAllowlist: false
|
|
64
|
+
adminAllowlist: true
|
|
45
65
|
- name: action-reject
|
|
46
66
|
publicAllowlist: false
|
|
67
|
+
adminAllowlist: true
|
|
47
68
|
- name: action-edit
|
|
48
69
|
publicAllowlist: false
|
|
70
|
+
adminAllowlist: true
|
|
49
71
|
hidden:
|
|
50
72
|
- remote-auth-status
|
|
51
73
|
- remote-auth-set-password
|
|
@@ -184,10 +184,11 @@ run_hook "admin" "" "$ENVELOPE"
|
|
|
184
184
|
[[ -z "$HOOK_STDERR" ]] || fail "case-5 stderr must be empty, got: $HOOK_STDERR"
|
|
185
185
|
|
|
186
186
|
# 5a. exactly ONE `trigger` log-ingest line carrying the operator session id
|
|
187
|
-
# and `
|
|
188
|
-
|
|
187
|
+
# and `conversationBytes=<n>` (Task 165 — full transcript walker
|
|
188
|
+
# replaces last-turn-only extractor; field is renamed for clarity).
|
|
189
|
+
TRIGGER_COUNT=$(ingest_lines | grep -cE "^trigger sessionId=${OP_ID} turnIndex=0 transcriptBytes=[0-9]+ conversationBytes=[0-9]+$" || true)
|
|
189
190
|
if [[ "$TRIGGER_COUNT" -eq 1 ]]; then
|
|
190
|
-
pass "case-5a exactly one trigger line emitted via log-ingest carrying operator id +
|
|
191
|
+
pass "case-5a exactly one trigger line emitted via log-ingest carrying operator id + conversationBytes"
|
|
191
192
|
else
|
|
192
193
|
fail "case-5a expected exactly 1 trigger line carrying ${OP_ID}, got $TRIGGER_COUNT (lines: $(ingest_lines))"
|
|
193
194
|
fi
|
|
@@ -209,7 +210,9 @@ BODY_OK=$(printf '%s' "$RECORDER_BODY" | python3 -c '
|
|
|
209
210
|
import sys, json
|
|
210
211
|
op = sys.argv[1]
|
|
211
212
|
INSTRUCTION = "update the graph with any new or missing information or intent"
|
|
212
|
-
|
|
213
|
+
# Task 165 — initialMessage carries the full transcript layout
|
|
214
|
+
# "user: <text>\nassistant: <text>\n…", not just the last assistant text.
|
|
215
|
+
EXPECTED_CONVERSATION = "user: hello operator\nassistant: hello, assistant reply"
|
|
213
216
|
try:
|
|
214
217
|
b = json.load(sys.stdin)
|
|
215
218
|
msg = b.get("initialMessage") or ""
|
|
@@ -219,7 +222,7 @@ try:
|
|
|
219
222
|
b.get("model") == "haiku",
|
|
220
223
|
b.get("adminSessionId") == op,
|
|
221
224
|
b.get("channel") == "browser",
|
|
222
|
-
msg == INSTRUCTION + "\n\n" +
|
|
225
|
+
msg == INSTRUCTION + "\n\n" + EXPECTED_CONVERSATION,
|
|
223
226
|
"Conversation id:" not in msg,
|
|
224
227
|
"Conversation transcript:" not in msg,
|
|
225
228
|
"originatingSessionId" not in b,
|
|
@@ -229,7 +232,7 @@ except Exception as e:
|
|
|
229
232
|
print("parse-fail:"+str(e))
|
|
230
233
|
' "$OP_ID" 2>/dev/null)
|
|
231
234
|
if [[ "$BODY_OK" == "yes" ]]; then
|
|
232
|
-
pass "case-5c body
|
|
235
|
+
pass "case-5c body carries full transcript layout (user: …\\nassistant: …) — Task 165"
|
|
233
236
|
else
|
|
234
237
|
fail "case-5c body shape wrong ($BODY_OK): $RECORDER_BODY"
|
|
235
238
|
fi
|
|
@@ -249,9 +252,10 @@ else
|
|
|
249
252
|
pass "case-5e no legacy stderr emissions"
|
|
250
253
|
fi
|
|
251
254
|
|
|
252
|
-
# --- Case 6 (Task
|
|
253
|
-
# trigger-skipped reason=
|
|
254
|
-
# line first (
|
|
255
|
+
# --- Case 6 (Task 165): transcript has zero qualifying records → --------
|
|
256
|
+
# trigger-skipped reason=conversation-empty. The hook emits the
|
|
257
|
+
# trigger line first (conversationBytes=0) and then the new skip
|
|
258
|
+
# reason.
|
|
255
259
|
EMPTY_TRANSCRIPT=$(mktemp); TMPFILES+=("$EMPTY_TRANSCRIPT")
|
|
256
260
|
printf '{"type":"system","summary":"noop"}\n' > "$EMPTY_TRANSCRIPT"
|
|
257
261
|
EMPTY_ENVELOPE=$(python3 -c '
|
|
@@ -261,14 +265,47 @@ print(json.dumps({"session_id": sys.argv[1], "transcript_path": sys.argv[2]}))
|
|
|
261
265
|
: > "$REQ_LOG"
|
|
262
266
|
run_hook "admin" "" "$EMPTY_ENVELOPE"
|
|
263
267
|
[[ "$HOOK_RC" -eq 0 ]] || fail "case-6 rc=$HOOK_RC"
|
|
264
|
-
if ingest_lines | grep -qE "^trigger-skipped sessionId=${OP_ID} reason=
|
|
265
|
-
pass "case-6
|
|
268
|
+
if ingest_lines | grep -qE "^trigger-skipped sessionId=${OP_ID} reason=conversation-empty$"; then
|
|
269
|
+
pass "case-6 zero user/assistant records → trigger-skipped reason=conversation-empty emitted"
|
|
266
270
|
else
|
|
267
|
-
fail "case-6 expected trigger-skipped
|
|
271
|
+
fail "case-6 expected trigger-skipped conversation-empty, got: $(ingest_lines)"
|
|
268
272
|
fi
|
|
269
273
|
# And no recorder-spawn POST is made.
|
|
270
274
|
if grep -qE '^/api/admin/claude-sessions ' "$REQ_LOG"; then
|
|
271
|
-
fail "case-6 recorder-spawn must NOT be called when
|
|
275
|
+
fail "case-6 recorder-spawn must NOT be called when conversation-empty"
|
|
276
|
+
fi
|
|
277
|
+
|
|
278
|
+
# --- Case 7 (Task 165): three-turn conversation walks user/assistant/user
|
|
279
|
+
# in transcript order; layout is "user: …\nassistant: …\nuser: …". ---
|
|
280
|
+
THREE_TURN_TRANSCRIPT=$(mktemp); TMPFILES+=("$THREE_TURN_TRANSCRIPT")
|
|
281
|
+
{
|
|
282
|
+
printf '{"type":"user","message":{"role":"user","content":"hello"}}\n'
|
|
283
|
+
printf '{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"what'"'"'s your business?"}]}}\n'
|
|
284
|
+
printf '{"type":"user","message":{"role":"user","content":"Smalleys"}}\n'
|
|
285
|
+
} > "$THREE_TURN_TRANSCRIPT"
|
|
286
|
+
THREE_ENVELOPE=$(python3 -c '
|
|
287
|
+
import json, sys
|
|
288
|
+
print(json.dumps({"session_id": sys.argv[1], "transcript_path": sys.argv[2]}))
|
|
289
|
+
' "$OP_ID" "$THREE_TURN_TRANSCRIPT")
|
|
290
|
+
: > "$REQ_LOG"
|
|
291
|
+
run_hook "admin" "" "$THREE_ENVELOPE"
|
|
292
|
+
[[ "$HOOK_RC" -eq 0 ]] || fail "case-7 rc=$HOOK_RC stderr=$HOOK_STDERR"
|
|
293
|
+
THREE_BODY=$(grep -E '^/api/admin/claude-sessions ' "$REQ_LOG" | head -1 | cut -f2-)
|
|
294
|
+
THREE_OK=$(printf '%s' "$THREE_BODY" | python3 -c '
|
|
295
|
+
import sys, json
|
|
296
|
+
INSTRUCTION = "update the graph with any new or missing information or intent"
|
|
297
|
+
EXPECTED = "user: hello\nassistant: what'"'"'s your business?\nuser: Smalleys"
|
|
298
|
+
try:
|
|
299
|
+
b = json.load(sys.stdin)
|
|
300
|
+
msg = b.get("initialMessage") or ""
|
|
301
|
+
print("yes" if msg == INSTRUCTION + "\n\n" + EXPECTED else "no:"+repr(msg))
|
|
302
|
+
except Exception as e:
|
|
303
|
+
print("parse-fail:"+str(e))
|
|
304
|
+
' 2>/dev/null)
|
|
305
|
+
if [[ "$THREE_OK" == "yes" ]]; then
|
|
306
|
+
pass "case-7 three-turn conversation walks user/assistant/user in order"
|
|
307
|
+
else
|
|
308
|
+
fail "case-7 expected multi-turn layout, got: $THREE_OK | body: $THREE_BODY"
|
|
272
309
|
fi
|
|
273
310
|
|
|
274
311
|
# --- Summary ------------------------------------------------------------
|
|
@@ -23,8 +23,8 @@
|
|
|
23
23
|
# "database-operator" (recursion guard)
|
|
24
24
|
# - Stop-hook stdin must be non-empty → reason=empty-stdin
|
|
25
25
|
# - transcript_path must exist on disk → reason=missing-transcript
|
|
26
|
-
# -
|
|
27
|
-
# must
|
|
26
|
+
# - at least one user or assistant record with → reason=conversation-empty
|
|
27
|
+
# non-empty text must exist in the JSONL (Task 165)
|
|
28
28
|
#
|
|
29
29
|
# Input: Claude Code's Stop hook stdin shape
|
|
30
30
|
# { "session_id": "<intrinsic>", "transcript_path": "<jsonl path>", ... }
|
|
@@ -91,8 +91,12 @@ if [ -z "$ADMIN_SESSION_ID" ] || [ -z "$TRANSCRIPT_PATH" ] || [ ! -f "$TRANSCRIP
|
|
|
91
91
|
exit 0
|
|
92
92
|
fi
|
|
93
93
|
|
|
94
|
-
#
|
|
95
|
-
|
|
94
|
+
# Task 165 — walk every user/assistant record in transcript order; emit
|
|
95
|
+
# "<type>: <text>" per record; join with "\n". The recorder needs the
|
|
96
|
+
# whole exchange to infer operator-asserted facts that span multiple
|
|
97
|
+
# turns (e.g. "hello / what's your business? / Smalleys"). The earlier
|
|
98
|
+
# "last-turn-only" extractor stripped the framing the recorder needed.
|
|
99
|
+
CONVERSATION_TEXT=$(python3 - "$TRANSCRIPT_PATH" <<'PY'
|
|
96
100
|
import sys, json
|
|
97
101
|
|
|
98
102
|
def block_text(block):
|
|
@@ -113,8 +117,7 @@ def record_text(rec):
|
|
|
113
117
|
return ""
|
|
114
118
|
|
|
115
119
|
path = sys.argv[1]
|
|
116
|
-
|
|
117
|
-
last_user = ""
|
|
120
|
+
turns = []
|
|
118
121
|
try:
|
|
119
122
|
with open(path, "r", encoding="utf-8") as f:
|
|
120
123
|
for line in f:
|
|
@@ -123,26 +126,24 @@ try:
|
|
|
123
126
|
try: rec = json.loads(line)
|
|
124
127
|
except Exception: continue
|
|
125
128
|
t = rec.get("type") if isinstance(rec, dict) else None
|
|
126
|
-
if t
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
tx = record_text(rec).strip()
|
|
131
|
-
if tx: last_user = tx
|
|
129
|
+
if t not in ("user", "assistant"): continue
|
|
130
|
+
tx = record_text(rec).strip()
|
|
131
|
+
if not tx: continue
|
|
132
|
+
turns.append(f"{t}: {tx}")
|
|
132
133
|
except Exception:
|
|
133
134
|
pass
|
|
134
135
|
|
|
135
|
-
print(
|
|
136
|
+
print("\n".join(turns), end="")
|
|
136
137
|
PY
|
|
137
138
|
)
|
|
138
139
|
|
|
139
|
-
|
|
140
|
+
CONVERSATION_BYTES=$(printf '%s' "$CONVERSATION_TEXT" | wc -c | tr -d ' ')
|
|
140
141
|
TRANSCRIPT_BYTES=$(wc -c <"$TRANSCRIPT_PATH" 2>/dev/null | tr -d ' ' || echo 0)
|
|
141
142
|
|
|
142
|
-
emit_log "trigger sessionId=${ADMIN_SESSION_ID} turnIndex=0 transcriptBytes=${TRANSCRIPT_BYTES}
|
|
143
|
+
emit_log "trigger sessionId=${ADMIN_SESSION_ID} turnIndex=0 transcriptBytes=${TRANSCRIPT_BYTES} conversationBytes=${CONVERSATION_BYTES}"
|
|
143
144
|
|
|
144
|
-
if [ -z "$
|
|
145
|
-
emit_log "trigger-skipped sessionId=${ADMIN_SESSION_ID} reason=
|
|
145
|
+
if [ -z "$CONVERSATION_TEXT" ]; then
|
|
146
|
+
emit_log "trigger-skipped sessionId=${ADMIN_SESSION_ID} reason=conversation-empty"
|
|
146
147
|
exit 0
|
|
147
148
|
fi
|
|
148
149
|
|
|
@@ -153,9 +154,9 @@ fi
|
|
|
153
154
|
SPAWN_BODY=$(python3 -c '
|
|
154
155
|
import sys, json
|
|
155
156
|
sid = sys.argv[1]
|
|
156
|
-
|
|
157
|
+
conversation = sys.argv[2]
|
|
157
158
|
instruction = "update the graph with any new or missing information or intent"
|
|
158
|
-
text = instruction + "\n\n" +
|
|
159
|
+
text = instruction + "\n\n" + conversation
|
|
159
160
|
body = {
|
|
160
161
|
"adminSessionId": sid,
|
|
161
162
|
"channel": "browser",
|
|
@@ -164,9 +165,9 @@ body = {
|
|
|
164
165
|
"initialMessage": text,
|
|
165
166
|
}
|
|
166
167
|
print(json.dumps(body))
|
|
167
|
-
' "$ADMIN_SESSION_ID" "$
|
|
168
|
+
' "$ADMIN_SESSION_ID" "$CONVERSATION_TEXT" 2>/dev/null)
|
|
168
169
|
|
|
169
|
-
INITIAL_BYTES=$(printf '%s\n\n%s' "update the graph with any new or missing information or intent" "$
|
|
170
|
+
INITIAL_BYTES=$(printf '%s\n\n%s' "update the graph with any new or missing information or intent" "$CONVERSATION_TEXT" | wc -c | tr -d ' ')
|
|
170
171
|
emit_log "spawn-request sessionId=${ADMIN_SESSION_ID} specialist=database-operator initialMessageBytes=${INITIAL_BYTES}"
|
|
171
172
|
|
|
172
173
|
SPAWN_RES_FILE=$(mktemp)
|
|
@@ -4,22 +4,31 @@ description: "CRM contacts plugin. Provides contact-create, contact-lookup, cont
|
|
|
4
4
|
tools:
|
|
5
5
|
- name: contact-create
|
|
6
6
|
publicAllowlist: false
|
|
7
|
+
adminAllowlist: false
|
|
7
8
|
- name: contact-lookup
|
|
8
9
|
publicAllowlist: false
|
|
10
|
+
adminAllowlist: false
|
|
9
11
|
- name: contact-update
|
|
10
12
|
publicAllowlist: false
|
|
13
|
+
adminAllowlist: false
|
|
11
14
|
- name: contact-delete
|
|
12
15
|
publicAllowlist: false
|
|
16
|
+
adminAllowlist: false
|
|
13
17
|
- name: contact-list
|
|
14
18
|
publicAllowlist: false
|
|
19
|
+
adminAllowlist: false
|
|
15
20
|
- name: contact-export
|
|
16
21
|
publicAllowlist: false
|
|
22
|
+
adminAllowlist: false
|
|
17
23
|
- name: contact-erase
|
|
18
24
|
publicAllowlist: false
|
|
25
|
+
adminAllowlist: false
|
|
19
26
|
- name: group-create
|
|
20
27
|
publicAllowlist: false
|
|
28
|
+
adminAllowlist: false
|
|
21
29
|
- name: group-manage
|
|
22
30
|
publicAllowlist: false
|
|
31
|
+
adminAllowlist: false
|
|
23
32
|
always: false
|
|
24
33
|
embed: false
|
|
25
34
|
metadata: {"platform":{}}
|