chore: v2.8.71 — remote clients (company hub), artifacts shared with chosen people, folders and viewable copies, fixes

Remote clients (docs/plans/remote-clients/README.md, B1–B3 for the Claude pool):
- server/hub/ holds the hub-only code (devices, remote sessions, upload ingest,
  LAN gateway), required only in server mode; hub-boundary.test.js keeps local
  code from importing it.
- A local install enrolls with `agenthub remote login` or Settings → AI tools →
  Company hub; the "Company account" AI Proxy (reserved name agenthub.company)
  sends a session through the hub's gateway, which adds a pool account's token —
  no credential reaches the machine. Only those sessions are uploaded, hourly
  and at their end; secret-shaped strings are redacted on the client.
- Gateway refuses: unknown device (401), a message without a session id, an
  unregistered session under AGENTHUB_REMOTE_GATE=block, a user over quota
  (EQUOTA), a purged session (410). Device tokens 90 days, conversations 30.
- New alert rule type remote_unmonitored: a company-account session its device
  never registered; a late registration clears the flag instead.
- The hub imports a session once per upload round (chunks carry `more`),
  not once per chunk.

Artifacts:
- Share an artifact with chosen people (artifact_shares, PATCH sharedWith,
  GET /api/artifacts/people) besides everyone; a "Shared with you" tab.
  Only the owner or an admin sees or edits who it is shared with; audited.
- Folders: each account's own arrangement (agenthub.artifacts.folders pref).
- A claude.ai page keeps a copy viewable without the publishing account: its
  HTML file (and supporting files) snapshotted at publish, or the copy Claude
  saves for a Doc / Slides / Design in artifacts-export/ (a synchronous
  PostToolUse hint hook, scripts/artifact-export-hint.js, admin switch),
  attached to the page at Stop. Viewed in HtmlFrame's sandbox, Markdown, or
  pdf.js (pdfjs-dist 6.4.299, legacy build, loaded only for a PDF).

Fixes:
- tmux: a cached /proc/<pid>/exe of an exited server falls back to PATH's
  tmux (stop the last pane, start again within 10 s failed with ENOENT).
- shelves: a shelf restores beside changes in other files.
- chat: a finished reply ends the turn even with no hook at all.
- Settings → AI tools: profile list display tweaks.
This commit is contained in:
2026-11-04 15:16:49 +07:00
parent 3daf7ed4cb
commit c946b73d11
95 changed files with 7918 additions and 340 deletions
+27 -5
View File
@@ -150,12 +150,27 @@ function makeHookEntry(hookType) {
return entry;
}
// The one hook that answers Claude: after a Claude Docs doc or a typed
// artifact (Slides, Design…) is published, it asks for a copy AgentHub can
// show to the people it is shared with. Synchronous (its answer is read), but
// only for these tools, so no other tool call waits on it.
const EXPORT_HINT = path.join(__dirname, "artifact-export-hint.js");
const EXPORT_HINT_MATCHER = "Artifact|mcp__claude_ai_.*__batch";
function makeExportHintEntry() {
return {
matcher: EXPORT_HINT_MATCHER,
hooks: [{ type: "command", command: `node "${EXPORT_HINT}"`, timeout: 5 }],
};
}
const OUR_SCRIPTS = ["hook-handler.js", "artifact-export-hint.js"];
const isOurCommand = (c) => typeof c === "string" && OUR_SCRIPTS.some((n) => c.includes(n));
function isOurEntry(entry) {
// Matches old format (entry.command) and new format (entry.hooks[].command)
if (entry.command && entry.command.includes("hook-handler.js")) return true;
if (Array.isArray(entry.hooks)) {
return entry.hooks.some((h) => h.command && h.command.includes("hook-handler.js"));
}
if (isOurCommand(entry.command)) return true;
if (Array.isArray(entry.hooks)) return entry.hooks.some((h) => isOurCommand(h.command));
return false;
}
@@ -221,7 +236,9 @@ function installHooks(silent = false) {
for (const hookType of HOOK_TYPES) {
if (!settings.hooks[hookType]) settings.hooks[hookType] = [];
const existing = settings.hooks[hookType].findIndex(isOurEntry);
const existing = settings.hooks[hookType].findIndex(
(e) => isOurEntry(e) && !JSON.stringify(e).includes("artifact-export-hint.js")
);
const entry = makeHookEntry(hookType);
if (existing >= 0) {
@@ -232,6 +249,11 @@ function installHooks(silent = false) {
installed++;
}
}
// The export hint is a second PostToolUse entry of ours, kept once.
const post = settings.hooks.PostToolUse;
const hint = post.findIndex((e) => JSON.stringify(e).includes("artifact-export-hint.js"));
if (hint >= 0) post[hint] = makeExportHintEntry();
else post.push(makeExportHintEntry());
const dir = path.dirname(getSettingsPath());
fs.mkdirSync(dir, { recursive: true });