Add bundled pi default and session token reporting
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -4,3 +4,5 @@ dist
|
||||
pnpm-lock.yaml
|
||||
coverage
|
||||
.pnpm-store
|
||||
.DS_Store
|
||||
src/.DS_Store
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# Agent Integration 🤖
|
||||
|
||||
CLAWDIS now ships with a single coding agent: Pi (the Tau CLI). Legacy Claude/Codex/Gemini/Opencode paths have been removed.
|
||||
Pi is bundled as a dependency of `clawdis`, so a fresh `pnpm install` gives you the `pi`/`tau` binaries automatically.
|
||||
|
||||
## Pi / Tau
|
||||
|
||||
@@ -12,7 +13,8 @@ The recommended (and only) agent for CLAWDIS. Built by Mario Zechner, forked wit
|
||||
"mode": "command",
|
||||
"agent": {
|
||||
"kind": "pi",
|
||||
"format": "json"
|
||||
"format": "json",
|
||||
"model": "claude-opus-4-5" // default if omitted
|
||||
},
|
||||
"command": [
|
||||
"node",
|
||||
@@ -44,6 +46,8 @@ RPC mode is enforced by CLAWDIS (we rewrite `--mode` to `rpc` for Pi invocations
|
||||
- 📊 Token usage tracking
|
||||
- 🔄 Streaming responses
|
||||
|
||||
If the agent does not report a model, CLAWDIS assumes `claude-opus-4-5` with ~200k context tokens (pi-ai defaults) for usage summaries.
|
||||
|
||||
## Session Management
|
||||
|
||||
### Per-Sender Sessions
|
||||
|
||||
@@ -43,7 +43,9 @@ CLAWDIS uses a JSON configuration file at `~/.clawdis/clawdis.json`.
|
||||
"mode": "command",
|
||||
"agent": {
|
||||
"kind": "pi",
|
||||
"format": "json"
|
||||
"format": "json",
|
||||
"model": "claude-opus-4-5",
|
||||
"contextTokens": 200000
|
||||
},
|
||||
"cwd": "/Users/you/clawd",
|
||||
"command": [
|
||||
@@ -99,6 +101,11 @@ Array of E.164 phone numbers allowed to trigger the AI. Use `["*"]` to allow eve
|
||||
| `timeoutSeconds` | number | Max time for agent to respond |
|
||||
| `heartbeatMinutes` | number | Interval for heartbeat pings |
|
||||
| `heartbeatBody` | string | Message sent on heartbeat |
|
||||
| `agent.kind` | string | Only `"pi"` is supported |
|
||||
| `agent.model` | string | Optional model name to annotate sessions (defaults to `claude-opus-4-5`) |
|
||||
| `agent.contextTokens` | number | Optional context window size; used for session token % reporting (defaults to ~200,000 for Opus 4.5) |
|
||||
|
||||
> Quick start: If you omit `inbound.reply`, CLAWDIS falls back to the bundled `@mariozechner/pi-coding-agent` with `--mode rpc`, per-sender sessions, and a 200k-token window. No extra install or config needed to get a reply.
|
||||
|
||||
### Template Variables
|
||||
|
||||
|
||||
@@ -62,6 +62,7 @@ clawdis status
|
||||
- [Direct Agent CLI](./agent-send.md) — Use `warelay agent` without sending WhatsApp messages
|
||||
- [Group Chats](./groups.md) — Mention patterns and filtering
|
||||
- [Media Handling](./media.md) — Images, voice, documents
|
||||
- [Session Management](./session.md) — How conversations are keyed and reset
|
||||
- [Security](./security.md) — Keeping your lobster safe
|
||||
- [Troubleshooting](./troubleshooting.md) — When the CLAWDIS misbehaves
|
||||
|
||||
|
||||
55
docs/session.md
Normal file
55
docs/session.md
Normal file
@@ -0,0 +1,55 @@
|
||||
# Session Management
|
||||
|
||||
CLAWDIS keeps lightweight session state so your agent can remember context between messages. Sessions are stored in a small JSON file and expire automatically after idle time or when you reset them.
|
||||
|
||||
## Where sessions live
|
||||
|
||||
- Default path: `~/.clawdis/sessions.json` (legacy: `~/.warelay/sessions.json`).
|
||||
- Override with `inbound.reply.session.store` in your config if you want a custom location.
|
||||
- The file is a plain map of `sessionKey -> { sessionId, updatedAt, ... }`; it is safe to delete if you want a full reset.
|
||||
|
||||
## How session keys are chosen
|
||||
|
||||
- Direct chats: normalized E.164 sender number (e.g., `+15551234567`).
|
||||
- Group chats: `group:<whatsapp-jid>` so group history stays isolated from DMs.
|
||||
- Global mode: set `inbound.reply.session.scope = "global"` to force a single shared session for all chats.
|
||||
- Unknown senders fall back to `unknown`.
|
||||
|
||||
## When sessions reset
|
||||
|
||||
- Idle timeout: `inbound.reply.session.idleMinutes` (default 60). If no messages arrive within this window, a new `sessionId` is created on the next message.
|
||||
- Reset triggers: `inbound.reply.session.resetTriggers` (default `['/new']`). Sending exactly `/new` or `/new <text>` starts a fresh session and passes the remaining text to the agent.
|
||||
- Manual nuke: delete the store file or remove specific keys with `jq`/your editor; a new file is created on the next message.
|
||||
|
||||
## Configuration recap
|
||||
|
||||
```json5
|
||||
// ~/.clawdis/clawdis.json
|
||||
{
|
||||
inbound: {
|
||||
reply: {
|
||||
session: {
|
||||
scope: "per-sender", // or "global"
|
||||
resetTriggers: ["/new"], // additional triggers allowed
|
||||
idleMinutes: 120, // extend or shrink timeout (min 1)
|
||||
store: "~/state/clawdis-sessions.json" // optional custom path
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Other session-related behaviors:
|
||||
- `thinkingLevel` and `verboseLevel` persist per session so inline directives stick until the session resets.
|
||||
- Heartbeats reuse the existing session for a recipient when available (good for keeping context warm).
|
||||
|
||||
## Inspecting sessions
|
||||
|
||||
- `clawdis status` shows the session store path, total count, and the five most recent keys with ages.
|
||||
- `clawdis sessions` lists every session (filter with `--active <minutes>` or use `--json` for scripts). It also reports token usage per session; set `inbound.reply.agent.contextTokens` to see the budget percentage (defaults to ~200k tokens for Opus 4.5 via pi-ai defaults).
|
||||
- For a deeper look, open the JSON store directly; the keys match the rules above.
|
||||
|
||||
## Tips
|
||||
|
||||
- Keep groups isolated: mention-based triggers plus the `group:<jid>` session key prevent group traffic from contaminating your DM history.
|
||||
- If you automate cleanup, prefer deleting specific keys instead of the whole file to keep other conversations intact.
|
||||
@@ -35,6 +35,7 @@
|
||||
"packageManager": "pnpm@10.23.0",
|
||||
"dependencies": {
|
||||
"@whiskeysockets/baileys": "7.0.0-rc.9",
|
||||
"@mariozechner/pi-coding-agent": "^0.12.4",
|
||||
"body-parser": "^2.2.1",
|
||||
"chalk": "^5.6.2",
|
||||
"commander": "^14.0.2",
|
||||
|
||||
34
src/agents/context.ts
Normal file
34
src/agents/context.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
// Lazy-load pi-ai model metadata so we can infer context windows when the agent
|
||||
// reports a model id. pi-coding-agent depends on @mariozechner/pi-ai, so it
|
||||
// should be present whenever CLAWDIS is installed from npm.
|
||||
|
||||
type ModelEntry = { id: string; contextWindow?: number };
|
||||
|
||||
const MODEL_CACHE = new Map<string, number>();
|
||||
const loadPromise = (async () => {
|
||||
try {
|
||||
const piAi = (await import("@mariozechner/pi-ai")) as {
|
||||
getProviders: () => string[];
|
||||
getModels: (provider: string) => ModelEntry[];
|
||||
};
|
||||
const providers = piAi.getProviders();
|
||||
for (const p of providers) {
|
||||
const models = piAi.getModels(p) as ModelEntry[];
|
||||
for (const m of models) {
|
||||
if (!m?.id) continue;
|
||||
if (typeof m.contextWindow === "number" && m.contextWindow > 0) {
|
||||
MODEL_CACHE.set(m.id, m.contextWindow);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// If pi-ai isn't available, leave cache empty; lookup will fall back.
|
||||
}
|
||||
})();
|
||||
|
||||
export function lookupContextTokens(modelId?: string): number | undefined {
|
||||
if (!modelId) return undefined;
|
||||
// Best-effort: kick off loading, but don't block.
|
||||
void loadPromise;
|
||||
return MODEL_CACHE.get(modelId);
|
||||
}
|
||||
5
src/agents/defaults.ts
Normal file
5
src/agents/defaults.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
// Defaults for agent metadata when upstream does not supply them.
|
||||
// Model id uses pi-ai's built-in Anthropic catalog.
|
||||
export const DEFAULT_MODEL = "claude-opus-4-5";
|
||||
// Context window: Opus 4.5 supports ~200k tokens (per pi-ai models.generated.ts).
|
||||
export const DEFAULT_CONTEXT_TOKENS = 200_000;
|
||||
26
src/agents/pi-path.ts
Normal file
26
src/agents/pi-path.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import fs from "node:fs";
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
|
||||
// Resolve the bundled pi/tau binary path from the installed dependency.
|
||||
export function resolveBundledPiBinary(): string | null {
|
||||
try {
|
||||
const require = createRequire(import.meta.url);
|
||||
const pkgPath = require.resolve(
|
||||
"@mariozechner/pi-coding-agent/package.json",
|
||||
);
|
||||
const pkgDir = path.dirname(pkgPath);
|
||||
// Prefer compiled binary if present, else fall back to dist/cli.js (has shebang).
|
||||
const binCandidates = [
|
||||
path.join(pkgDir, "dist", "pi"),
|
||||
path.join(pkgDir, "dist", "cli.js"),
|
||||
path.join(pkgDir, "bin", "tau-dev.mjs"),
|
||||
];
|
||||
for (const candidate of binCandidates) {
|
||||
if (fs.existsSync(candidate)) return candidate;
|
||||
}
|
||||
} catch {
|
||||
// Dependency missing or resolution failed.
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -1,4 +1,7 @@
|
||||
import crypto from "node:crypto";
|
||||
import { lookupContextTokens } from "../agents/context.js";
|
||||
import { DEFAULT_CONTEXT_TOKENS, DEFAULT_MODEL } from "../agents/defaults.js";
|
||||
import { resolveBundledPiBinary } from "../agents/pi-path.js";
|
||||
import { loadConfig, type WarelayConfig } from "../config/config.js";
|
||||
import {
|
||||
DEFAULT_IDLE_MINUTES,
|
||||
@@ -121,6 +124,28 @@ function stripMentions(
|
||||
return result.replace(/\s+/g, " ").trim();
|
||||
}
|
||||
|
||||
function makeDefaultPiReply() {
|
||||
const piBin = resolveBundledPiBinary() ?? "pi";
|
||||
const defaultContext =
|
||||
lookupContextTokens(DEFAULT_MODEL) ?? DEFAULT_CONTEXT_TOKENS;
|
||||
return {
|
||||
mode: "command" as const,
|
||||
command: [piBin, "--mode", "rpc", "{{BodyStripped}}"],
|
||||
agent: {
|
||||
kind: "pi" as const,
|
||||
model: DEFAULT_MODEL,
|
||||
contextTokens: defaultContext,
|
||||
format: "json" as const,
|
||||
},
|
||||
session: {
|
||||
scope: "per-sender" as const,
|
||||
resetTriggers: [DEFAULT_RESET_TRIGGER],
|
||||
idleMinutes: DEFAULT_IDLE_MINUTES,
|
||||
},
|
||||
timeoutSeconds: 600,
|
||||
};
|
||||
}
|
||||
|
||||
export async function getReplyFromConfig(
|
||||
ctx: MsgContext,
|
||||
opts?: GetReplyOptions,
|
||||
@@ -129,7 +154,7 @@ export async function getReplyFromConfig(
|
||||
): Promise<ReplyPayload | ReplyPayload[] | undefined> {
|
||||
// Choose reply from config: static text or external command stdout.
|
||||
const cfg = configOverride ?? loadConfig();
|
||||
const reply = cfg.inbound?.reply;
|
||||
const reply = cfg.inbound?.reply ?? makeDefaultPiReply();
|
||||
const timeoutSeconds = Math.max(reply?.timeoutSeconds ?? 600, 1);
|
||||
const timeoutMs = timeoutSeconds * 1000;
|
||||
let started = false;
|
||||
@@ -718,6 +743,49 @@ export async function getReplyFromConfig(
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const usage = meta.agentMeta?.usage;
|
||||
const model =
|
||||
meta.agentMeta?.model ||
|
||||
reply?.agent?.model ||
|
||||
sessionEntry?.model ||
|
||||
DEFAULT_MODEL;
|
||||
const contextTokens =
|
||||
reply?.agent?.contextTokens ??
|
||||
lookupContextTokens(model) ??
|
||||
sessionEntry?.contextTokens ??
|
||||
DEFAULT_CONTEXT_TOKENS;
|
||||
|
||||
if (usage) {
|
||||
const entry = sessionEntry ?? sessionStore[sessionKey];
|
||||
if (entry) {
|
||||
const input = usage.input ?? 0;
|
||||
const output = usage.output ?? 0;
|
||||
const total = usage.total ?? input + output;
|
||||
sessionEntry = {
|
||||
...entry,
|
||||
inputTokens: (entry.inputTokens ?? 0) + input,
|
||||
outputTokens: (entry.outputTokens ?? 0) + output,
|
||||
totalTokens: (entry.totalTokens ?? 0) + total,
|
||||
model,
|
||||
contextTokens: contextTokens ?? entry.contextTokens,
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
sessionStore[sessionKey] = sessionEntry;
|
||||
await saveSessionStore(storePath, sessionStore);
|
||||
}
|
||||
} else if (model || contextTokens) {
|
||||
const entry = sessionEntry ?? sessionStore[sessionKey];
|
||||
if (entry) {
|
||||
sessionEntry = {
|
||||
...entry,
|
||||
model: model ?? entry.model,
|
||||
contextTokens: contextTokens ?? entry.contextTokens,
|
||||
};
|
||||
sessionStore[sessionKey] = sessionEntry;
|
||||
await saveSessionStore(storePath, sessionStore);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (meta.agentMeta && isVerbose()) {
|
||||
logVerbose(`Agent meta: ${JSON.stringify(meta.agentMeta)}`);
|
||||
|
||||
@@ -2,6 +2,7 @@ import chalk from "chalk";
|
||||
import { Command } from "commander";
|
||||
import { agentCommand } from "../commands/agent.js";
|
||||
import { sendCommand } from "../commands/send.js";
|
||||
import { sessionsCommand } from "../commands/sessions.js";
|
||||
import { statusCommand } from "../commands/status.js";
|
||||
import { loadConfig } from "../config/config.js";
|
||||
import { danger, info, setVerbose } from "../globals.js";
|
||||
@@ -490,6 +491,42 @@ Examples:
|
||||
}
|
||||
});
|
||||
|
||||
program
|
||||
.command("sessions")
|
||||
.description("List stored conversation sessions")
|
||||
.option("--json", "Output as JSON", false)
|
||||
.option("--verbose", "Verbose logging", false)
|
||||
.option(
|
||||
"--store <path>",
|
||||
"Path to session store (default: resolved from config)",
|
||||
)
|
||||
.option(
|
||||
"--active <minutes>",
|
||||
"Only show sessions updated within the past N minutes",
|
||||
)
|
||||
.addHelpText(
|
||||
"after",
|
||||
`
|
||||
Examples:
|
||||
clawdis sessions # list all sessions
|
||||
clawdis sessions --active 120 # only last 2 hours
|
||||
clawdis sessions --json # machine-readable output
|
||||
clawdis sessions --store ./tmp/sessions.json
|
||||
|
||||
Shows token usage per session when the agent reports it; set inbound.reply.agent.contextTokens to see % of your model window.`,
|
||||
)
|
||||
.action(async (opts) => {
|
||||
setVerbose(Boolean(opts.verbose));
|
||||
await sessionsCommand(
|
||||
{
|
||||
json: Boolean(opts.json),
|
||||
store: opts.store as string | undefined,
|
||||
active: opts.active as string | undefined,
|
||||
},
|
||||
defaultRuntime,
|
||||
);
|
||||
});
|
||||
|
||||
program
|
||||
.command("relay:tmux")
|
||||
.description(
|
||||
|
||||
167
src/commands/sessions.ts
Normal file
167
src/commands/sessions.ts
Normal file
@@ -0,0 +1,167 @@
|
||||
import { lookupContextTokens } from "../agents/context.js";
|
||||
import { DEFAULT_CONTEXT_TOKENS, DEFAULT_MODEL } from "../agents/defaults.js";
|
||||
import { loadConfig } from "../config/config.js";
|
||||
import {
|
||||
loadSessionStore,
|
||||
resolveStorePath,
|
||||
type SessionEntry,
|
||||
} from "../config/sessions.js";
|
||||
import { info } from "../globals.js";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
|
||||
type SessionRow = {
|
||||
key: string;
|
||||
kind: "direct" | "group" | "global" | "unknown";
|
||||
updatedAt: number | null;
|
||||
ageMs: number | null;
|
||||
sessionId?: string;
|
||||
systemSent?: boolean;
|
||||
abortedLastRun?: boolean;
|
||||
thinkingLevel?: string;
|
||||
verboseLevel?: string;
|
||||
inputTokens?: number;
|
||||
outputTokens?: number;
|
||||
totalTokens?: number;
|
||||
model?: string;
|
||||
contextTokens?: number;
|
||||
};
|
||||
|
||||
const formatAge = (ms: number | null | undefined) => {
|
||||
if (!ms || ms < 0) return "unknown";
|
||||
const minutes = Math.round(ms / 60_000);
|
||||
if (minutes < 1) return "just now";
|
||||
if (minutes < 60) return `${minutes}m ago`;
|
||||
const hours = Math.round(minutes / 60);
|
||||
if (hours < 48) return `${hours}h ago`;
|
||||
const days = Math.round(hours / 24);
|
||||
return `${days}d ago`;
|
||||
};
|
||||
|
||||
function classifyKey(key: string): SessionRow["kind"] {
|
||||
if (key === "global") return "global";
|
||||
if (key.startsWith("group:")) return "group";
|
||||
if (key === "unknown") return "unknown";
|
||||
return "direct";
|
||||
}
|
||||
|
||||
function toRows(store: Record<string, SessionEntry>): SessionRow[] {
|
||||
return Object.entries(store)
|
||||
.map(([key, entry]) => {
|
||||
const updatedAt = entry?.updatedAt ?? null;
|
||||
return {
|
||||
key,
|
||||
kind: classifyKey(key),
|
||||
updatedAt,
|
||||
ageMs: updatedAt ? Date.now() - updatedAt : null,
|
||||
sessionId: entry?.sessionId,
|
||||
systemSent: entry?.systemSent,
|
||||
abortedLastRun: entry?.abortedLastRun,
|
||||
thinkingLevel: entry?.thinkingLevel,
|
||||
verboseLevel: entry?.verboseLevel,
|
||||
inputTokens: entry?.inputTokens,
|
||||
outputTokens: entry?.outputTokens,
|
||||
totalTokens: entry?.totalTokens,
|
||||
model: entry?.model,
|
||||
contextTokens: entry?.contextTokens,
|
||||
} satisfies SessionRow;
|
||||
})
|
||||
.sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0));
|
||||
}
|
||||
|
||||
export async function sessionsCommand(
|
||||
opts: { json?: boolean; store?: string; active?: string },
|
||||
runtime: RuntimeEnv,
|
||||
) {
|
||||
const cfg = loadConfig();
|
||||
const configContextTokens =
|
||||
cfg.inbound?.reply?.agent?.contextTokens ??
|
||||
lookupContextTokens(cfg.inbound?.reply?.agent?.model) ??
|
||||
DEFAULT_CONTEXT_TOKENS;
|
||||
const configModel = cfg.inbound?.reply?.agent?.model ?? DEFAULT_MODEL;
|
||||
const storePath = resolveStorePath(
|
||||
opts.store ?? cfg.inbound?.reply?.session?.store,
|
||||
);
|
||||
const store = loadSessionStore(storePath);
|
||||
|
||||
const activeMinutes = opts.active
|
||||
? Number.parseInt(String(opts.active), 10)
|
||||
: undefined;
|
||||
if (
|
||||
opts.active !== undefined &&
|
||||
(Number.isNaN(activeMinutes) || activeMinutes <= 0)
|
||||
) {
|
||||
runtime.error("--active must be a positive integer (minutes)");
|
||||
runtime.exit(1);
|
||||
return;
|
||||
}
|
||||
|
||||
const rows = toRows(store).filter((row) => {
|
||||
if (!activeMinutes) return true;
|
||||
if (!row.updatedAt) return false;
|
||||
return Date.now() - row.updatedAt <= activeMinutes * 60_000;
|
||||
});
|
||||
|
||||
if (opts.json) {
|
||||
runtime.log(
|
||||
JSON.stringify(
|
||||
{
|
||||
path: storePath,
|
||||
count: rows.length,
|
||||
activeMinutes: activeMinutes ?? null,
|
||||
sessions: rows.map((r) => ({
|
||||
...r,
|
||||
contextTokens:
|
||||
r.contextTokens ??
|
||||
lookupContextTokens(r.model) ??
|
||||
configContextTokens ??
|
||||
null,
|
||||
model: r.model ?? configModel ?? null,
|
||||
})),
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
runtime.log(info(`Session store: ${storePath}`));
|
||||
runtime.log(info(`Sessions listed: ${rows.length}`));
|
||||
if (activeMinutes) {
|
||||
runtime.log(info(`Filtered to last ${activeMinutes} minute(s)`));
|
||||
}
|
||||
if (rows.length === 0) {
|
||||
runtime.log("No sessions found.");
|
||||
return;
|
||||
}
|
||||
|
||||
for (const row of rows) {
|
||||
const model = row.model ?? configModel;
|
||||
const contextTokens =
|
||||
row.contextTokens ?? lookupContextTokens(model) ?? configContextTokens;
|
||||
const input = row.inputTokens ?? 0;
|
||||
const output = row.outputTokens ?? 0;
|
||||
const total = row.totalTokens ?? input + output;
|
||||
const pct = contextTokens
|
||||
? `${Math.min(100, Math.round((total / contextTokens) * 100))}%`
|
||||
: null;
|
||||
|
||||
const parts = [
|
||||
`${row.key} [${row.kind}]`,
|
||||
row.updatedAt ? formatAge(Date.now() - row.updatedAt) : "age unknown",
|
||||
];
|
||||
if (row.sessionId) parts.push(`id ${row.sessionId}`);
|
||||
if (row.thinkingLevel) parts.push(`think=${row.thinkingLevel}`);
|
||||
if (row.verboseLevel) parts.push(`verbose=${row.verboseLevel}`);
|
||||
if (row.systemSent) parts.push("systemSent");
|
||||
if (row.abortedLastRun) parts.push("aborted");
|
||||
if (total > 0) {
|
||||
const tokenStr = `tokens in:${input} out:${output} total:${total}`;
|
||||
parts.push(
|
||||
contextTokens ? `${tokenStr} (${pct} of ${contextTokens})` : tokenStr,
|
||||
);
|
||||
}
|
||||
if (model) parts.push(`model=${model}`);
|
||||
runtime.log(`- ${parts.join(" | ")}`);
|
||||
}
|
||||
}
|
||||
@@ -82,6 +82,8 @@ export type WarelayConfig = {
|
||||
kind: AgentKind;
|
||||
format?: "text" | "json";
|
||||
identityPrefix?: string;
|
||||
model?: string;
|
||||
contextTokens?: number;
|
||||
};
|
||||
};
|
||||
};
|
||||
@@ -141,6 +143,8 @@ const ReplySchema = z
|
||||
kind: z.literal("pi"),
|
||||
format: z.union([z.literal("text"), z.literal("json")]).optional(),
|
||||
identityPrefix: z.string().optional(),
|
||||
model: z.string().optional(),
|
||||
contextTokens: z.number().int().positive().optional(),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
|
||||
@@ -15,6 +15,11 @@ export type SessionEntry = {
|
||||
abortedLastRun?: boolean;
|
||||
thinkingLevel?: string;
|
||||
verboseLevel?: string;
|
||||
inputTokens?: number;
|
||||
outputTokens?: number;
|
||||
totalTokens?: number;
|
||||
model?: string;
|
||||
contextTokens?: number;
|
||||
};
|
||||
|
||||
export const SESSION_STORE_DEFAULT = path.join(
|
||||
|
||||
@@ -148,6 +148,11 @@ export async function ensureFunnel(
|
||||
runtime.error(
|
||||
"Failed to enable Tailscale Funnel. Is it allowed on your tailnet?",
|
||||
);
|
||||
runtime.error(
|
||||
info(
|
||||
"Tip: Funnel is optional for CLAWDIS. You can keep running the web relay without it: `pnpm clawdis relay`",
|
||||
),
|
||||
);
|
||||
if (isVerbose()) {
|
||||
if (stdout.trim()) runtime.error(chalk.gray(`stdout: ${stdout.trim()}`));
|
||||
if (stderr.trim()) runtime.error(chalk.gray(`stderr: ${stderr.trim()}`));
|
||||
|
||||
Reference in New Issue
Block a user