@lightupai/polaris 0.0.77 → 0.0.79
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/.github/workflows/cd.yml +77 -0
- package/docs/architecture.md +203 -0
- package/docs/product-critique-2026-06.md +136 -0
- package/docs/readiness-review-2026-06.md +130 -0
- package/hooks/capture-prompt.ts +60 -0
- package/hooks/capture-stop.ts +10 -3
- package/hooks/capture.sh +12 -0
- package/hooks/statusline.sh +8 -2
- package/package.json +1 -1
- package/scripts/backup.sh +71 -0
- package/src/bridge-discover-org.ts +30 -4
- package/src/cli/cli.ts +304 -34
- package/src/client/client.ts +44 -11
- package/src/daemon/daemon.ts +262 -38
- package/src/service/auth.ts +10 -1
- package/src/service/db.ts +338 -63
- package/src/service/server.ts +267 -11
- package/src/slack/bridge.ts +172 -34
- package/src/types.ts +22 -0
- package/src/web/app.ts +379 -8
- package/src/web/layout.ts +10 -3
- package/src/web/pages.ts +2 -2
- package/src/web/views.ts +420 -6
- package/tests/acl.test.ts +100 -0
- package/tests/annotations.test.ts +209 -0
- package/tests/bridge-discover.test.ts +70 -0
- package/tests/daemon-auth.test.ts +123 -0
- package/tests/event-lookup.test.ts +113 -0
- package/tests/label.test.ts +90 -0
- package/tests/pagination.test.ts +165 -0
- package/tests/recover.test.ts +83 -0
- package/tests/reply-attribution.test.ts +103 -0
- package/tests/search-acl.test.ts +117 -0
- package/tests/search.test.ts +145 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// hooks/capture-prompt.ts — Forward UserPromptSubmit hook events to the daemon
|
|
3
|
+
// and deliver any queued advisor injects to the agent via additionalContext.
|
|
4
|
+
//
|
|
5
|
+
// The daemon queues inject events arriving over its cloud WebSocket (see
|
|
6
|
+
// injectQueues in src/daemon/daemon.ts); the /events response for a
|
|
7
|
+
// UserPromptSubmit hook drains that queue as pendingInjects, which we surface
|
|
8
|
+
// to Claude Code as hookSpecificOutput. Always exits 0; never blocks.
|
|
9
|
+
|
|
10
|
+
const POLARIS_PORT = process.env.POLARIS_PORT ?? "4322";
|
|
11
|
+
const POLARIS_URL = `http://127.0.0.1:${POLARIS_PORT}/events`;
|
|
12
|
+
|
|
13
|
+
// Daemon auth: include the shared local secret when provided (wired into the
|
|
14
|
+
// hook command env by `polaris install`)
|
|
15
|
+
const headers: Record<string, string> = { "Content-Type": "application/json" };
|
|
16
|
+
if (process.env.POLARIS_DAEMON_SECRET) {
|
|
17
|
+
headers["x-polaris-daemon-secret"] = process.env.POLARIS_DAEMON_SECRET;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const input = JSON.parse(await Bun.stdin.text());
|
|
22
|
+
|
|
23
|
+
const res = await fetch(POLARIS_URL, {
|
|
24
|
+
method: "POST",
|
|
25
|
+
headers,
|
|
26
|
+
body: JSON.stringify(input),
|
|
27
|
+
}).catch(() => null);
|
|
28
|
+
|
|
29
|
+
if (res && res.ok) {
|
|
30
|
+
const body = (await res.json().catch(() => null)) as {
|
|
31
|
+
pendingInjects?: Array<{ from: string; content: string; timestamp: string }>;
|
|
32
|
+
} | null;
|
|
33
|
+
const items = body?.pendingInjects;
|
|
34
|
+
if (Array.isArray(items) && items.length > 0) {
|
|
35
|
+
// Present injects with honest provenance and let the model apply its own
|
|
36
|
+
// judgment. NOTE (verified live): the model treats content arriving via
|
|
37
|
+
// this channel as not-from-the-developer and will surface/consider it but
|
|
38
|
+
// will NOT obey imperative instructions through it — no hook wording
|
|
39
|
+
// reliably changes that, nor should it. Use injects to SHARE CONTEXT with
|
|
40
|
+
// the agent, not to remote-control its actions. (For trusted imperative
|
|
41
|
+
// steering, the claude/channel path is the right mechanism — deferred.)
|
|
42
|
+
const formatted =
|
|
43
|
+
"Context shared by teammates collaborating in this session via Polaris " +
|
|
44
|
+
"(for your awareness; it did not come directly from the developer):\n" +
|
|
45
|
+
items.map((i) => `- ${i.from}: ${i.content}`).join("\n");
|
|
46
|
+
process.stdout.write(
|
|
47
|
+
JSON.stringify({
|
|
48
|
+
hookSpecificOutput: {
|
|
49
|
+
hookEventName: "UserPromptSubmit",
|
|
50
|
+
additionalContext: formatted,
|
|
51
|
+
},
|
|
52
|
+
})
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
} catch {
|
|
57
|
+
// Always exit 0 to avoid blocking the coding agent
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
process.exit(0);
|
package/hooks/capture-stop.ts
CHANGED
|
@@ -9,6 +9,13 @@
|
|
|
9
9
|
const POLARIS_PORT = process.env.POLARIS_PORT ?? "4322";
|
|
10
10
|
const POLARIS_URL = `http://127.0.0.1:${POLARIS_PORT}/events`;
|
|
11
11
|
|
|
12
|
+
// Daemon auth: include the shared local secret when provided (wired into the
|
|
13
|
+
// hook command env by `polaris install`)
|
|
14
|
+
const headers: Record<string, string> = { "Content-Type": "application/json" };
|
|
15
|
+
if (process.env.POLARIS_DAEMON_SECRET) {
|
|
16
|
+
headers["x-polaris-daemon-secret"] = process.env.POLARIS_DAEMON_SECRET;
|
|
17
|
+
}
|
|
18
|
+
|
|
12
19
|
try {
|
|
13
20
|
const input = JSON.parse(await Bun.stdin.text());
|
|
14
21
|
|
|
@@ -16,7 +23,7 @@ try {
|
|
|
16
23
|
// Not a Stop event — forward as-is
|
|
17
24
|
await fetch(POLARIS_URL, {
|
|
18
25
|
method: "POST",
|
|
19
|
-
headers
|
|
26
|
+
headers,
|
|
20
27
|
body: JSON.stringify(input),
|
|
21
28
|
}).catch(() => {});
|
|
22
29
|
process.exit(0);
|
|
@@ -102,7 +109,7 @@ try {
|
|
|
102
109
|
|
|
103
110
|
const res = await fetch(POLARIS_URL, {
|
|
104
111
|
method: "POST",
|
|
105
|
-
headers
|
|
112
|
+
headers,
|
|
106
113
|
body: JSON.stringify(payload),
|
|
107
114
|
}).catch(() => null);
|
|
108
115
|
|
|
@@ -111,7 +118,7 @@ try {
|
|
|
111
118
|
delete (payload as Record<string, unknown>).raw_turn;
|
|
112
119
|
await fetch(POLARIS_URL, {
|
|
113
120
|
method: "POST",
|
|
114
|
-
headers
|
|
121
|
+
headers,
|
|
115
122
|
body: JSON.stringify(payload),
|
|
116
123
|
}).catch(() => {});
|
|
117
124
|
}
|
package/hooks/capture.sh
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
# hooks/capture.sh - Forward Claude Code hook events to polaris local client
|
|
3
3
|
# Reads hook JSON from stdin, POSTs to the local polaris client.
|
|
4
4
|
# Always exits 0 to avoid blocking the coding agent.
|
|
5
|
+
#
|
|
6
|
+
# The raw hook payload is forwarded verbatim — no fields are stripped — so any
|
|
7
|
+
# tool approval/rejection or permission decision fields Claude Code includes
|
|
8
|
+
# (e.g. permission_mode, permission decisions on PreToolUse/PostToolUse) pass
|
|
9
|
+
# through to the daemon untouched.
|
|
5
10
|
|
|
6
11
|
POLARIS_PORT="${POLARIS_PORT:-4322}"
|
|
7
12
|
POLARIS_URL="http://127.0.0.1:${POLARIS_PORT}/events"
|
|
@@ -9,9 +14,16 @@ POLARIS_URL="http://127.0.0.1:${POLARIS_PORT}/events"
|
|
|
9
14
|
# Read all of stdin
|
|
10
15
|
INPUT=$(cat)
|
|
11
16
|
|
|
17
|
+
# Shared local daemon secret (wired by `polaris install`); sent when available
|
|
18
|
+
SECRET_HEADER=""
|
|
19
|
+
if [ -n "$POLARIS_DAEMON_SECRET" ]; then
|
|
20
|
+
SECRET_HEADER="x-polaris-daemon-secret: $POLARIS_DAEMON_SECRET"
|
|
21
|
+
fi
|
|
22
|
+
|
|
12
23
|
# POST to polaris local client, fail silently
|
|
13
24
|
curl -s -X POST \
|
|
14
25
|
-H "Content-Type: application/json" \
|
|
26
|
+
${SECRET_HEADER:+-H "$SECRET_HEADER"} \
|
|
15
27
|
-d "$INPUT" \
|
|
16
28
|
"$POLARIS_URL" \
|
|
17
29
|
>/dev/null 2>&1 || true
|
package/hooks/statusline.sh
CHANGED
|
@@ -4,6 +4,12 @@
|
|
|
4
4
|
|
|
5
5
|
POLARIS_DAEMON_PORT="${POLARIS_DAEMON_PORT:-4322}"
|
|
6
6
|
|
|
7
|
+
# Shared local daemon secret (wired by `polaris install`); sent when available
|
|
8
|
+
SECRET_HEADER=""
|
|
9
|
+
if [ -n "$POLARIS_DAEMON_SECRET" ]; then
|
|
10
|
+
SECRET_HEADER="x-polaris-daemon-secret: $POLARIS_DAEMON_SECRET"
|
|
11
|
+
fi
|
|
12
|
+
|
|
7
13
|
# Read stdin (session JSON from the coding agent)
|
|
8
14
|
INPUT=$(cat)
|
|
9
15
|
|
|
@@ -12,13 +18,13 @@ CC_SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // ""' 2>/dev/null || echo ""
|
|
|
12
18
|
|
|
13
19
|
# Query daemon — try specific session first, fall back to any connected session
|
|
14
20
|
if [ -n "$CC_SESSION_ID" ]; then
|
|
15
|
-
STATUS=$(curl -s "http://127.0.0.1:${POLARIS_DAEMON_PORT}/status/${CC_SESSION_ID}" 2>/dev/null)
|
|
21
|
+
STATUS=$(curl -s ${SECRET_HEADER:+-H "$SECRET_HEADER"} "http://127.0.0.1:${POLARIS_DAEMON_PORT}/status/${CC_SESSION_ID}" 2>/dev/null)
|
|
16
22
|
CONNECTED=$(echo "$STATUS" | jq -r '.connected' 2>/dev/null || echo "false")
|
|
17
23
|
fi
|
|
18
24
|
|
|
19
25
|
# If no match, check if there's any active session
|
|
20
26
|
if [ "$CONNECTED" != "true" ]; then
|
|
21
|
-
STATUS=$(curl -s "http://127.0.0.1:${POLARIS_DAEMON_PORT}/status" 2>/dev/null)
|
|
27
|
+
STATUS=$(curl -s ${SECRET_HEADER:+-H "$SECRET_HEADER"} "http://127.0.0.1:${POLARIS_DAEMON_PORT}/status" 2>/dev/null)
|
|
22
28
|
FIRST_SESSION=$(echo "$STATUS" | jq -r '.sessions[0] // empty' 2>/dev/null)
|
|
23
29
|
if [ -n "$FIRST_SESSION" ]; then
|
|
24
30
|
STATUS="{\"connected\":true,\"project\":$(echo "$STATUS" | jq '.sessions[0].project'),\"session\":$(echo "$STATUS" | jq '.sessions[0].session'),\"user\":$(echo "$STATUS" | jq '.sessions[0].user')}"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lightupai/polaris",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.79",
|
|
4
4
|
"description": "Record and stream Claude Code sessions to Slack. Like Gong for AI coding — capture every prompt, response, and tool call. Multiplayer collaboration across AI coding agents.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude-code",
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# Polaris Postgres backup: pg_dump to a timestamped file.
|
|
5
|
+
#
|
|
6
|
+
# Never destructive — this script only creates new files. It never deletes,
|
|
7
|
+
# rotates, or overwrites anything (it refuses to clobber an existing dump).
|
|
8
|
+
#
|
|
9
|
+
# Usage:
|
|
10
|
+
# DATABASE_URL=postgres://user:pass@host:5432/polaris ./scripts/backup.sh [output-dir]
|
|
11
|
+
#
|
|
12
|
+
# Parameters (env):
|
|
13
|
+
# DATABASE_URL (required) Postgres connection string to back up.
|
|
14
|
+
# BACKUP_DIR (optional) Output directory (default: ./backups).
|
|
15
|
+
# A positional arg, if given, takes precedence.
|
|
16
|
+
# BACKUP_S3_URI (optional) Object-storage target, e.g. s3://my-bucket/polaris.
|
|
17
|
+
# If set and the `aws` CLI is installed, the dump is
|
|
18
|
+
# also uploaded there (the local copy is kept).
|
|
19
|
+
#
|
|
20
|
+
# Cron wiring (on the prod host, e.g. nightly at 03:17 UTC):
|
|
21
|
+
# crontab -e
|
|
22
|
+
# 17 3 * * * DATABASE_URL=postgres://polaris:PASS@127.0.0.1:5432/polaris \
|
|
23
|
+
# BACKUP_DIR=/var/backups/polaris /opt/polaris/scripts/backup.sh \
|
|
24
|
+
# >> /var/log/polaris-backup.log 2>&1
|
|
25
|
+
#
|
|
26
|
+
# Retention is intentionally NOT handled here (this script never deletes).
|
|
27
|
+
# If you want it, pair the cron entry with something like:
|
|
28
|
+
# find /var/backups/polaris -name 'polaris-*.dump' -mtime +30 -delete
|
|
29
|
+
#
|
|
30
|
+
# Restore with:
|
|
31
|
+
# pg_restore --clean --if-exists -d "$DATABASE_URL" /path/to/polaris-<stamp>.dump
|
|
32
|
+
#
|
|
33
|
+
# Note: pg_dump's major version must be >= the server's (pg_dump aborts
|
|
34
|
+
# otherwise). On a host without a matching client, run via the container:
|
|
35
|
+
# docker exec <postgres-container> pg_dump --format=custom "$DATABASE_URL" > out.dump
|
|
36
|
+
|
|
37
|
+
DATABASE_URL="${DATABASE_URL:?DATABASE_URL is required (postgres://user:pass@host:port/db)}"
|
|
38
|
+
BACKUP_DIR="${1:-${BACKUP_DIR:-./backups}}"
|
|
39
|
+
BACKUP_S3_URI="${BACKUP_S3_URI:-}"
|
|
40
|
+
|
|
41
|
+
STAMP="$(date -u +%Y%m%d-%H%M%S)"
|
|
42
|
+
OUT="$BACKUP_DIR/polaris-$STAMP.dump"
|
|
43
|
+
|
|
44
|
+
mkdir -p "$BACKUP_DIR"
|
|
45
|
+
|
|
46
|
+
if [ -e "$OUT" ]; then
|
|
47
|
+
echo "Refusing to overwrite existing $OUT" >&2
|
|
48
|
+
exit 1
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
echo "Backing up to $OUT ..."
|
|
52
|
+
|
|
53
|
+
# Custom format (-Fc): compressed, restorable with pg_restore.
|
|
54
|
+
# Dump to a .partial first so an interrupted dump is never mistaken for a
|
|
55
|
+
# complete backup, then atomically rename.
|
|
56
|
+
pg_dump --format=custom --file="$OUT.partial" "$DATABASE_URL"
|
|
57
|
+
mv "$OUT.partial" "$OUT"
|
|
58
|
+
|
|
59
|
+
echo "Backup written: $OUT ($(du -h "$OUT" | cut -f1))"
|
|
60
|
+
|
|
61
|
+
if [ -n "$BACKUP_S3_URI" ]; then
|
|
62
|
+
if command -v aws >/dev/null 2>&1; then
|
|
63
|
+
echo "Uploading to $BACKUP_S3_URI/ ..."
|
|
64
|
+
aws s3 cp "$OUT" "$BACKUP_S3_URI/$(basename "$OUT")"
|
|
65
|
+
echo "Upload complete."
|
|
66
|
+
else
|
|
67
|
+
echo "BACKUP_S3_URI is set but the aws CLI is not installed; skipping upload (local copy kept)." >&2
|
|
68
|
+
fi
|
|
69
|
+
fi
|
|
70
|
+
|
|
71
|
+
echo "Done."
|
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
import postgres from "postgres";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
import type { Sql } from "./service/db";
|
|
3
|
+
|
|
4
|
+
// All orgs eligible for a Slack bridge: Slack-connected (team id) with a bot token.
|
|
5
|
+
export async function discoverBridgeOrgs(sql: Sql): Promise<string[]> {
|
|
6
|
+
const rows = await sql`
|
|
7
|
+
SELECT id FROM orgs
|
|
8
|
+
WHERE slack_team_id IS NOT NULL AND slack_bot_token IS NOT NULL
|
|
9
|
+
ORDER BY created_at ASC
|
|
10
|
+
`;
|
|
11
|
+
return rows.map((row) => row.id as string);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Back-compat: first eligible org (legacy single-org callers).
|
|
15
|
+
export async function discoverBridgeOrg(sql: Sql): Promise<string | null> {
|
|
16
|
+
const orgs = await discoverBridgeOrgs(sql);
|
|
17
|
+
return orgs[0] ?? null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// CLI mode (docker/bridge-entrypoint.sh): print the first eligible org id.
|
|
21
|
+
// Kept single-line so `ORG_ID=$(bun run src/bridge-discover-org.ts)` stays valid;
|
|
22
|
+
// multi-org deployments should run bridge.ts with no args (startAllBridges) instead.
|
|
23
|
+
if (import.meta.main) {
|
|
24
|
+
const sql = postgres(process.env.DATABASE_URL ?? "");
|
|
25
|
+
try {
|
|
26
|
+
const orgId = await discoverBridgeOrg(sql);
|
|
27
|
+
if (orgId) console.log(orgId);
|
|
28
|
+
} finally {
|
|
29
|
+
await sql.end();
|
|
30
|
+
}
|
|
31
|
+
}
|