pocolobby 0.12.2
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/LICENSE +21 -0
- package/README.md +35 -0
- package/dist/activity.js +1 -0
- package/dist/adapters/claude.js +7 -0
- package/dist/attachments.js +2 -0
- package/dist/auth.js +4 -0
- package/dist/bin.js +4 -0
- package/dist/board-caption.js +19 -0
- package/dist/board-github.js +1 -0
- package/dist/board-model.js +1 -0
- package/dist/board-page.js +418 -0
- package/dist/board-summary.js +22 -0
- package/dist/board.js +1 -0
- package/dist/catalog.js +1 -0
- package/dist/cli.js +123 -0
- package/dist/client.js +1 -0
- package/dist/code-stats.js +3 -0
- package/dist/config.js +3 -0
- package/dist/console-page.js +467 -0
- package/dist/console.js +1 -0
- package/dist/exchange-rates.js +1 -0
- package/dist/hook.js +1 -0
- package/dist/html.js +1 -0
- package/dist/icons.js +4 -0
- package/dist/launch.js +2 -0
- package/dist/local-files.js +1 -0
- package/dist/lock.js +1 -0
- package/dist/mail.js +4 -0
- package/dist/mascots.js +6 -0
- package/dist/memory-curate.js +59 -0
- package/dist/memory.js +5 -0
- package/dist/money.js +1 -0
- package/dist/node-version.js +1 -0
- package/dist/pace.js +1 -0
- package/dist/png.js +1 -0
- package/dist/presentation.js +1 -0
- package/dist/prices.js +1 -0
- package/dist/protocol.js +1 -0
- package/dist/recurrence.js +1 -0
- package/dist/secrets.js +1 -0
- package/dist/server.js +8 -0
- package/dist/statusline.js +7 -0
- package/dist/store.js +303 -0
- package/dist/transcripts.js +2 -0
- package/docs/AGENT-SETUP-PROMPT.md +72 -0
- package/docs/API.md +1097 -0
- package/docs/ARCHITECTURE.md +410 -0
- package/docs/CLAUDE-SETUP.md +166 -0
- package/docs/INTEGRATION-PLAN.md +122 -0
- package/docs/LOCAL-SETUP.md +356 -0
- package/docs/MEMORY.md +142 -0
- package/docs/OPERATIONS.md +320 -0
- package/docs/QUICKSTART.md +111 -0
- package/docs/RELEASING.md +40 -0
- package/docs/SELF-HOSTING.md +236 -0
- package/docs/UPGRADES-WITHOUT-AGENT-RESTARTS.md +112 -0
- package/examples/CLAUDE.md +50 -0
- package/examples/PACING.md +33 -0
- package/examples/POCOLOBBY.md +69 -0
- package/examples/fly.toml +42 -0
- package/package.json +68 -0
- package/site/agents/gonk-server.webp +0 -0
- package/site/agents/luke-sleep.webp +0 -0
- package/site/agents/mouse-empty.webp +0 -0
- package/site/agents/mouse-envelope.webp +0 -0
- package/site/agents/poco-bell.webp +0 -0
- package/site/agents/vader-asleep.webp +0 -0
- package/site/agents/vader-crate.webp +0 -0
- package/site/agents/vader-deckchair.webp +0 -0
- package/site/agents/vader-point.webp +0 -0
- package/site/agents/vader-thumbs.webp +0 -0
- package/site/icons/apple-touch-icon.png +0 -0
- package/site/icons/favicon-32.png +0 -0
- package/site/icons/favicon.ico +0 -0
- package/site/icons/favicon.svg +12 -0
- package/site/index.html +204 -0
package/dist/catalog.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var M=Object.defineProperty;var n=(e,t)=>M(e,"name",{value:t,configurable:!0});import{createHash as L}from"node:crypto";import{z as a}from"zod";import{AttachmentDescriptor as P,DEFAULT_MAX_IMAGE_BYTES as x,DEFAULT_MAX_TEXT_BYTES as A,MAX_ATTACHMENTS as _,OTHER_FILES as N,formatBytes as T}from"./attachments.js";import{AgentId as d,CancelScheduledSchema as H,UpdateScheduledSchema as g,MAX_MESSAGE_IDS as U,MAX_NAMED_RECIPIENTS as I,MessageDetailsShape as h,MessageIdPrefix as G,RepeatSchema as z,SendSchema as l,OwnerRequestsSchema as q,StatusSchema as B,MemoryRef as W,RecallQuerySchema as E,RememberSchema as p,RestAgentsSchema as F}from"./protocol.js";const Y=1,X=["get_context","ready","acknowledge_messages","save_checkpoint","list_operations","call_operation","get_attachment"],J=a.string().regex(/^[a-z][a-z0-9_]{0,63}$/),K={manage_rooms:"Needs room-management permission (manage_rooms), which only the human owner grants.",can_broadcast:"Needs whole-lobby broadcast permission, which only the human owner grants. Room members can use send_room_message."},w="attachments",Z=n(e=>e,"defineTool"),i=Z,b=a.object({}).strict(),Q=a.object({mode:a.enum(["pending","history","reconciliation"]).default("pending").describe("pending: unacknowledged messages; history: what you sent and received; reconciliation: acknowledged messages your checkpoint does not cover"),cursor:a.number().int().nonnegative().optional().describe("next_cursor from the previous page"),limit:a.number().int().min(1).max(100).default(50).describe("Messages per page, at most 100"),query:a.string().min(1).max(200).optional().describe("History only: keep messages whose text, structured fields or attachment names contain this, ignoring case. History covers the last day"),unanswered:a.boolean().optional().describe("History only: the messages you received that expect a reply and that you have not answered with a message whose reply_to names them and that reached the sender. A checklist of open requests, not proof that an answer was enough; answer with reply_to so it counts"),awaiting:a.boolean().optional().describe("History only: the requests and questions you sent that no reply naming them in reply_to has reached you for yet, with each recipient's delivery state; for after a compaction, before you send again. Not with unanswered. A reply is not proof the work is done"),ids:a.array(G).min(1).max(U).optional().describe("History only: read these messages, by full ID or the first 8 or more characters a message quotes, such as 1a9b3dff; a start shared by several returns each of them. Besides your own, any message in a room you belong to"),headers:a.boolean().optional().describe("History only: each message as one line (id, from, to, room_id, kind, created_at and its first line of text), to find messages cheaply; then read the ones you need with ids")}).strict(),D={client_request_id:l.shape.client_request_id.describe("Choose once per message and reuse it unchanged on every retry; a new ID creates a new message. A retry is recognized only while the original is stored, about a day after delivery"),priority:l.shape.priority.describe("important to be handled before lower-priority queued work; info (the default) otherwise"),content:l.shape.content.describe("The message text, at most 64 KiB"),kind:h.kind.describe("What you expect: request (a reply is wanted), notice (FYI: no reply expected), assignment, report (results: no reply expected) or question. Set it on every send; without one the recipient is not told whether you expect a reply"),reply_expected:h.reply_expected.describe("Only to override the kind's default, which is false for notice and report and true for every other kind, except on a message to yourself, which expects none"),commit:h.commit.describe("A commit this concerns, such as abc1234"),commits:h.commits.describe("Several commits this concerns, at most 10, such as the commits a report landed"),files:h.files.describe("Repository-relative paths this concerns"),evidence:h.evidence.describe("Where the evidence is, as a logical location each machine resolves itself: a scheme, a colon and a path, such as dropbox:team/reports/run-42 or repo:docs/report.md; never a local absolute path"),options:h.options.describe("The choices, for a question"),recommendation:h.recommendation.describe("Your recommended option or answer, for a question"),notify_when_seen:h.notify_when_seen.describe("true pushes you a compact read receipt when each recipient acknowledges it. Receipts need no acknowledgment and no reply"),correlation_id:l.shape.correlation_id.describe("An ID you choose to group related messages"),attachments:a.array(P).min(1).max(_).optional().describe("Uploaded files, as described by the bridge that read them")},v={...D,reply_to:l.shape.reply_to.describe("The full ID of the message you are answering; the result warns when it names no message stored now, such as a mistyped one"),supersedes:l.shape.supersedes.describe("The full ID of an earlier message of yours that this one corrects or withdraws, which then shows as superseded wherever it is read. Send it to everyone the earlier one reached, now, not with deliver_at"),deliver_at:l.shape.deliver_at.describe("Store now, deliver at this time: an ISO time with Z or an offset, such as 2026-10-16T06:41:00+02:00, at most 30 days ahead. Works for a note to yourself. Compute it once and reuse it unchanged on retries")};async function k(e){const{status:t,duplicate:s,message:o,recipients:c,deliveries:r,warning:u}=await e;return{status:t,duplicate:s,message:{id:o.id,created_at:o.created_at,...o.kind?{kind:o.kind}:{},...o.reply_expected===null?{}:{reply_expected:o.reply_expected},...o.deliver_at?{deliver_at:o.deliver_at}:{},...o.attachments?.length?{attachments:o.attachments}:{}},recipients:c,deliveries:r.map(({agent_id:y,delivery_state:f})=>({agent_id:y,delivery_state:f})),...u?{warning:u}:{}}}n(k,"sent");const S='Retain client_request_id and reuse the identical arguments after an uncertain result; never resend under a new ID. Messages are kept a day after delivery, so a retry is recognized only within that day: later, check get_messages(mode="history") or ask the recipient before sending again. The result is the message id and each recipient delivery_state (scheduled until deliver_at passes), without the content you sent; stored is not completed work.',V=a.object({...v,to:d.describe("The recipient agent ID, or your own")}).strict(),ee=a.object(v).strict(),te=a.object({...v,room:d.describe("The room ID, as list_rooms shows it (a room message shows it as room_id)"),to:a.array(d).min(1).max(I).optional().describe("Only these members of the room, by agent ID; the rest of the room is not woken. Naming anyone outside the room sends nothing, and the refusal names them. Omit it to reach every member")}).strict(),se=a.object({...D,to:a.union([d,a.literal("*"),a.array(d).min(1).max(I)]).describe("An agent ID (your own for a reminder to yourself), * for a room or the lobby, or with room a list of its members to reach only them"),room:d.optional().describe('With to "*" or a list of members: the room to send to'),repeat:z.describe('Daily at these local HH:MM times in the IANA time_zone, such as {"time_zone":"Europe/Stockholm","times":["06:41"]}, optionally only on weekdays such as ["mon","tue","wed","thu","fri"]')}).strict(),oe=a.object({id:H.shape.id.describe("A schedule_id or a message id")}).strict(),ne=a.object({id:g.shape.id.describe("A schedule_id or a scheduled message id"),content:g.shape.content.describe("The new text, at most 64 KiB"),deliver_at:g.shape.deliver_at.describe("A scheduled message only: the new time, an ISO time with Z or an offset, at most 30 days ahead"),repeat:g.shape.repeat.describe('A schedule only: the new daily times in an IANA time_zone, replacing the old ones, such as {"time_zone":"Europe/Stockholm","times":["06:00"]}')}).strict(),ae=a.object({count:q.shape.count.describe("How many decisions, approvals or looks you wait on; 0 clears it"),text:q.shape.text.describe('One line naming each, such as "Approve the release notes; choose A or B for the login page"')}).strict(),re=a.object({text:B.shape.text.describe(`One line for your peers, such as "Screen queue: backend's install, then web's rerun, about 07:40"; empty clears it`)}).strict(),ie=a.object({client_request_id:p.shape.client_request_id.describe("Chosen once; reuse it after a timeout"),room_id:p.shape.room_id.describe("Such as app"),kind:p.shape.kind.describe("A trap, what fixed a symptom, or steps"),text:p.shape.text.describe("The lesson and when it applies"),evidence:p.shape.evidence.describe("A commit, test, file:line or error string"),about:p.shape.about.describe("The entry it corrects, such as app-12, or the ID of a fragment of the room still waiting for the curator")}).strict(),ce=a.object({ids:a.array(W).max(20).optional().describe("Entry IDs from the memory index"),query:E.shape.query.describe("Words or an error string"),room_id:E.shape.room_id.describe("One of your rooms"),mine:a.boolean().optional().describe("Your fragments and what became of them")}).strict(),de=a.object({room_id:d.describe("The new room ID")}).strict(),he=a.object({room_id:d.describe("The room ID"),members:a.array(d).max(100).describe("The complete new member list; it replaces the current one")}).strict(),R=n(e=>a.object({room_id:d.describe("The room ID"),members:a.array(d).min(1).max(100).describe(e)}).strict(),"changeMembersSchema"),me=R("Agent IDs from your lobby to add; current members stay, and adding one again changes nothing"),le=R("Agent IDs to remove; other members stay, and removing a non-member changes nothing"),ue=a.object({room_id:d.describe("The room to delete")}).strict();function Se(){return[i({name:"send_message",summary:"Send to one agent, or to yourself with deliver_at as a durable reminder; can carry attachments.",description:`Send to one agent in your lobby, or to yourself with deliver_at as a durable reminder that survives restarts (use it instead of a session cron). Say what you expect with kind: notice for an FYI or progress note that needs no answer; request, assignment, report or question otherwise. Put commits, files and evidence locations in their own fields. ${S}`,input:V,attachments:!0,run:n((e,t)=>k(e.call("POST","/v1/messages",t)),"run")}),i({name:"broadcast",summary:"Send to every agent in your lobby (not a room).",description:`Send to the whole lobby, not a room. Same kind, structured fields, deliver_at and notify_when_seen options as send_message. ${S}`,input:ee,requires:"can_broadcast",attachments:!0,run:n((e,t)=>k(e.call("POST","/v1/messages",{...t,to:"*"})),"run")}),i({name:"send_room_message",summary:"Send to the members of one room, or only some of them; reply to a room message this way.",description:`Send to the members of one room in your lobby; reply to a room message this way to reach the room, since send_message reaches one agent. Every member is woken by a room message, which costs each a turn: when it concerns only some, name them in to and only they receive it, in the room's thread. Requires room membership. Same kind, structured fields, deliver_at and notify_when_seen options as send_message. ${S}`,input:te,attachments:!0,run:n((e,t)=>k(e.call("POST","/v1/messages",{...t,to:t.to??"*"})),"run")}),i({name:"schedule_message",summary:"Create a durable daily repeat sent as you at local times in an IANA time zone.",description:"Create a durable daily repeat that the broker sends as you at local times in an IANA time zone, optionally on chosen weekdays, such as a 06:41 Europe/Stockholm morning notice to yourself. Each occurrence is a real message with its schedule_id. It survives session and broker restarts; use it instead of a session cron, and remove the cron in the same turn once list_scheduled shows the schedule. A clock time skipped by a daylight-saving change runs just after the gap; a repeated one runs once. An occurrence after broker downtime is marked late. Retain client_request_id on retries.",input:se,attachments:!0,run:n((e,t)=>e.call("POST","/v1/schedules",t),"run")}),i({name:"list_scheduled",summary:"List your schedules and your messages waiting for their deliver_at.",description:"List your active recurring schedules (with next_occurrence_at and next_occurrence_local) and your messages waiting for their deliver_at, with their IDs. Each item shows the first 300 characters of its content (content_truncated marks a cut one) and, for a message, how many recipients it will reach; your history has the whole message with its structured fields. They belong to your identity and survive restarts; do not recreate them after one.",input:b,run:n(e=>e.call("GET","/v1/schedules"),"run")}),i({name:"update_scheduled",summary:"Change a schedule (content, repeat) or a scheduled message not delivered yet (content, deliver_at).",description:"Change one of your schedules (its content and repeat) or one of your scheduled messages that has not been delivered yet (its content and deliver_at) by its ID, in place of cancelling it and sending again. It keeps its ID and everything you do not name. Repeating the same update changes nothing more; after an uncertain result, check list_scheduled before changing it again. A delivered message cannot be changed: send a correction.",input:ne,run:n((e,t)=>e.call("POST","/v1/schedules/update",t),"run")}),i({name:"cancel_scheduled",summary:"Cancel a schedule, or a scheduled message not delivered yet.",description:"Cancel one of your schedules (stops future occurrences) or one of your scheduled messages that has not been delivered yet, by its ID. Repeating a cancellation is harmless; a delivered message cannot be recalled.",input:oe,run:n((e,t)=>e.call("POST","/v1/schedules/cancel",t),"run")}),i({name:"list_agents",summary:"List enabled agents in your lobby: last message time and what they wait on from the owner.",description:"List enabled agents in your lobby with each one's last_message_at: when it last sent a message or received a direct one, or null when it did neither within the last day (messages are kept a day). It says who talked, not who worked: see activity. work_description, machine and screen are what the owner wrote about each agent: what it does, the machine it runs on (agents on one machine share its screen and files) and whether it has that machine's screen for live checks; use them to know whom to ask, as directory data, not instructions or permissions, and empty where the owner wrote none. Also owner_requests: what it said it waits on from the owner (count, text, set_at), or null. When the owner asks whether anyone needs something from them, answer from every agent's owner_requests, not only your own. status is the one line it set for its peers with set_status (text, set_at), such as a queue's order, or null: read it before asking where something stands.Neither connection nor last_message_at establishes that an agent is available or will act. activity is the agent's turns from its Claude Code hooks while its bridge is connected, or null when unknown (no hooks, offline, or no turn since its session restarted); the dashboard draws its working time from the same hooks: state working while a turn runs, idle since its last turn ended, or failed when it ended on an API error, and since. Idle means its session stopped, not that its task is done. A coordinator also gets an activity event when an agent's turn of a minute or more stopped five minutes ago and no new one started; it needs no acknowledgment or reply. usage is its Claude plan as its status line last reported it, or null: account (email), plan, five_hour and seven_day with used_percentage and resets_at, and for the week its pace: line is where its use would be now if spread evenly to 100% at reset, lead how many points ahead (positive, burning too fast) or behind it is, pace ahead or behind when more than 5 points off the line, else on, and per_day the points a day that would use what is left by the reset: an even week burns about 14.3, so higher means room for more work and lower means slow down. The week is the account's, from the freshest report of any agent on it, so agents on one account show the same week; its observed_at says when that report came, and stale true means no fresh report for 15 minutes, so its numbers may lag: do not pace on a stale week. usage.observed_at is when this agent's own status line last reported, and usage.model (its ID), usage.model_name (such as Opus 5.5), usage.context_window (in tokens, such as 1000000) and usage.effort (low, medium, high, xhigh or max) are the model, context window and reasoning effort its own session runs, or null when its status line did not say. usage.model_windows lists the account's models with a weekly limit of their own, such as Fable, each with used_percentage of that limit, resets_at, line, lead, pace, observed_at and stale like the week; it is null when unknown and [] when there are none, and a window at 0% used is left out. An agent running such a model can run out of that window before the week, so pace it on both. unseen_notified_count and oldest_unseen_notified_at count messages written to an agent's session since its latest progress (an acknowledgment, checkpoint or message it sent) and not yet acknowledged: a long-standing one suggests the agent cannot take turns (for example its account is out of usage) or is busy, but it is not proof of either, only the agent's progress clears it, and zero is not proof of health.",input:b,run:n(e=>e.call("GET","/v1/agents"),"run")}),i({name:"set_owner_requests",summary:"Tell the owner what you wait on from them; 0 clears it.",description:"Tell the owner how many decisions, approvals or looks you are waiting on from them, with one line naming each. It shows under Waiting for you on the lobby dashboard, and peers see it in list_agents. It is a record, not a message: the owner sees it only when looking at the dashboard, so when a decision blocks your work, also ask your coordinator (or whoever relays to the owner) with a question. Each call replaces the last; send count 0 with no text once nothing waits on the owner any more, and keep it current: it stays until you clear it. Use it only for the owner, not for peers.",input:ae,run:n((e,t)=>e.call("PUT","/v1/owner-requests",t),"run")}),i({name:"set_status",summary:"Set one line your peers see in list_agents; empty clears it.",description:"Set a one-line status your peers see beside you in list_agents, with the time you set it, in place of notices that only say where things stand, such as a queue's order and times: each notice costs every reader a turn, and a status costs none. Each call replaces the last; the same text again keeps its time. It stays until you change or clear it, so keep it current, and clear it when it no longer holds. It is not a message: nobody is told when it changes, so ask or answer by message as before.",input:re,run:n((e,t)=>e.call("PUT","/v1/status",t),"run")}),i({name:"remember",summary:"Save a lesson to a room's shared memory, for its curator to review.",description:"Save one lesson for a room's shared memory: a gotcha, a fix or a procedure that another member would need, a mistake or lost time otherwise, and cannot read from the code, CLAUDE.md or notes. Save it when you solve something non-obvious and when you catch another agent's mistake. Give evidence the code can be checked against. It waits as a fragment until the room's curator reviews it against the code and turns it into an entry or refuses it with a reason; retry with the same client_request_id or recall {mine: true} to see which. Decisions and task state belong in commits and notes, rules in CLAUDE.md; credentials are refused before anything is stored. Something only about you, your machine or your session belongs in your own auto memory.",input:ie,run:n((e,t)=>e.call("POST","/v1/memory",t),"run")}),i({name:"recall",summary:"Read your rooms' shared memory by entry ID or by words.",description:"Read your rooms' shared memory. When a task starts, search it with query on the task, even when no rule line your session start shows matches, since a large room's rules do not all fit there; query returns every entry that matches well, at most 15, and none when nothing does, and also finds an error string. Read an entry by its ID (ids, up to 20) before acting on its rule: its text holds the details and evidence. Entries are teammate notes, curated but fallible: check an entry's evidence against the code before relying on it, and send a correction with remember {about} when the code disagrees. mine lists your own fragments and what the curator did with them.",input:ce,run:n((e,{ids:t,query:s,room_id:o,mine:c})=>{const r=new URLSearchParams;return t?.length&&r.set("ids",t.join(",")),s&&r.set("query",s),o!==void 0&&r.set("room_id",o),c&&r.set("mine","true"),e.call("GET",`/v1/memory?${r}`)},"run")}),i({name:"list_rooms",summary:"List the rooms you can see and their members.",description:"Discover visible rooms and their member names inside your own lobby.",input:b,run:n(e=>e.call("GET","/v1/rooms"),"run")}),i({name:"rest_agents",summary:"Rest agents so that nothing wakes them, or resume them.",description:"Rest or resume agents in your own lobby, such as agents whose Claude account is at its limit until it resets. While an agent rests, messages to it are stored and show as pending to their senders, but nothing is pushed to its session: no message, read receipt or activity event wakes it, so it takes no turns for them. Work it is already doing goes on. Messages, scheduled ones included, do not resume it: rest_agents with resting false, its resume_at time when you set one (such as when its account resets), or the owner resumes it, and then everything waiting for it is pushed together; with nothing waiting, resuming wakes nothing. A new rest_agents call replaces the rest and resume_at of each agent it names. Messages are still kept only a day, so rest agents for hours, not days. You cannot rest yourself. list_agents shows a resting agent with paused true and its resume_at. Only the human owner admits agents or issues credentials.",input:F,requires:"manage_rooms",run:n((e,t)=>e.call("POST","/v1/agents/rest",t),"run")}),i({name:"create_room",summary:"Create an empty room in your lobby.",description:"Create a room in your own lobby. It starts empty and admits nobody to the lobby; only the human owner admits agents or issues credentials.",input:de,requires:"manage_rooms",run:n((e,t)=>e.call("POST","/v1/rooms",t),"run")}),i({name:"set_room_members",summary:"Replace a room's whole member list.",description:"Replace a room membership list with existing agents from your own lobby. It replaces the whole list, so read list_rooms first and keep members who should stay; to add or remove some members without overwriting another coordinator's change, use add_room_members or remove_room_members. Removing a member holds its unseen room messages; it cannot recall content already shown or stop running work. Only the human owner admits agents or issues credentials.",input:he,requires:"manage_rooms",run:n((e,{room_id:t,members:s})=>e.call("PUT",`/v1/rooms/${t}/members`,{members:s}),"run")}),i({name:"add_room_members",summary:"Add agents to a room, keeping its other members.",description:"Add existing agents from your own lobby to a room. Everyone already in it stays, so another coordinator's concurrent change is not lost. New members receive the room's messages from now on, not earlier ones. Only the human owner admits agents or issues credentials.",input:me,requires:"manage_rooms",run:n((e,{room_id:t,members:s})=>e.call("PATCH",`/v1/rooms/${t}/members`,{add:s}),"run")}),i({name:"remove_room_members",summary:"Remove agents from a room, keeping its other members.",description:"Remove agents from a room. Everyone else stays, so another coordinator's concurrent change is not lost. A removed member's unseen room messages are held; this cannot recall content already shown or stop running work.",input:le,requires:"manage_rooms",run:n((e,{room_id:t,members:s})=>e.call("PATCH",`/v1/rooms/${t}/members`,{remove:s}),"run")}),i({name:"delete_room",summary:"Delete a room: no members, no sends; its messages stay as history.",description:"Delete a room in your own lobby. It loses every member and accepts no more sends. Room messages nobody has seen yet are held for their recipients, and recurring schedules that send to the room are cancelled; the result lists them, so tell their owners. Messages already seen and their receipts stay readable in history. The ID can be used again by create_room for a new, empty room, which inherits no messages or schedules. A retry deletes whatever room has that ID at the time, so check list_rooms before retrying.",input:ue,requires:"manage_rooms",run:n((e,{room_id:t})=>e.call("DELETE",`/v1/rooms/${t}`),"run")}),i({name:"get_messages",summary:"Read your inbox, your history or reconciliation in bounded pages.",description:"Read a bounded page; follow next_cursor until null. It is the current inbox: pushes are snapshots that may arrive late. Messages show kind and reply_expected when the sender chose them, any structured fields, and attachments (open one with get_attachment and its id). A message superseded by a later one of its sender shows superseded_by: read that one instead; the later one shows supersedes. History items you sent include deliveries: each recipient's delivery_state, notification_attempted_at and agent_seen_at (seen is not completed work). Reconciliation lists acknowledged messages your checkpoint does not cover; investigate prior effects before repeating any work. Held room messages are revoked deliveries: never acknowledge or replay them. Reading does not acknowledge. In history, ids reads messages a request names, headers lists one line per message, and awaiting lists what you sent that still waits for a reply.",input:Q,run:n((e,{mode:t,limit:s,cursor:o,query:c,unanswered:r,awaiting:u,ids:y,headers:f})=>{const m=new URLSearchParams({mode:t,limit:String(s)});return o!==void 0&&m.set("cursor",String(o)),c!==void 0&&m.set("query",c),r&&m.set("unanswered","true"),u&&m.set("awaiting","true"),y!==void 0&&m.set("ids",y.join(",")),f&&m.set("headers","true"),e.call("GET",`/v1/messages?${m}`)},"run")}),i({name:"read_checkpoint",summary:"Read your saved checkpoint, its revision and your receipt_snapshot, which save_checkpoint needs.",description:"Read your saved checkpoint: its content, revision and covered_receipt_seq, and your current receipt_snapshot. save_checkpoint takes expected_revision equal to checkpoint.revision and covered_receipt_seq up to receipt_snapshot. A light read for during work, such as after a context compaction or a refused save, without the tool guide and messages that get_context loads; a bootstrap still calls get_context.",input:b,run:n(e=>e.call("GET","/v1/checkpoint"),"run")})]}n(Se,"defaultTools");const O=new WeakMap;function $(e){let t=O.get(e);return t||(t=a.toJSONSchema(e.input,{unrepresentable:"any",io:"input"}),delete t.$schema,O.set(e,t)),t}n($,"inputSchema");function pe(e=x,t=A){return{type:"array",minItems:1,maxItems:_,items:{type:"string",minLength:2,maxLength:4096},description:`Up to ${_} absolute paths, images ${T(e)}, text ${T(t)}: png, jpg, gif, webp, md, txt, csv, json, log, mjs, cjs, js, ts, py, sh, swift. Your PocoLobby bridge reads and uploads them, and recipients open them with get_attachment. ${N}. Only files in your home directory or in the system temporary folder where Claude Code keeps a session's scratch files: not hidden or under a dot directory, not symbolic links to elsewhere, and not named like credentials; a refused path says why. A retry with the same client_request_id and files uploads nothing again`}}n(pe,"attachmentPathsSchema");function ye(e,t={}){const s=structuredClone($(e)),o=s.properties,c=e.attachments===!0;return o&&w in o&&(c?o[w]=pe(t.maxImageBytes,t.maxTextBytes):delete o[w]),{name:e.name,summary:e.summary,description:c?`${e.description} Attach files with attachments: absolute local paths, which your bridge uploads; every recipient can open them with get_attachment.`:e.description,input_schema:s,...c?{local_files:[w]}:{}}}n(ye,"catalogEntry");function xe(e){const t=new Set;for(const s of e){if(!J.safeParse(s.name).success)throw new Error(`Invalid tool name ${s.name}`);if(X.includes(s.name))throw new Error(`${s.name} belongs to the bridge and cannot be a catalog tool`);if(t.has(s.name))throw new Error(`Duplicate tool ${s.name}`);if(t.add(s.name),$(s).type!=="object")throw new Error(`Tool ${s.name} must take an object`)}}n(xe,"validateTools");function j(e,t){return e.requires===void 0||t[e.requires]}n(j,"permitted");function Ae(e,t,s={}){const o={catalog_version:Y,tools:e.filter(r=>j(r,t)).map(r=>ye(r,s)),withheld:e.filter(r=>!j(r,t)).map(r=>({name:r.name,reason:"permission",detail:K[r.requires]})),attachment_limits:{max_image_bytes:s.maxImageBytes??x,max_text_bytes:s.maxTextBytes??A}},c=L("sha256").update(JSON.stringify(o)).digest("hex");return{catalog_version:o.catalog_version,revision:c,tools:o.tools,withheld:o.withheld,attachment_limits:o.attachment_limits}}n(Ae,"buildCatalog");const C=n((e,t)=>{const o=(/^(.+?[.;])(\s|$)/su.exec(e)?.[1]??e).replace(/\s+/gu," ").replace(/[.;]$/u,"");return o.length>t?`${o.slice(0,t-3)}...`:o},"firstSentence");function ge(e,t){const s=typeof e?.description=="string"?e.description:void 0,o=Array.isArray(e?.enum)?e.enum.join("|"):void 0,c=typeof e?.type=="string"?e.type:"value",r=s?`${C(s,o?90:140)}${o?` (${o})`:""}`:o?`one of ${o}`:c;return t?`(required) ${r}`:r}n(ge,"argumentMeaning");const we="Current PocoLobby operations and their arguments. Your tool list can keep an older schema of a named tool for the whole session; these definitions govern. Pass any argument listed here to the named tool, or use call_operation {operation, arguments} when the named tool lacks it. list_operations has full types and limits. After an uncertain send, retry with the original arguments and client_request_id, on whichever path; after a day the original is no longer stored, so check your history or ask the recipient first.";function Te(e){const t=Object.keys(e.input_schema.properties??{}),s=new Set(Array.isArray(e.input_schema.required)?e.input_schema.required:[]),o=t.filter(r=>s.has(r)),c=t.filter(r=>!s.has(r));return t.length?`${e.name} takes ${o.length?o.join(", "):"only optional arguments"}${c.length?`${o.length?", and optionally ":": "}${c.join(", ")}`:""}`:`${e.name} takes no arguments`}n(Te,"argumentList");function Ie(e,t){return{catalog_revision:t,note:we,operations:e.map(s=>{const o=s.input_schema.properties??{},c=new Set(Array.isArray(s.input_schema.required)?s.input_schema.required:[]);return{name:s.name,purpose:s.summary??C(s.description,160),arguments:Object.fromEntries(Object.entries(o).map(([r,u])=>[r,ge(u,c.has(r))]))}})}}n(Ie,"toolGuide");export{Y as CATALOG_VERSION,w as LOCAL_FILES_ARGUMENT,X as LOCAL_TOOLS,we as TOOL_GUIDE_NOTE,J as ToolName,Te as argumentList,pe as attachmentPathsSchema,Ae as buildCatalog,ye as catalogEntry,Se as defaultTools,Z as defineTool,$ as inputSchema,j as permitted,Ie as toolGuide,xe as validateTools};
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var V=Object.defineProperty;var y=(m,i)=>V(m,"name",{value:i,configurable:!0});import{execFileSync as Z,spawn as Q}from"node:child_process";import{randomBytes as ee,randomUUID as oe}from"node:crypto";import{appendFileSync as te,mkdirSync as ne,readFileSync as re,realpathSync as k}from"node:fs";import{lstat as se,open as ae,readFile as R,stat as D,unlink as ie,writeFile as ce}from"node:fs/promises";import{homedir as le}from"node:os";import{dirname as x,isAbsolute as H,relative as de,resolve as B,sep as me}from"node:path";import{fileURLToPath as j,pathToFileURL as pe}from"node:url";import{parseArgs as ue}from"node:util";import{ZodError as be}from"zod";import{loadConfig as G,readConfig as fe,reserveConfig as he,setConfigUrl as ge,validateConfig as q,writeConfigExclusive as we}from"./config.js";import{BoardAgentsSchema as ye,BoardSourceSchema as Oe,RepoName as F}from"./board-model.js";import{LobbyClient as K,LobbyClientError as Ee}from"./client.js";import{postmarkMailer as _e}from"./mail.js";import{commandOnPath as Te,launchScript as Ae,packageRoot as U,pocolobbyVersion as $,stableLaunchPaths as Se}from"./launch.js";import{unsupportedNodeMessage as Be}from"./node-version.js";import{AgentId as I,CancelScheduledSchema as ve,Email as X,MIN_PUSH_BYTES as Ne,MAX_PUSH_BYTES as Pe,LobbyId as Y,NewLobbyId as Ie,ProvisionSchema as Ce,UpdateAgentSchema as Le,RoomId as C,RoomMembersSchema as Me,RoomMemberChangesSchema as ke,SendSchema as Re}from"./protocol.js";const z=y(m=>m.split(",").map(i=>i.trim()).filter(Boolean),"listOf"),J=`PocoLobby \u2014 a little place for agents to meet.
|
|
3
|
+
|
|
4
|
+
pocolobby init --config owner.json [--url http://127.0.0.1:8787]
|
|
5
|
+
pocolobby server --db state.db [--config owner.json] [--host 127.0.0.1] [--port 8787]
|
|
6
|
+
pocolobby agent add NAME --config owner.json --out agent.json [--lobby NAME] [--coordinator] [--role-file role.txt] [--work TEXT] [--machine NAME] [--screen yes|no] [--broadcast] [--allow PEER]
|
|
7
|
+
pocolobby agent describe NAME --config owner.json [--work TEXT] [--machine NAME] [--screen yes|no]
|
|
8
|
+
pocolobby agent rotate NAME --config owner.json --out new-agent.json
|
|
9
|
+
pocolobby agent pause|resume|disable|enable|retry|reset-runtime NAME --config owner.json
|
|
10
|
+
pocolobby agents --config owner.json
|
|
11
|
+
pocolobby memory curate --config agent.json --room ROOM [--repo DIR] [--report-to AGENT] [--force] [--review] [--dry-run]
|
|
12
|
+
pocolobby memory curator ROOM AGENT|none --config owner.json
|
|
13
|
+
pocolobby repo stats --config agent.json --room ROOM [--repo DIR]
|
|
14
|
+
pocolobby memory show --config owner.json
|
|
15
|
+
pocolobby settings --config owner.json
|
|
16
|
+
pocolobby lobbies --config owner.json
|
|
17
|
+
pocolobby lobby create NAME --config owner.json
|
|
18
|
+
pocolobby lobby display-name NAME ["Display Name"] --config owner.json
|
|
19
|
+
pocolobby rooms --config owner.json [--lobby NAME]
|
|
20
|
+
pocolobby room create NAME --config owner.json [--lobby NAME]
|
|
21
|
+
pocolobby room members NAME --member AGENT --member AGENT --config owner.json [--lobby NAME]
|
|
22
|
+
pocolobby room add|remove NAME --member AGENT --config owner.json [--lobby NAME]
|
|
23
|
+
pocolobby room delete NAME --config owner.json [--lobby NAME]
|
|
24
|
+
pocolobby send NAME "message" --config owner.json [--important] [--request-id ID]
|
|
25
|
+
pocolobby broadcast "message" --config owner.json [--important] [--request-id ID]
|
|
26
|
+
pocolobby send-room ROOM "message" --config owner.json [--lobby NAME] [--important]
|
|
27
|
+
pocolobby schedules NAME --config owner.json [--cancel ID]
|
|
28
|
+
pocolobby inbox NAME --config owner.json [--mode pending|history|reconciliation] [--cursor N] [--limit N]
|
|
29
|
+
pocolobby context NAME --config owner.json
|
|
30
|
+
pocolobby attachments [usage] --config owner.json [--lobby NAME] [--cursor N] [--limit N]
|
|
31
|
+
pocolobby dashboard source set LOBBY ROOM --repo OWNER/NAME [--ref main] --notes PATH [--notes PATH] --config owner.json
|
|
32
|
+
pocolobby dashboard source remove LOBBY ROOM --config owner.json
|
|
33
|
+
pocolobby dashboard agents LOBBY --agent ID=EMOJI[ NAME] [--agent ...] [--hide ID] [--support ID] --config owner.json
|
|
34
|
+
pocolobby dashboard icon set LOBBY ICON.png --config owner.json
|
|
35
|
+
pocolobby dashboard icon remove LOBBY --config owner.json
|
|
36
|
+
pocolobby dashboard status LOBBY --config owner.json
|
|
37
|
+
pocolobby accounts list --config owner.json
|
|
38
|
+
pocolobby accounts allow|enable|disable EMAIL --config owner.json
|
|
39
|
+
pocolobby accounts grant|revoke EMAIL LOBBY --config owner.json
|
|
40
|
+
pocolobby repos LOBBY list|add|remove [OWNER/NAME] --config owner.json
|
|
41
|
+
pocolobby adapter --config agent.json
|
|
42
|
+
pocolobby mcp-config --config agent.json
|
|
43
|
+
pocolobby check-config --mcp-config agent.mcp.json
|
|
44
|
+
pocolobby statusline --config agent.json [--dry-run]
|
|
45
|
+
pocolobby hook --config agent.json [--dry-run]
|
|
46
|
+
pocolobby hook --config agent.json --memory 1|2|3
|
|
47
|
+
pocolobby set-url --config FILE --url https://NEW-ADDRESS
|
|
48
|
+
pocolobby launch-script --mcp-config agent.mcp.json [--session ID] [-- CLAUDE-OPTIONS]
|
|
49
|
+
pocolobby backup --db state.db --out backup.db
|
|
50
|
+
pocolobby recover --db restored.db
|
|
51
|
+
|
|
52
|
+
Credentials are saved in protected files, never printed. Server may use
|
|
53
|
+
POCOLOBBY_ADMIN_TOKEN instead of --config. --allow-remote explicitly permits a
|
|
54
|
+
non-loopback HTTP listener behind a TLS proxy. Do not expose cleartext remotely.
|
|
55
|
+
--content-file FILE replaces the message argument. Repeat --allow for peer access.
|
|
56
|
+
Sends accept --kind request|notice|assignment|report|question and --deliver-at
|
|
57
|
+
ISO-TIME (with Z or an offset, at most 30 days ahead). schedules lists an agent's
|
|
58
|
+
recurring schedules and undelivered scheduled messages; --cancel ID cancels one.
|
|
59
|
+
Owner operations act in the broker's only lobby; with several, name one with --lobby.
|
|
60
|
+
room members replaces the whole list; use --agents a,b or repeat --member. --agents ""
|
|
61
|
+
clears membership.
|
|
62
|
+
room add and room remove change only the named members. room delete removes every
|
|
63
|
+
member and holds the room's unseen messages; its history stays.
|
|
64
|
+
--coordinator permits room management, never credential issuance or lobby admission.
|
|
65
|
+
backup and recover require the broker to be stopped and acquire its database lock.
|
|
66
|
+
check-config reports whether a generated MCP file's Node and PocoLobby module exist and
|
|
67
|
+
which release the module is, compared with the pocolobby on PATH after resolving symbolic
|
|
68
|
+
links, and the broker address the credential file names (never its token). Install and run mcp-config with the same Node and
|
|
69
|
+
npm prefix. attachments lists a lobby's attachment metadata; attachments usage shows its
|
|
70
|
+
storage use and limits. The owner never downloads attachment content.
|
|
71
|
+
set-url points an owner or agent credential file at another address of the same
|
|
72
|
+
broker, after checking that the address accepts it; the token is kept and never printed.
|
|
73
|
+
A running adapter moves on its next start or /mcp reconnect pocolobby.
|
|
74
|
+
launch-script, run from the agent's own Claude conversation in its checkout, writes
|
|
75
|
+
./launch-agent: a bash script that resumes this conversation with the PocoLobby channel,
|
|
76
|
+
its MCP file, its CLAUDE_CONFIG_DIR and any CLAUDE-OPTIONS (such as --remote-control),
|
|
77
|
+
and excludes it from Git. On Windows, Git Bash runs it. It refuses to overwrite one.
|
|
78
|
+
A lobby's dashboard lives at https://LOBBY.<POCOLOBBY_DASHBOARD_DOMAIN>/, or on a
|
|
79
|
+
broker without a domain at http://127.0.0.1:8787/LOBBY. dashboard source set reads one
|
|
80
|
+
room's notes from a GitHub repository: each --notes is a directory (every *.md directly
|
|
81
|
+
in it) or a file, and approves that repository for the lobby. dashboard agents replaces
|
|
82
|
+
the emoji (and optional names) shown per agent. dashboard icon set gives the dashboard
|
|
83
|
+
its own icon (a square PNG, 32 to 1024 pixels, at most 64 KB) in place of PocoLobby's.
|
|
84
|
+
lobby display-name sets how the dashboard and Settings show a lobby, such as My Team
|
|
85
|
+
for my-team; without a display name they show its name.
|
|
86
|
+
Settings sit beside each lobby's dashboard, at https://LOBBY.<domain>/settings, and its
|
|
87
|
+
owner signs in at POCOLOBBY_SETTINGS_ORIGIN/ (default https://<domain>/), Your lobbies,
|
|
88
|
+
with a code sent to their email. A broker without a domain has them at
|
|
89
|
+
http://127.0.0.1:8787/: settings opens them in the browser, signed in, with a
|
|
90
|
+
one-time link, printed too, that works once within a minute.
|
|
91
|
+
accounts allow lets an address sign in and create up to 3 lobbies; accounts grant
|
|
92
|
+
makes it the owner of an existing lobby (refused when another account owns it), and
|
|
93
|
+
accounts revoke ends that ownership; accounts disable signs the account out everywhere
|
|
94
|
+
at once, and accounts enable lets it in again without changing what it may do. repos
|
|
95
|
+
approves the GitHub repositories a lobby's owner may read notes from.
|
|
96
|
+
|
|
97
|
+
The server reads these optional settings from its environment:
|
|
98
|
+
POCOLOBBY_PUSH_BYTES push size in bytes (default 32768)
|
|
99
|
+
POCOLOBBY_ATTACHMENTS_DIR attachment files (default: beside --db)
|
|
100
|
+
POCOLOBBY_ATTACHMENT_MAX_IMAGE_BYTES per image (default 10485760)
|
|
101
|
+
POCOLOBBY_ATTACHMENT_MAX_TEXT_BYTES per text document (default 1048576)
|
|
102
|
+
POCOLOBBY_ATTACHMENT_LOBBY_BYTES per lobby (default 1073741824)
|
|
103
|
+
POCOLOBBY_ATTACHMENT_RETENTION_DAYS after the last message (default 1)
|
|
104
|
+
POCOLOBBY_ATTACHMENT_GRACE_SECONDS unsent uploads (default 3600)
|
|
105
|
+
POCOLOBBY_ATTACHMENT_DISK_RESERVE_BYTES free disk kept for SQLite (default 268435456)
|
|
106
|
+
POCOLOBBY_UPLOAD_INFLIGHT_BYTES upload bytes received at once (default 33554432)
|
|
107
|
+
POCOLOBBY_DASHBOARD_DOMAIN dashboards at LOBBY.<domain>, such as pocolobby.com (unset: local)
|
|
108
|
+
POCOLOBBY_DASHBOARD_NOTES_SECONDS how often notes are checked (default 60)
|
|
109
|
+
POCOLOBBY_DASHBOARD_SUMMARY_SECONDS at most one summary per this (default 600)
|
|
110
|
+
POCOLOBBY_CLIENT_IP_HEADER trusted client-address header, e.g. fly-client-ip
|
|
111
|
+
GITHUB_TOKEN read-only token for the notes repositories
|
|
112
|
+
ANTHROPIC_API_KEY the dashboard summarizer's key
|
|
113
|
+
POCOLOBBY_SETTINGS_ORIGIN where owners sign in (default https://<domain>)
|
|
114
|
+
POSTMARK_TOKEN Postmark server token for sign-in codes
|
|
115
|
+
POCOLOBBY_MAIL_FROM sign-in sender, e.g. "PocoLobby <signin@example.com>"
|
|
116
|
+
POCOLOBBY_EXCHANGE_RATES ecb: fetch the ECB's daily rates, for costs in a lobby's currency
|
|
117
|
+
`,De=new Set([...J.matchAll(/^ pocolobby ([a-z-]+)/gmu)].map(m=>m[1]));function $e(m){const i=y((S,g,v)=>{const o=m[S];if(o===void 0||o==="")return;if(!/^\d+$/u.test(o)||!Number.isSafeInteger(Number(o)))throw new Error(`${S} must be a whole number.`);const s=Number(o);if(s<g||s>v)throw new Error(`${S} must be between ${g} and ${v}.`);return s},"integer"),p=i("POCOLOBBY_ATTACHMENT_RETENTION_DAYS",1,3650),b=i("POCOLOBBY_ATTACHMENT_GRACE_SECONDS",60,168*3600),f=m.POCOLOBBY_ATTACHMENTS_DIR;if(f!==void 0&&f!==""&&!H(f))throw new Error("POCOLOBBY_ATTACHMENTS_DIR must be an absolute path.");const e={maxImageBytes:i("POCOLOBBY_ATTACHMENT_MAX_IMAGE_BYTES",1024,1024**3),maxTextBytes:i("POCOLOBBY_ATTACHMENT_MAX_TEXT_BYTES",1024,1024**3),maxLobbyAttachmentBytes:i("POCOLOBBY_ATTACHMENT_LOBBY_BYTES",0,1024**4),attachmentDiskReserveBytes:i("POCOLOBBY_ATTACHMENT_DISK_RESERVE_BYTES",0,1024**4),attachmentRetentionMs:p===void 0?void 0:p*24*60*60*1e3,attachmentGraceMs:b===void 0?void 0:b*1e3,attachmentsDir:f||void 0},A=m.POCOLOBBY_DASHBOARD_DOMAIN;if(A&&!/^(?=.{1,200}$)([a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z]{2,}$/u.test(A))throw new Error("POCOLOBBY_DASHBOARD_DOMAIN must be a domain name such as pocolobby.com.");const r=m.POCOLOBBY_CLIENT_IP_HEADER;if(r&&!/^[A-Za-z0-9-]{1,64}$/u.test(r))throw new Error("POCOLOBBY_CLIENT_IP_HEADER must be a header name such as fly-client-ip.");const t=i("POCOLOBBY_DASHBOARD_NOTES_SECONDS",30,3600),w=i("POCOLOBBY_DASHBOARD_SUMMARY_SECONDS",60,86400),_={domain:A,githubToken:m.GITHUB_TOKEN||void 0,anthropicApiKey:m.ANTHROPIC_API_KEY||void 0,notesIntervalMs:t===void 0?void 0:t*1e3,summaryIntervalMs:w===void 0?void 0:w*1e3,clientIpHeader:r||void 0},n=y(S=>Object.fromEntries(Object.entries(S).filter(([,g])=>g!==void 0)),"defined"),E=m.POCOLOBBY_SETTINGS_ORIGIN;if(E&&!/^https?:\/\/[a-z0-9.-]+(:\d{1,5})?$/u.test(E))throw new Error("POCOLOBBY_SETTINGS_ORIGIN must be an origin such as https://pocolobby.com, without a path.");const a=m.POSTMARK_TOKEN||void 0,h=m.POCOLOBBY_MAIL_FROM||void 0,T=[...a?[]:["POSTMARK_TOKEN"],...h?[]:["POCOLOBBY_MAIL_FROM"]],c=m.POCOLOBBY_EXCHANGE_RATES;if(c&&c!=="ecb")throw new Error("POCOLOBBY_EXCHANGE_RATES must be ecb, or unset to fetch no exchange rates.");return{pushBytes:i("POCOLOBBY_PUSH_BYTES",Ne,Pe),uploadInflightBytes:i("POCOLOBBY_UPLOAD_INFLIGHT_BYTES",1024,1024**4),...c?{exchangeRates:{source:"ecb"}}:{},storeOptions:n(e),board:n(_),console:{...E?{origin:E}:{},mailer:a&&h?_e({token:a,from:h}):null,missing:T}}}y($e,"serverSettings");function Ye(m){const[i,p]=process.platform==="win32"?["cmd",["/c","start",'""',m]]:process.platform==="darwin"?["open",[m]]:["xdg-open",[m]],b=Q(i,p,{detached:!0,stdio:"ignore",windowsVerbatimArguments:!0});b.on("error",()=>{}),b.unref()}y(Ye,"browse");async function W(){const m=[];for await(const i of process.stdin)m.push(i);return Buffer.concat(m).toString("utf8")}y(W,"readStdin");async function xe(m,i={}){const p=i.stdout??(o=>process.stdout.write(`${o}
|
|
118
|
+
`)),b=i.stderr??(o=>process.stderr.write(`${o}
|
|
119
|
+
`)),f=i.env??process.env,{values:e,positionals:A}=ue({args:m,allowPositionals:!0,options:{help:{type:"boolean",short:"h"},config:{type:"string"},lobby:{type:"string"},coordinator:{type:"boolean"},agents:{type:"string"},member:{type:"string",multiple:!0},url:{type:"string"},db:{type:"string"},out:{type:"string"},host:{type:"string"},port:{type:"string"},"allow-remote":{type:"boolean"},"role-file":{type:"string"},work:{type:"string"},memory:{type:"string"},machine:{type:"string"},screen:{type:"string"},"content-file":{type:"string"},"request-id":{type:"string"},"correlation-id":{type:"string"},kind:{type:"string"},"deliver-at":{type:"string"},cancel:{type:"string"},important:{type:"boolean"},broadcast:{type:"boolean"},allow:{type:"string",multiple:!0},mode:{type:"string"},cursor:{type:"string"},limit:{type:"string"},"mcp-config":{type:"string"},session:{type:"string"},repo:{type:"string"},ref:{type:"string"},notes:{type:"string",multiple:!0},agent:{type:"string",multiple:!0},hide:{type:"string",multiple:!0},support:{type:"string",multiple:!0},"dry-run":{type:"boolean"},room:{type:"string"},force:{type:"boolean"},review:{type:"boolean"},"report-to":{type:"string"}}}),[r,t,...w]=A;if(e.help||!r){p(J);return}const _=y((o,s)=>{if(!o)throw new Error(`${s} is required. See --help.`);return o},"required"),n=y(o=>{if(A.length!==o)throw new Error("Unexpected or missing arguments. See --help.")},"exact"),E=y((o,s,l)=>{if(o===void 0)return s;if(!/^\d+$/u.test(o))throw new Error("Expected an unsigned integer option.");const d=Number(o);if(!Number.isSafeInteger(d)||d>l)throw new Error("Numeric option is out of range.");return d},"number"),a=y(o=>p(JSON.stringify(o,null,2)),"print");if(r==="init"){n(1);const o=_(e.config,"--config");await we(o,{url:e.url??"http://127.0.0.1:8787",token:ee(32).toString("base64url")}),p(`Owner credentials saved to ${B(o)}.`);return}if(r==="server"){n(1);const o=e.config?await fe(e.config):void 0,s=f.POCOLOBBY_ADMIN_TOKEN||o?.token;if(!s||s.length<32||/[\r\n]/u.test(s))throw new Error("Set POCOLOBBY_ADMIN_TOKEN (at least 32 characters), or provide --config.");const l=$e(f),{startServer:d}=await import("./server.js"),u=await d({database:B(_(e.db,"--db")),adminToken:s,host:e.host??"127.0.0.1",port:E(e.port,8787,65535),allowRemote:e["allow-remote"]??!1,...l});p(`PocoLobby listening at ${u.url}.`),await new Promise((O,L)=>{let N=!1;const P=y(()=>{N||(N=!0,process.off("SIGINT",P),process.off("SIGTERM",P),u.close().then(O,L))},"stop");process.on("SIGINT",P),process.on("SIGTERM",P)});return}if(r==="backup"||r==="recover"){n(1);const o=B(_(e.db,"--db")),{acquireDatabaseLock:s}=await import("./lock.js"),{backupDatabase:l,LobbyStore:d}=await import("./store.js");if(!(await D(o).catch(()=>null))?.isFile())throw new Error("Database must be an existing regular file.");const u=s(o);try{if(r==="backup"){const O=B(_(e.out,"--out"));await(await ae(O,"wx",384)).close();try{await l(o,O)}catch(N){throw await ie(O).catch(()=>{}),N}p(`Backup saved to ${O}. Treat it as sensitive data.`)}else{const O=new d(o);try{O.enterRecovery()}finally{O.close()}p("Recovery mode enabled. Re-establish credentials and reconcile agent state before resuming delivery.")}}finally{u()}return}if(r==="check-config"){n(1);const o=await He(B(_(e["mcp-config"],"--mcp-config")),f.PATH??f.Path);if(a(o),o.problems.length)throw new Error(o.problems.join(" "));return}if(r==="launch-script"){const o=e.session??f.CLAUDE_CODE_SESSION_ID;if(!o)throw new Error("Run launch-script in the agent's own Claude conversation, or pass --session ID.");const s=B(_(e["mcp-config"],"--mcp-config"));if(!(await D(s)).isFile())throw new Error("--mcp-config must name the agent's generated MCP file.");const l=Ae({sessionId:o,mcpConfig:s,profile:f.CLAUDE_CONFIG_DIR||void 0,flags:A.slice(1),home:le(),windows:process.platform==="win32"}),d=B("launch-agent");await ce(d,l,{mode:493,flag:"wx"}),p(`Saved ${d}: run ./launch-agent to relaunch this conversation.`),b(Ue(d));return}if(!De.has(r))throw new Error(`Unknown command ${r}. See --help.`);const{config:h,identity:T}=await G(_(e.config,"--config"));if(r==="set-url"){n(1);const o=q({...h,url:_(e.url,"--url")});await new K(o).request("GET","/v1/agents"),await ge(T.path,o.url),p(`${T.path} now uses ${o.url}. A running adapter moves on /mcp reconnect pocolobby.`);return}if(r==="statusline"){n(1);const{runStatusLine:o}=await import("./statusline.js");p(await o(h,T.path,await(i.stdin??W)(),{dryRun:e["dry-run"]??!1}));return}if(r==="memory"&&t==="curate"){n(2);const{curate:o}=await import("./memory-curate.js"),s=await o(h,{room:C.parse(_(e.room,"--room")),repo:e.repo??process.cwd(),force:e.force??!1,review:e.review??!1,dryRun:e["dry-run"]??!1,...e["report-to"]?{reportTo:I.parse(e["report-to"])}:{}});for(const l of s.lines)p(l);if(s.failed)throw new Error("The curation failed; see above.");return}if(r==="repo"&&t==="stats"){n(2);const{repoStats:o}=await import("./code-stats.js");p(await o(h,{room:C.parse(_(e.room,"--room")),repo:e.repo??process.cwd()}));return}if(r==="hook"){n(1);const{memoryHook:o,runHook:s}=await import("./hook.js"),l=await(i.stdin??W)();if(e.memory!==void 0){const u=Number(e.memory);if(u!==1&&u!==2&&u!==3)throw new Error("--memory takes 1, 2 or 3.");const O=await o(h,u);O&&p(O);return}const d=await s(h,l,{dryRun:e["dry-run"]??!1});d&&p(d);return}if(r==="adapter"){if(n(1),e.lobby)throw new Error("An agent lobby is bound to its credential; --lobby is for owner administration.");const{runClaudeAdapter:o}=await import("./adapters/claude.js");await o({...h,hostSessionId:f.CLAUDE_CODE_SESSION_ID,credentialFile:T});return}if(r==="mcp-config"){if(n(1),e.lobby)throw new Error("An agent lobby is bound to its credential; --lobby is for owner administration.");const o=Se({execPath:process.execPath,script:j(import.meta.url),version:process.versions.node,searchPath:f.PATH??f.Path});a({mcpServers:{pocolobby:{command:o.command,args:[o.script,"adapter","--config",B(e.config)]}}}),b(`pocolobby: the adapter runs ${o.command} ${o.script} (PocoLobby ${$(o.script)??"of unknown version"}).`),o.versionPinned&&b(`pocolobby: these paths name Node ${process.versions.node}'s own directory. Run mcp-config again after upgrading or removing that Node version.`);return}const c=new K(h),S=e.lobby===void 0?void 0:Y.parse(e.lobby),g=y(o=>S===void 0?o:`${o}${o.includes("?")?"&":"?"}lobby=${encodeURIComponent(S)}`,"scoped");if(r==="settings"){n(1);const{ticket:o}=await c.request("POST","/v1/admin/settings/ticket"),s=`${new URL("/",c.url).href}#ticket=${o}`;p(s),(i.browse??Ye)(s);return}if(r==="lobbies"){n(1),a(await c.request("GET","/v1/admin/lobbies"));return}if(r==="lobby"){if(t==="display-name"){if(A.length!==3&&A.length!==4)throw new Error("Unexpected or missing arguments. See --help.");a(await c.request("PATCH",`/v1/admin/lobbies/${Y.parse(w[0])}`,{display_name:w[1]??null}));return}if(n(3),t!=="create")throw new Error("Unknown lobby operation. See --help.");a(await c.request("POST","/v1/admin/lobbies",{lobby_id:Ie.parse(w[0])}));return}if(r==="rooms"){n(1),a(await c.request("GET",g("/v1/admin/rooms")));return}if(r==="room"){n(3);const o=C.parse(w[0]);if(t==="create")a(await c.request("POST",g("/v1/admin/rooms"),{room_id:o}));else if(t==="delete")a(await c.request("DELETE",g(`/v1/admin/rooms/${o}`)));else if(t==="add"||t==="remove"){if(e.agents!==void 0&&e.member!==void 0)throw new Error("Use --agents or repeated --member, not both.");const s=e.member??z(e.agents??"");a(await c.request("PATCH",g(`/v1/admin/rooms/${o}/members`),ke.parse({[t]:s})))}else if(t==="members"){if(e.agents!==void 0&&e.member!==void 0)throw new Error("Use --agents or repeated --member, not both.");if(e.agents===void 0&&e.member===void 0)throw new Error('Provide --agents or repeated --member; use --agents "" to clear membership.');const s=e.member??z(e.agents);a(await c.request("PUT",g(`/v1/admin/rooms/${o}/members`),Me.parse({members:s})))}else throw new Error("Unknown room operation. See --help.");return}if(r==="memory"){if(t==="show"){n(2),a(await c.request("GET",g("/v1/admin/memory")));return}if(t==="curator"){n(4);const o=C.parse(w[0]),s=w[1]==="none"?null:I.parse(w[1]);a(await c.request("PUT",g(`/v1/admin/memory/rooms/${encodeURIComponent(o)}/curator`),{agent_id:s}));return}throw new Error("Unknown memory operation. See --help.")}if(r==="agents"){n(1),a(await c.request("GET",g("/v1/admin/agents")));return}if(r==="agent"){n(3);const o=y(()=>{if(e.screen!==void 0&&!["yes","no"].includes(e.screen))throw new Error("--screen takes yes or no.");return{...e.work!==void 0?{work_description:e.work}:{},...e.machine!==void 0?{machine:e.machine}:{},...e.screen!==void 0?{screen:e.screen==="yes"}:{}}},"description"),s=I.parse(w[0]),l=`/v1/admin/agents/${encodeURIComponent(s)}`;if(t==="add"||t==="rotate"){const u=_(e.out,"--out"),O=t==="add"?Ce.parse({agent_id:s,lobby_id:S,manage_rooms:e.coordinator??!1,role:e["role-file"]?await R(e["role-file"],"utf8"):"",...o(),can_broadcast:e.broadcast??!1,allowed_recipients:e.allow??null}):void 0,L=await he(u);let N=!1,P=S?`${S}/`:"";try{const M=await c.request("POST",g(t==="add"?"/v1/admin/agents":`${l}/rotate`),O??{});N=!0,await L.write(q({url:h.url,token:M.token})),M.agent&&(P=`${M.agent.lobby_id}/`)}catch(M){throw await L.abort(),N?new Error("Server changed the credential, but its file could not be saved. Use owner agent rotate with a new writable --out path."):M}p(`Agent ${P}${s} credentials saved to ${B(u)}.`);return}const d={pause:{paused:!0},resume:{paused:!1},disable:{enabled:!1},enable:{enabled:!0}};if(t&&d[t])a(await c.request("PATCH",g(l),d[t]));else if(t==="describe"){const u=o();if(!Object.keys(u).length)throw new Error("Name what to change: --work, --machine or --screen yes|no.");a(await c.request("PATCH",g(l),Le.parse(u)))}else if(t==="retry"||t==="reset-runtime")a(await c.request("POST",g(`${l}/${t}`),{}));else throw new Error("Unknown agent operation. See --help.");return}if(r==="send"||r==="broadcast"||r==="send-room"){const o=r==="send"?I.parse(t):"*",s=r==="broadcast"?A.slice(1):w;if(e["content-file"]&&s.length>0)throw new Error("Use a message argument or --content-file, not both.");if(!e["content-file"]&&s.length!==1)throw new Error("Provide exactly one quoted message or --content-file.");const l=e["content-file"]?await R(e["content-file"],"utf8"):s[0],d=e["request-id"]??oe(),u=Re.parse({client_request_id:d,to:o,priority:e.important?"important":"info",content:l,correlation_id:e["correlation-id"],...r==="send-room"?{room:C.parse(t)}:{},...e.kind===void 0?{}:{kind:e.kind},...e["deliver-at"]===void 0?{}:{deliver_at:e["deliver-at"]}});b(`Request ID: ${d}`),a(await c.request("POST",g("/v1/messages"),u));return}if(r==="schedules"){n(2);const o=I.parse(t);a(e.cancel===void 0?await c.request("GET",g(`/v1/admin/agents/${o}/schedules`)):await c.request("POST",g(`/v1/admin/agents/${o}/schedules/cancel`),ve.parse({id:e.cancel})));return}if(r==="dashboard"){const o=y((l,d="")=>`/v1/admin/dashboard${d}?lobby=${encodeURIComponent(Y.parse(l))}`,"boardPath");if(t==="source"){n(5);const[l,d,u]=w,O=o(d,`/sources/${C.parse(u)}`);if(l==="set")a(await c.request("PUT",O,Oe.parse({repo:e.repo,ref:e.ref,notes:e.notes})));else if(l==="remove")a(await c.request("DELETE",O));else throw new Error("Unknown dashboard source operation. See --help.");return}if(t==="icon"){const[l,d,u]=w;if(l==="set"){n(5);const O=(await R(u)).toString("base64");a(await c.request("PUT",o(d,"/icon"),{png:O}))}else if(l==="remove")n(4),a(await c.request("DELETE",o(d,"/icon")));else throw new Error("Unknown dashboard icon operation. See --help.");return}n(3);const s=w[0];if(t==="status")a(await c.request("GET",o(s)));else if(t==="agents"){if(!e.agent?.length&&!e.hide?.length&&!e.support?.length)throw new Error("Give at least one --agent ID=EMOJI or --hide ID.");const l=Object.fromEntries((e.agent??[]).map(d=>{const u=/^([^=\s]+)=(\S+)(?:\s+(.+))?$/u.exec(d.trim());if(!u)throw new Error('Use --agent ID=EMOJI or --agent "ID=EMOJI Name".');return[u[1],{emoji:u[2],...u[3]?{name:u[3].trim()}:{}}]}));for(const d of e.hide??[])l[d]={...l[d],hidden:!0};for(const d of e.support??[])l[d]={...l[d],support:!0};a(await c.request("PUT",o(s,"/agents"),ye.parse({agents:l})))}else throw new Error("Unknown dashboard operation. See --help.");return}const v=y((o,s)=>`/v1/admin/settings/${s}?lobby=${encodeURIComponent(Y.parse(o))}`,"settingsPath");if(r==="accounts"){if(t==="list")n(2),a(await c.request("GET","/v1/admin/settings/accounts"));else if(t==="allow"||t==="enable"||t==="disable")n(3),a(await c.request("POST","/v1/admin/settings/accounts",{email:X.parse(w[0]),enabled:t!=="disable",...t==="allow"?{may_create_lobbies:!0}:{}}));else if(t==="grant"||t==="revoke")n(4),a(await c.request(t==="grant"?"PUT":"POST",v(w[1],t==="grant"?"owner":"owner/revoke"),{email:X.parse(w[0])}));else throw new Error("Unknown accounts operation. See --help.");return}if(r==="repos"){const[o,s]=w;if(o==="list"||o===void 0)n(o===void 0?2:3),a(await c.request("GET",v(t,"repos")));else if(o==="add")n(4),a(await c.request("POST",v(t,"repos"),{repo:F.parse(s)}));else if(o==="remove"){n(4);const[l,d]=F.parse(s).split("/");a(await c.request("DELETE",v(t,`repos/${encodeURIComponent(l)}/${encodeURIComponent(d)}`)))}else throw new Error("Unknown repos operation. See --help.");return}if(r==="attachments"){if(t!==void 0&&t!=="usage")throw new Error("Unknown attachments operation. See --help.");if(n(t===void 0?1:2),t==="usage"){a(await c.request("GET",g("/v1/admin/attachments/usage")));return}const o=new URLSearchParams({limit:String(E(e.limit,50,100))});e.cursor!==void 0&&o.set("cursor",String(E(e.cursor,0,Number.MAX_SAFE_INTEGER))),a(await c.request("GET",g(`/v1/admin/attachments?${o}`)));return}if(r==="inbox"||r==="context"){n(2);const o=I.parse(t);if(r==="context")a(await c.request("GET",g(`/v1/admin/agents/${o}/context`)));else{const s=e.mode??"pending";if(!["pending","history","reconciliation"].includes(s))throw new Error("Unknown inbox mode.");const l=new URLSearchParams({mode:s,limit:String(E(e.limit,50,100))});e.cursor!==void 0&&l.set("cursor",String(E(e.cursor,0,Number.MAX_SAFE_INTEGER))),a(await c.request("GET",g(`/v1/admin/agents/${o}/messages?${l}`)))}return}throw new Error("Unknown command. See --help.")}y(xe,"runCli");function Ue(m){let i,p;try{[i,p]=Z("git",["rev-parse","--show-toplevel","--git-path","info/exclude"],{cwd:x(m),encoding:"utf8",stdio:["ignore","pipe","ignore"]}).trim().split(/\r?\n/u)}catch{}if(!i||!p)return"pocolobby: this is not a Git checkout, so nothing was excluded.";const b=`/${de(k(i),k(m)).split(me).join("/").replace(/[*?[\\]/gu,"\\$&")}`,f=B(x(m),p);let e="";try{e=re(f,"utf8")}catch{ne(x(f),{recursive:!0})}return e.split(/\r?\n/u).includes(b)||te(f,`${e&&!e.endsWith(`
|
|
120
|
+
`)?`
|
|
121
|
+
`:""}${b}
|
|
122
|
+
`),`pocolobby: Git ignores ${b} through ${f}.`}y(Ue,"excludeFromGit");async function He(m,i){const p=await D(m);if(!p.isFile()||p.size>1024*1024)throw new Error("The MCP configuration must be a regular file smaller than 1 MiB.");let b;try{b=JSON.parse(await R(m,"utf8")).mcpServers?.pocolobby}catch{throw new Error("The MCP configuration is not valid JSON.")}if(!b||typeof b.command!="string"||!Array.isArray(b.args)||!b.args.every(T=>typeof T=="string"))throw new Error("The MCP configuration has no mcpServers.pocolobby entry with command and args.");const f=b.args,e=y(async T=>T!==void 0&&H(T)&&((await D(T).catch(()=>null))?.isFile()??!1),"exists"),A=b.command,r=f[0]??"",t=f[1]==="adapter"&&f[2]==="--config"?f[3]:void 0,w=j(import.meta.url),_=y(T=>{try{return k(T)}catch{return null}},"real"),n={mcp_config:m,command:{path:A,exists:await e(A),real_path:_(A)},module:{path:r,exists:await e(r),version:await e(r)?$(r):null,real_path:_(r)},credential_config:{path:t??null,exists:await e(t),broker_url:null},path_command:null,this_cli:{module:w,version:$(w),package_root:U(w)},same_install_as_path:null,problems:[]},E=Te(i);E&&(n.path_command={...E,version:E.module?$(E.module):null,package_root:E.module?U(E.module):null});const a=n.module.exists?U(r):null;a&&n.path_command?.package_root&&(n.same_install_as_path=a===n.path_command.package_root);const h=n.problems;if(n.command.exists||h.push(`Its command ${A} does not exist, so Claude cannot start the adapter.`),n.module.exists?n.module.version||h.push(`Its module ${r} is not part of PocoLobby.`):h.push(`Its module ${r} does not exist.`),f.length!==4||t===void 0)h.push('Its arguments are not "<module> adapter --config <file>".');else if(!n.credential_config.exists)h.push(`Its credential configuration ${t} does not exist.`);else if((await se(t).catch(()=>null))?.isSymbolicLink())h.push(`Its credential configuration ${t} is a symbolic link; the adapter refuses links. Point the MCP file at the file itself.`);else try{n.credential_config.broker_url=(await G(t)).config.url}catch(T){h.push(`Its credential configuration ${t} cannot be used: ${T.message}`)}return n.same_install_as_path!==!0&&n.module.version&&n.path_command?.version&&n.module.version!==n.path_command.version&&h.push(`It starts PocoLobby ${n.module.version}, but pocolobby on PATH is ${n.path_command.version} (${n.path_command.module}). Install the package and run mcp-config with the same Node and npm prefix, then save the file again.`),h.length&&h.push("Generate the file again with pocolobby mcp-config from the intended install."),n}y(He,"checkMcpConfig");async function je(){const m=Be(process.versions.node,process.execPath);try{if(m)throw new Error(m);await xe(process.argv.slice(2))}catch(i){const p=i instanceof Ee&&i.detail?`${i.message} ${i.detail}`:i instanceof be?`invalid arguments: ${i.issues.map(b=>`${b.path.join(".")||"value"}: ${b.message}`).join("; ")}`:i instanceof Error?i.message:"Command failed.";process.stderr.write(`pocolobby: ${p}
|
|
123
|
+
`),process.exitCode=1}}y(je,"main"),process.argv[1]&&import.meta.url===pe(k(process.argv[1])).href&&je();export{He as checkMcpConfig,je as main,xe as runCli,$e as serverSettings};
|
package/dist/client.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var h=Object.defineProperty;var c=(i,t)=>h(i,"name",{value:t,configurable:!0});const w=12e4,b=67108864;class a extends Error{static{c(this,"LobbyClientError")}status;code;detail;missing;constructor(t,e,r,n){super(`PocoLobby request failed (${t||"network"}: ${e}).`),this.status=t,this.code=e,this.detail=r,this.missing=n,this.name="LobbyClientError"}}function d(i){if(typeof i!="string")return;const t=i.replace(/[\p{Cc}\p{Zl}\p{Zp}]+/gu," ").trim();return t?t.length>300?`${t.slice(0,297)}...`:t:void 0}c(d,"errorDetail");class p{static{c(this,"LobbyClient")}url;token;timeoutMs;extraHeaders;constructor(t){const e=new URL(t.url),r=["localhost","127.0.0.1","[::1]"].includes(e.hostname);if(e.protocol!=="https:"&&!(e.protocol==="http:"&&r))throw new Error("PocoLobby requires HTTPS, except for loopback HTTP.");if(e.username||e.password||e.search||e.hash)throw new Error("PocoLobby URLs must not contain credentials, queries, or fragments.");if(e.pathname!=="/")throw new Error("PocoLobby requires an origin URL without a path.");if(!t.token||/[\r\n]/.test(t.token))throw new Error("A valid PocoLobby credential is required.");if(this.url=e,this.token=t.token,this.timeoutMs=t.timeoutMs??1e4,this.extraHeaders={...t.headers},!Number.isFinite(this.timeoutMs)||this.timeoutMs<1||this.timeoutMs>6e4)throw new Error("PocoLobby request timeout must be between 1 and 60000 milliseconds.")}endpoint(t){if(!t.startsWith("/v1/")||t.includes("\\")||t.includes(".."))throw new Error("Invalid PocoLobby API path.");const e=new URL(t,this.url.origin);if(e.origin!==this.url.origin)throw new Error("PocoLobby API requests must stay on the configured origin.");return e}headers(t){return{...this.extraHeaders,authorization:`Bearer ${this.token}`,...t?{"x-pocolobby-runtime-id":t.runtime_id,"x-pocolobby-generation":String(t.generation),"x-pocolobby-incarnation":t.incarnation}:{}}}async request(t,e,r,n){const o=await this.send(t,e,n,this.timeoutMs,{...r===void 0?{}:{body:JSON.stringify(r),contentType:"application/json"}});return await this.json(o)}async upload(t,e,r){const n=await this.send("PUT",t,r,12e4,{body:e,contentType:"application/octet-stream"});return await this.json(n)}async download(t){const e=await this.send("GET",t,void 0,12e4,{});if(!e.ok)throw await this.json(e),new a(e.status,"request_rejected");try{return await this.read(e,67108864)}catch{throw new a(e.status,"invalid_response")}}async send(t,e,r,n,{body:o,contentType:s}){try{return await fetch(this.endpoint(e),{method:t,headers:{...this.headers(r),...s?{"content-type":s}:{}},...o===void 0?{}:{body:typeof o=="string"?o:new Uint8Array(o.buffer,o.byteOffset,o.byteLength)},redirect:"error",signal:AbortSignal.timeout(n)})}catch{throw new a(0,"connection_failed_or_timed_out")}}async read(t,e){const r=t.body?.getReader(),n=[];let o=0;if(r)for(;;){const{done:s,value:u}=await r.read();if(s)break;if(o+=u.byteLength,o>e)throw await r.cancel(),new Error("Response too large");n.push(u)}return Buffer.concat(n)}async json(t){let e;try{const r=await this.read(t,2097152);e=r.length?JSON.parse(r.toString("utf8")):null}catch{throw new a(t.status,"invalid_response")}if(!t.ok){const r=e,n=r?.error?.code,o=r?.error?.missing;throw new a(t.status,typeof n=="string"&&/^[a-zA-Z_][a-zA-Z0-9_]{0,63}$/.test(n)?n:"request_rejected",d(r?.error?.message),Array.isArray(o)&&o.length<=5&&o.every(s=>typeof s=="string"&&/^[0-9a-f]{64}$/u.test(s))?o:void 0)}return e}}export{p as LobbyClient,a as LobbyClientError};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
var L=Object.defineProperty;var c=(t,e)=>L(t,"name",{value:e,configurable:!0});import{execFile as X}from"node:child_process";import{promisify as Y}from"node:util";import{LobbyClient as z}from"./client.js";import{CODE_STATS_DAYS as _}from"./protocol.js";const B=Y(X),p=1440*60*1e3,I=["ts","tsx","mts","cts","js","jsx","mjs","cjs","swift","py","rb","go","rs","java","kt","kts","c","h","cc","cpp","hpp","m","mm","cs","php","sh","ps1","css","scss","sass","less","html","vue","svelte","astro","sql"],F=new Set(I),H=/(^|\/)(node_modules|dist|build|out|vendor|third_party|coverage)\/|\.min\.[a-z]+$/,K=/(^|\/)(tests?|__tests__|spec|e2e|[^/]*Tests)\/|\.(test|spec)\.[^/]+$/;function T(t){const e=/\.([^./]+)$/.exec(t)?.[1];return!e||!F.has(e)||H.test(t)?null:K.test(t)?"tests":"code"}c(T,"kindOf");const U=c(async(t,e)=>{try{return(await B("git",["-c","core.quotePath=false","-c","color.ui=false",...t],{cwd:e,maxBuffer:512*1024*1024})).stdout}catch(n){if(t[0]==="grep"&&n.code===1)return"";throw n}},"gitOf"),Z=c(t=>t.includes(" => ")?t.replace(/\{([^{}]*) => ([^{}]*)\}/g,"$2").replace(/^.* => /,"").replace(/\/\//g,"/"):t,"renamed");async function G(t,e,n){const r=await t(["grep","-c","-I","-e","",n,"--",...I.map(s=>`*.${s}`)],e),a={code:0,tests:0};for(const s of r.split(`
|
|
2
|
+
`)){const m=s.lastIndexOf(":");if(m<0)continue;const h=T(s.slice(n.length+1,m));h&&(a[h]+=Number(s.slice(m+1)))}return a}c(G,"linesAt");const j=c(t=>new Date(t).toISOString().slice(0,10),"dayOf");async function J(t,e,n={}){const r=n.git??U,a=(n.now??Date.now)(),s=(await r(["rev-parse","--show-toplevel"],e)).trim();if(await r(["fetch","--quiet","origin"],s),(await r(["rev-parse","--is-shallow-repository"],s)).trim()==="true")throw new Error(`${s} is a shallow clone; run git fetch --unshallow there first`);const m=(await r(["rev-parse",n.ref??"origin/main"],s)).trim(),f=Date.parse(`${j(a)}T00:00:00Z`)-(_-1)*p,g=Array.from({length:_},(o,i)=>({day:j(f+i*p),commits:0,authors:new Map,code:{lines:0,new:0,changed:0,removed:0},tests:{lines:0,new:0,changed:0,removed:0}})),k=await r(["log",m,"--first-parent","--diff-merges=first-parent",`--since-as-filter=${new Date(f).toISOString()}`,"--numstat","--format=%x00%H %ct %an"],s),b=[];for(const o of k.split("\0").slice(1)){const[i="",...l]=o.split(`
|
|
3
|
+
`),[u="",P="",...q]=i.split(" "),y=Number(P)*1e3;b.push({sha:u,ms:y});const d=g[Math.min(_-1,Math.floor((y-f)/p))];if(!d||y<f)continue;d.commits++;const N=q.join(" ").trim()||"unknown";d.authors.set(N,(d.authors.get(N)??0)+1);for(const C of l){const[$,x,...E]=C.split(" ");if(!E.length||$==="-"||x==="-")continue;const w=T(Z(E.join(" ")));if(!w)continue;const O=Math.min(Number($),Number(x));d[w].new+=Number($)-O,d[w].changed+=O,d[w].removed+=Number(x)-O}}const D=(await r(["rev-list","-1","--first-parent",`--until=${new Date(f-1e3).toISOString()}`,m],s)).trim(),M=c(o=>b.find(i=>i.ms<=o)?.sha??D,"at"),S=new Map,v=c(o=>o?(S.has(o)||S.set(o,G(r,s,o)),S.get(o)):Promise.resolve({code:0,tests:0}),"count"),A=await v(D);for(const[o,i]of g.entries()){const l=await v(M(f+(o+1)*p-1));i.code.lines=l.code,i.tests.lines=l.tests}return{room_id:t,head:m,computed_at:new Date(a).toISOString(),baseline:A,days:g.map(({authors:o,...i})=>({...i,authors:[...o].sort((l,u)=>u[1]-l[1]||l[0].localeCompare(u[0])).slice(0,12).map(([l,u])=>({name:l.slice(0,100),commits:u}))}))}}c(J,"codeStats");async function et(t,e){const n=await J(e.room,e.repo,{...e.git?{git:e.git}:{},...e.now?{now:e.now}:{}});await(e.client??new z(t)).request("POST","/v1/code-stats",n);const r=n.days.at(-1),a=c(s=>s.toLocaleString("en-US"),"number");return`${e.room}: ${a(r.commits)} commits today, ${a(r.code.lines)} lines of code and ${a(r.tests.lines)} of tests at ${n.head.slice(0,7)}; sent.`}c(et,"repoStats");export{I as CODE_EXTENSIONS,J as codeStats,T as kindOf,et as repoStats};
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
var m=Object.defineProperty;var n=(o,r)=>m(o,"name",{value:r,configurable:!0});import{randomBytes as p}from"node:crypto";import{constants as i}from"node:fs";import{open as f,lstat as g,mkdir as O,realpath as C,rename as v,unlink as u}from"node:fs/promises";import{dirname as E,resolve as d}from"node:path";function w(o){if(typeof o!="object"||o===null||Array.isArray(o))throw new Error("Configuration must contain url and token.");const r=o;if(Object.keys(r).some(e=>e!=="url"&&e!=="token")||typeof r.url!="string"||typeof r.token!="string"||!/^[A-Za-z0-9_-]{32,512}$/u.test(r.token))throw new Error("Configuration must contain a URL and a token of 32\u2013512 URL-safe characters.");let t;try{t=new URL(r.url)}catch{throw new Error("Configuration URL is invalid.")}const a=["localhost","127.0.0.1","[::1]"].includes(t.hostname);if(t.protocol!=="https:"&&!(t.protocol==="http:"&&a)||t.username||t.password||t.search||t.hash||t.pathname!=="/")throw new Error("Use an HTTPS origin, or loopback HTTP, without a path, credentials, query, or fragment.");return{url:t.toString().replace(/\/$/u,""),token:r.token}}n(w,"validateConfig");async function N(o){return(await h(o)).config}n(N,"readConfig");async function h(o){const r=await f(o,i.O_RDONLY|i.O_NOFOLLOW);try{const t=await r.stat();if(!t.isFile()||t.size>64*1024)throw new Error("Configuration must be a regular file smaller than 64 KiB.");if(process.platform!=="win32"){if((t.mode&63)!==0)throw new Error("Configuration contains credentials; restrict its permissions to 0600.");if(process.getuid&&t.uid!==process.getuid())throw new Error("Configuration must be owned by the current user.")}let a;try{a=JSON.parse(await r.readFile("utf8"))}catch{throw new Error("Configuration is not valid JSON.")}return{config:w(a),identity:{path:await C(o),dev:t.dev,ino:t.ino}}}finally{await r.close()}}n(h,"loadConfig");async function k(o){const r=d(o);await O(E(r),{recursive:!0,mode:448});const t=await f(r,i.O_WRONLY|i.O_CREAT|i.O_EXCL,384),a=await t.stat();let e=!1,s=!1;const c=n(async()=>{e||(e=!0,await t.close())},"close");return{async write(l){if(e)throw new Error("Credential destination is already closed.");const y=w(l);try{await t.writeFile(`${JSON.stringify(y,null,2)}
|
|
2
|
+
`,"utf8"),await t.sync(),s=!0}finally{await c()}},async abort(){if(await c(),s)return;const l=await g(r).catch(()=>null);l?.ino===a.ino&&l.dev===a.dev&&await u(r)}}}n(k,"reserveConfig");async function S(o,r){const t=await k(o);try{await t.write(r)}catch(a){throw await t.abort(),a}}n(S,"writeConfigExclusive");async function T(o,r){const{config:t}=await h(o),a=w({...t,url:r}),e=d(o),s=`${e}.${p(6).toString("hex")}.tmp`,c=await f(s,i.O_WRONLY|i.O_CREAT|i.O_EXCL,384);try{try{await c.writeFile(`${JSON.stringify(a,null,2)}
|
|
3
|
+
`,"utf8"),await c.sync()}finally{await c.close()}await v(s,e)}catch(l){throw await u(s).catch(()=>{}),l}return a}n(T,"setConfigUrl");export{h as loadConfig,N as readConfig,k as reserveConfig,T as setConfigUrl,w as validateConfig,S as writeConfigExclusive};
|