c946b73d11
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.
67 lines
2.8 KiB
JavaScript
67 lines
2.8 KiB
JavaScript
/**
|
|
* @file Tests for scripts/artifact-export-hint.js: when Claude is asked to save
|
|
* a viewable copy of a published Doc / typed artifact, and when it is not.
|
|
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
|
*/
|
|
|
|
const { describe, it } = require("node:test");
|
|
const assert = require("node:assert/strict");
|
|
const { spawnSync } = require("node:child_process");
|
|
const path = require("node:path");
|
|
const { hintFor } = require("../../scripts/artifact-export-hint");
|
|
|
|
const PANE = { AGENTHUB_LANE_ID: "3" };
|
|
const docBirth = {
|
|
tool_name: "mcp__claude_ai_Claude_Docs__batch",
|
|
tool_input: { container: { kind: "project", create: { name: "Plan" } } },
|
|
tool_response: [{ type: "text", text: '{"frame":{"url":"https://claude.ai/artifact/doc-9"}}' }],
|
|
};
|
|
const slides = {
|
|
tool_name: "Artifact",
|
|
tool_input: { type_url: "https://claude.ai/types/slides", title: "Deck" },
|
|
tool_response: "Created https://claude.ai/artifact/deck-1",
|
|
};
|
|
|
|
describe("artifact export hint", () => {
|
|
it("asks for a Markdown copy of a new doc and a PDF/HTML copy of a typed artifact", () => {
|
|
assert.match(hintFor(docBirth, PANE), /artifacts-export\/doc-9\.md/);
|
|
const s = hintFor(slides, PANE);
|
|
assert.match(s, /artifacts-export\/deck-1\.pdf/);
|
|
assert.match(s, /artifacts-export\/deck-1\.html/);
|
|
});
|
|
|
|
it("stays silent everywhere else", () => {
|
|
assert.equal(hintFor(slides, {}), null, "not an AgentHub pane");
|
|
assert.equal(hintFor(slides, { ...PANE, AGENTHUB_ARTIFACT_EXPORT: "0" }), null, "turned off");
|
|
const html = { ...slides, tool_input: { file_path: "/w/page.html" } };
|
|
assert.equal(hintFor(html, PANE), null, "an HTML page is copied by AgentHub itself");
|
|
const edit = {
|
|
...docBirth,
|
|
tool_input: { container: { kind: "project", id: "p1" }, batch: [] },
|
|
};
|
|
assert.equal(hintFor(edit, PANE), null, "a later batch edits the doc");
|
|
assert.equal(hintFor({ ...slides, tool_response: "no link" }, PANE), null);
|
|
assert.equal(hintFor({ ...slides, tool_name: "ArtifactData" }, PANE), null);
|
|
assert.equal(hintFor(null, PANE), null);
|
|
});
|
|
|
|
it("prints Claude Code's PostToolUse context, and nothing on bad input", () => {
|
|
const script = path.join(__dirname, "..", "..", "scripts", "artifact-export-hint.js");
|
|
const run = (input, env = PANE) =>
|
|
spawnSync(process.execPath, [script], {
|
|
input,
|
|
env: { PATH: process.env.PATH, ...env },
|
|
encoding: "utf8",
|
|
timeout: 10_000,
|
|
});
|
|
const ok = run(JSON.stringify(slides));
|
|
assert.equal(ok.status, 0);
|
|
const out = JSON.parse(ok.stdout);
|
|
assert.equal(out.hookSpecificOutput.hookEventName, "PostToolUse");
|
|
assert.match(out.hookSpecificOutput.additionalContext, /deck-1/);
|
|
const bad = run("{not json");
|
|
assert.equal(bad.status, 0);
|
|
assert.equal(bad.stdout, "");
|
|
});
|
|
});
|