Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a1b1d12da2 | ||
|
|
4fbb5a83a5 | ||
|
|
876b0820a4 | ||
|
|
bdf7aa6b3e | ||
|
|
a5e5796d18 |
@@ -21,9 +21,6 @@ LOG_LEVEL=info
|
|||||||
# /connect_calendar and follow the two-step flow — no refresh token needed here.
|
# /connect_calendar and follow the two-step flow — no refresh token needed here.
|
||||||
GOOGLE_CLIENT_ID=
|
GOOGLE_CLIENT_ID=
|
||||||
GOOGLE_CLIENT_SECRET=
|
GOOGLE_CLIENT_SECRET=
|
||||||
# Deprecated. If set at startup, seeded once into workspace/.secretary/google-token.json
|
|
||||||
# and then ignored. Use /connect_calendar in the bot for new setups.
|
|
||||||
GOOGLE_REFRESH_TOKEN=
|
|
||||||
|
|
||||||
# Playwright browser tools (MCP bridge). When PLAYWRIGHT_ENABLED=true the agent
|
# Playwright browser tools (MCP bridge). When PLAYWRIGHT_ENABLED=true the agent
|
||||||
# gets ~60 browser_* tools (navigate, click, type, screenshot, run arbitrary
|
# gets ~60 browser_* tools (navigate, click, type, screenshot, run arbitrary
|
||||||
|
|||||||
@@ -33,11 +33,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends python3-venv \
|
|||||||
&& /opt/tilion/bin/python -c "import tilion_fortress as t; tag = t._CHANNELS['latest']['tag']; t._download('linux-x64', t._host(tag), tag)"
|
&& /opt/tilion/bin/python -c "import tilion_fortress as t; tag = t._CHANNELS['latest']['tag']; t._download('linux-x64', t._host(tag), tag)"
|
||||||
|
|
||||||
# Claude Code CLI must be on PATH: pi-claude-code-provider shells out to `claude`.
|
# Claude Code CLI must be on PATH: pi-claude-code-provider shells out to `claude`.
|
||||||
# Pinned to the version pi-claude-code-provider 0.4.0 is tested against (its
|
RUN npm install -g @anthropic-ai/claude-code
|
||||||
# src/compatibility.ts). The provider rejects any stream-json record ahead of
|
|
||||||
# system/init, so an unpinned newer CLI can break every turn with
|
|
||||||
# "Claude emitted a record before initialization". Bump together with the provider.
|
|
||||||
RUN npm install -g @anthropic-ai/claude-code@2.1.278
|
|
||||||
|
|
||||||
COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
|
COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
|
||||||
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
|
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
|
||||||
|
|||||||
@@ -98,7 +98,6 @@ The agent's edits to `MEMORY.md` are local runtime data. Leave them uncommitted,
|
|||||||
| `CAPTCHA_API_KEY` | no | | Captcha solver key; enables `fortress_solve_captcha` and auto-solve during fetch |
|
| `CAPTCHA_API_KEY` | no | | Captcha solver key; enables `fortress_solve_captcha` and auto-solve during fetch |
|
||||||
| `GOOGLE_CLIENT_ID` | no | | OAuth client id (TVs and Limited Input devices) — enables the calendar tool |
|
| `GOOGLE_CLIENT_ID` | no | | OAuth client id (TVs and Limited Input devices) — enables the calendar tool |
|
||||||
| `GOOGLE_CLIENT_SECRET` | no | | OAuth client secret — required alongside `GOOGLE_CLIENT_ID` |
|
| `GOOGLE_CLIENT_SECRET` | no | | OAuth client secret — required alongside `GOOGLE_CLIENT_ID` |
|
||||||
| `GOOGLE_REFRESH_TOKEN` | no | | Deprecated. If set, seeded once into `workspace/.secretary/google-token.json`, then ignored. |
|
|
||||||
|
|
||||||
## Keeping it running on Windows
|
## Keeping it running on Windows
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ services:
|
|||||||
- LOG_LEVEL=${LOG_LEVEL:-info}
|
- LOG_LEVEL=${LOG_LEVEL:-info}
|
||||||
- GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID:-}
|
- GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID:-}
|
||||||
- GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET:-}
|
- GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET:-}
|
||||||
- GOOGLE_REFRESH_TOKEN=${GOOGLE_REFRESH_TOKEN:-}
|
|
||||||
- FORTRESS_ENABLED=${FORTRESS_ENABLED:-false}
|
- FORTRESS_ENABLED=${FORTRESS_ENABLED:-false}
|
||||||
- FORTRESS_HEADLESS=${FORTRESS_HEADLESS:-true}
|
- FORTRESS_HEADLESS=${FORTRESS_HEADLESS:-true}
|
||||||
- FORTRESS_CHANNEL=${FORTRESS_CHANNEL:-}
|
- FORTRESS_CHANNEL=${FORTRESS_CHANNEL:-}
|
||||||
|
|||||||
@@ -2,23 +2,26 @@
|
|||||||
set -e
|
set -e
|
||||||
|
|
||||||
PI_DIR="${PI_CODING_AGENT_DIR:-/pi-agent}"
|
PI_DIR="${PI_CODING_AGENT_DIR:-/pi-agent}"
|
||||||
# Marker version bumped from `.installed` to `.installed.v2` so existing volumes with a
|
PKG=pi-claude-code-provider
|
||||||
# previously-patched pi-claude-code-provider get a clean reinstall (we removed the
|
PKG_DIR="$PI_DIR/npm/node_modules/$PKG"
|
||||||
# claude-args.ts sed patch — see commit history).
|
|
||||||
MARKER="$PI_DIR/.pi-claude-code-provider.installed.v2"
|
|
||||||
|
|
||||||
if [ ! -f "$MARKER" ]; then
|
# Keep pi-claude-code-provider on the latest npm release: on every start compare the
|
||||||
echo "[entrypoint] pi install npm:pi-claude-code-provider"
|
# installed version with the registry and force a clean reinstall when they differ.
|
||||||
|
# If the registry is unreachable, keep whatever is installed.
|
||||||
|
# Install the exact version: an unversioned `npm install` keeps the range already in
|
||||||
|
# $PI_DIR/npm/package.json (e.g. ^0.2.0), which silently reinstalls the old release.
|
||||||
|
installed=$(node -p "require('$PKG_DIR/package.json').version" 2>/dev/null || true)
|
||||||
|
latest=$(npm view "$PKG" version 2>/dev/null || true)
|
||||||
|
|
||||||
|
if [ -z "$installed" ] || { [ -n "$latest" ] && [ "$installed" != "$latest" ]; }; then
|
||||||
|
echo "[entrypoint] pi install npm:$PKG (installed: ${installed:-none}, latest: ${latest:-unknown})"
|
||||||
mkdir -p "$PI_DIR"
|
mkdir -p "$PI_DIR"
|
||||||
# Wipe any previous (possibly-patched) install so pi restores the canonical package.
|
rm -rf "$PKG_DIR"
|
||||||
rm -rf "$PI_DIR/npm/node_modules/pi-claude-code-provider"
|
if ! /app/node_modules/.bin/pi install "npm:$PKG${latest:+@$latest}"; then
|
||||||
if /app/node_modules/.bin/pi install npm:pi-claude-code-provider; then
|
|
||||||
touch "$MARKER"
|
|
||||||
else
|
|
||||||
echo "[entrypoint] pi install failed; will retry on next start" >&2
|
echo "[entrypoint] pi install failed; will retry on next start" >&2
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
echo "[entrypoint] pi-claude-code-provider already installed"
|
echo "[entrypoint] $PKG $installed is up to date"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ $# -gt 0 ]; then
|
if [ $# -gt 0 ]; then
|
||||||
|
|||||||
1201
package-lock.json
generated
1201
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -13,7 +13,7 @@
|
|||||||
"test:watch": "vitest"
|
"test:watch": "vitest"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@earendil-works/pi-coding-agent": "^0.85.1",
|
"@earendil-works/pi-coding-agent": "*",
|
||||||
"@grammyjs/auto-retry": "^2.0.2",
|
"@grammyjs/auto-retry": "^2.0.2",
|
||||||
"@modelcontextprotocol/sdk": "^1.30.0",
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
||||||
"@playwright/mcp": "^0.0.80",
|
"@playwright/mcp": "^0.0.80",
|
||||||
|
|||||||
@@ -72,25 +72,12 @@ describe("loadConfig", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("accepts Google client_id + client_secret without a refresh token", () => {
|
it("accepts Google client_id + client_secret", () => {
|
||||||
const cfg = loadConfig(
|
const cfg = loadConfig(
|
||||||
{ ...base, GOOGLE_CLIENT_ID: "cid", GOOGLE_CLIENT_SECRET: "csecret" },
|
{ ...base, GOOGLE_CLIENT_ID: "cid", GOOGLE_CLIENT_SECRET: "csecret" },
|
||||||
"/p",
|
"/p",
|
||||||
);
|
);
|
||||||
expect(cfg.google).toEqual({ clientId: "cid", clientSecret: "csecret", refreshToken: undefined });
|
expect(cfg.google).toEqual({ clientId: "cid", clientSecret: "csecret" });
|
||||||
});
|
|
||||||
|
|
||||||
it("accepts Google client_id + client_secret + refresh_token together", () => {
|
|
||||||
const cfg = loadConfig(
|
|
||||||
{
|
|
||||||
...base,
|
|
||||||
GOOGLE_CLIENT_ID: "cid",
|
|
||||||
GOOGLE_CLIENT_SECRET: "csecret",
|
|
||||||
GOOGLE_REFRESH_TOKEN: "rt",
|
|
||||||
},
|
|
||||||
"/p",
|
|
||||||
);
|
|
||||||
expect(cfg.google).toEqual({ clientId: "cid", clientSecret: "csecret", refreshToken: "rt" });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("rejects Google config that has client_id without client_secret", () => {
|
it("rejects Google config that has client_id without client_secret", () => {
|
||||||
@@ -105,12 +92,6 @@ describe("loadConfig", () => {
|
|||||||
).toThrow(/GOOGLE_CLIENT_ID.*GOOGLE_CLIENT_SECRET/);
|
).toThrow(/GOOGLE_CLIENT_ID.*GOOGLE_CLIENT_SECRET/);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("rejects a lone GOOGLE_REFRESH_TOKEN without client_id/secret", () => {
|
|
||||||
expect(() =>
|
|
||||||
loadConfig({ ...base, GOOGLE_REFRESH_TOKEN: "rt" }, "/p"),
|
|
||||||
).toThrow(/GOOGLE_CLIENT_ID.*GOOGLE_CLIENT_SECRET/);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("leaves google undefined when no GOOGLE_* vars are set", () => {
|
it("leaves google undefined when no GOOGLE_* vars are set", () => {
|
||||||
expect(loadConfig(base, "/p").google).toBeUndefined();
|
expect(loadConfig(base, "/p").google).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ export interface Config {
|
|||||||
export interface GoogleConfig {
|
export interface GoogleConfig {
|
||||||
clientId: string;
|
clientId: string;
|
||||||
clientSecret: string;
|
clientSecret: string;
|
||||||
refreshToken?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PlaywrightConfig {
|
export interface PlaywrightConfig {
|
||||||
@@ -65,17 +64,16 @@ export function loadConfig(env: NodeJS.ProcessEnv = process.env, cwd: string = p
|
|||||||
|
|
||||||
const googleClientId = blankToUndefined(env.GOOGLE_CLIENT_ID);
|
const googleClientId = blankToUndefined(env.GOOGLE_CLIENT_ID);
|
||||||
const googleClientSecret = blankToUndefined(env.GOOGLE_CLIENT_SECRET);
|
const googleClientSecret = blankToUndefined(env.GOOGLE_CLIENT_SECRET);
|
||||||
const googleRefreshToken = blankToUndefined(env.GOOGLE_REFRESH_TOKEN);
|
|
||||||
const hasClientId = Boolean(googleClientId);
|
const hasClientId = Boolean(googleClientId);
|
||||||
const hasClientSecret = Boolean(googleClientSecret);
|
const hasClientSecret = Boolean(googleClientSecret);
|
||||||
if (hasClientId !== hasClientSecret || (!hasClientId && googleRefreshToken)) {
|
if (hasClientId !== hasClientSecret) {
|
||||||
throw new ConfigError(
|
throw new ConfigError(
|
||||||
"Google Calendar needs both GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET (refresh token is optional; if set at startup it will be migrated into the workspace store).",
|
"Google Calendar needs both GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const google: GoogleConfig | undefined =
|
const google: GoogleConfig | undefined =
|
||||||
googleClientId && googleClientSecret
|
googleClientId && googleClientSecret
|
||||||
? { clientId: googleClientId, clientSecret: googleClientSecret, refreshToken: googleRefreshToken }
|
? { clientId: googleClientId, clientSecret: googleClientSecret }
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const playwright: PlaywrightConfig | undefined = parseBool(env.PLAYWRIGHT_ENABLED)
|
const playwright: PlaywrightConfig | undefined = parseBool(env.PLAYWRIGHT_ENABLED)
|
||||||
|
|||||||
@@ -91,13 +91,6 @@ async function main(): Promise<void> {
|
|||||||
let tokenStore: GoogleTokenStore | null = null;
|
let tokenStore: GoogleTokenStore | null = null;
|
||||||
if (config.google) {
|
if (config.google) {
|
||||||
tokenStore = createGoogleTokenStore(config.workspaceDir);
|
tokenStore = createGoogleTokenStore(config.workspaceDir);
|
||||||
if (config.google.refreshToken && tokenStore.read() === null) {
|
|
||||||
tokenStore.write(config.google.refreshToken);
|
|
||||||
logger.warn(
|
|
||||||
"GOOGLE_REFRESH_TOKEN env var seeded into workspace/.secretary/google-token.json. " +
|
|
||||||
"You can remove GOOGLE_REFRESH_TOKEN from .env now — future updates come through /connect_calendar.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
customTools.push(createCalendarTool({
|
customTools.push(createCalendarTool({
|
||||||
clientId: config.google.clientId,
|
clientId: config.google.clientId,
|
||||||
clientSecret: config.google.clientSecret,
|
clientSecret: config.google.clientSecret,
|
||||||
|
|||||||
@@ -299,7 +299,7 @@ describe("registerBridge", () => {
|
|||||||
const h = harness();
|
const h = harness();
|
||||||
await h.bot.handleUpdate(textUpdate("/usr/local/bin is on my path", OWNER, "private", false));
|
await h.bot.handleUpdate(textUpdate("/usr/local/bin is on my path", OWNER, "private", false));
|
||||||
expect(h.texts()).toEqual([]);
|
expect(h.texts()).toEqual([]);
|
||||||
expect(h.host.sent).toEqual([{ text: "/usr/local/bin is on my path", images: undefined }]);
|
await vi.waitFor(() => expect(h.host.sent).toEqual([{ text: "/usr/local/bin is on my path", images: undefined }]));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("tells the owner it cannot handle unsupported media", async () => {
|
it("tells the owner it cannot handle unsupported media", async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user