mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-14 01:50:39 +09:00
Merge remote-tracking branch 'public-gh/master' into paperclip-routines
* public-gh/master: (46 commits) chore(lockfile): refresh pnpm-lock.yaml (#1377) fix: manage codex home per company by default Ensure agent home directories exist before use Handle directory entries in imported zip archives Fix portability import and org chart test blockers Fix PR verify failures after merge fix: address greptile follow-up feedback Address remaining Greptile portability feedback docs: clarify quickstart npx usage Add guarded dev restart handling Fix PAP-576 settings toggles and transcript default Add username log censor setting fix: use standard toggle component for permission controls fix: add missing setPrincipalPermission mock in portability tests fix: use fixed 1280x640 dimensions for org chart export image Adjust default CEO onboarding task copy fix: link Agent Company to agentcompanies.io in export README fix: strip agents and projects sections from COMPANY.md export body fix: default company export page to README.md instead of first file Add default agent instructions bundle ... # Conflicts: # packages/adapters/pi-local/src/server/execute.ts # packages/db/src/migrations/meta/0039_snapshot.json # packages/db/src/migrations/meta/_journal.json # server/src/__tests__/agent-permissions-routes.test.ts # server/src/__tests__/agent-skills-routes.test.ts # server/src/services/company-portability.ts # skills/paperclip/references/company-skills.md # ui/src/api/agents.ts
This commit is contained in:
commit
e3c92a20f1
96 changed files with 15366 additions and 1684 deletions
|
|
@ -1,19 +1,29 @@
|
|||
import type { TranscriptEntry } from "./types.js";
|
||||
|
||||
export const REDACTED_HOME_PATH_USER = "[]";
|
||||
export const REDACTED_HOME_PATH_USER = "*";
|
||||
|
||||
export interface HomePathRedactionOptions {
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
function maskHomePathUserSegment(value: string) {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) return REDACTED_HOME_PATH_USER;
|
||||
return `${trimmed[0]}${"*".repeat(Math.max(1, Array.from(trimmed).length - 1))}`;
|
||||
}
|
||||
|
||||
const HOME_PATH_PATTERNS = [
|
||||
{
|
||||
regex: /\/Users\/[^/\\\s]+/g,
|
||||
replace: `/Users/${REDACTED_HOME_PATH_USER}`,
|
||||
regex: /\/Users\/([^/\\\s]+)/g,
|
||||
replace: (_match: string, user: string) => `/Users/${maskHomePathUserSegment(user)}`,
|
||||
},
|
||||
{
|
||||
regex: /\/home\/[^/\\\s]+/g,
|
||||
replace: `/home/${REDACTED_HOME_PATH_USER}`,
|
||||
regex: /\/home\/([^/\\\s]+)/g,
|
||||
replace: (_match: string, user: string) => `/home/${maskHomePathUserSegment(user)}`,
|
||||
},
|
||||
{
|
||||
regex: /([A-Za-z]:\\Users\\)[^\\/\s]+/g,
|
||||
replace: `$1${REDACTED_HOME_PATH_USER}`,
|
||||
regex: /([A-Za-z]:\\Users\\)([^\\/\s]+)/g,
|
||||
replace: (_match: string, prefix: string, user: string) => `${prefix}${maskHomePathUserSegment(user)}`,
|
||||
},
|
||||
] as const;
|
||||
|
||||
|
|
@ -23,7 +33,8 @@ function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|||
return proto === Object.prototype || proto === null;
|
||||
}
|
||||
|
||||
export function redactHomePathUserSegments(text: string): string {
|
||||
export function redactHomePathUserSegments(text: string, opts?: HomePathRedactionOptions): string {
|
||||
if (opts?.enabled === false) return text;
|
||||
let result = text;
|
||||
for (const pattern of HOME_PATH_PATTERNS) {
|
||||
result = result.replace(pattern.regex, pattern.replace);
|
||||
|
|
@ -31,12 +42,12 @@ export function redactHomePathUserSegments(text: string): string {
|
|||
return result;
|
||||
}
|
||||
|
||||
export function redactHomePathUserSegmentsInValue<T>(value: T): T {
|
||||
export function redactHomePathUserSegmentsInValue<T>(value: T, opts?: HomePathRedactionOptions): T {
|
||||
if (typeof value === "string") {
|
||||
return redactHomePathUserSegments(value) as T;
|
||||
return redactHomePathUserSegments(value, opts) as T;
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((entry) => redactHomePathUserSegmentsInValue(entry)) as T;
|
||||
return value.map((entry) => redactHomePathUserSegmentsInValue(entry, opts)) as T;
|
||||
}
|
||||
if (!isPlainObject(value)) {
|
||||
return value;
|
||||
|
|
@ -44,12 +55,12 @@ export function redactHomePathUserSegmentsInValue<T>(value: T): T {
|
|||
|
||||
const redacted: Record<string, unknown> = {};
|
||||
for (const [key, entry] of Object.entries(value)) {
|
||||
redacted[key] = redactHomePathUserSegmentsInValue(entry);
|
||||
redacted[key] = redactHomePathUserSegmentsInValue(entry, opts);
|
||||
}
|
||||
return redacted as T;
|
||||
}
|
||||
|
||||
export function redactTranscriptEntryPaths(entry: TranscriptEntry): TranscriptEntry {
|
||||
export function redactTranscriptEntryPaths(entry: TranscriptEntry, opts?: HomePathRedactionOptions): TranscriptEntry {
|
||||
switch (entry.kind) {
|
||||
case "assistant":
|
||||
case "thinking":
|
||||
|
|
@ -57,23 +68,27 @@ export function redactTranscriptEntryPaths(entry: TranscriptEntry): TranscriptEn
|
|||
case "stderr":
|
||||
case "system":
|
||||
case "stdout":
|
||||
return { ...entry, text: redactHomePathUserSegments(entry.text) };
|
||||
return { ...entry, text: redactHomePathUserSegments(entry.text, opts) };
|
||||
case "tool_call":
|
||||
return { ...entry, name: redactHomePathUserSegments(entry.name), input: redactHomePathUserSegmentsInValue(entry.input) };
|
||||
return {
|
||||
...entry,
|
||||
name: redactHomePathUserSegments(entry.name, opts),
|
||||
input: redactHomePathUserSegmentsInValue(entry.input, opts),
|
||||
};
|
||||
case "tool_result":
|
||||
return { ...entry, content: redactHomePathUserSegments(entry.content) };
|
||||
return { ...entry, content: redactHomePathUserSegments(entry.content, opts) };
|
||||
case "init":
|
||||
return {
|
||||
...entry,
|
||||
model: redactHomePathUserSegments(entry.model),
|
||||
sessionId: redactHomePathUserSegments(entry.sessionId),
|
||||
model: redactHomePathUserSegments(entry.model, opts),
|
||||
sessionId: redactHomePathUserSegments(entry.sessionId, opts),
|
||||
};
|
||||
case "result":
|
||||
return {
|
||||
...entry,
|
||||
text: redactHomePathUserSegments(entry.text),
|
||||
subtype: redactHomePathUserSegments(entry.subtype),
|
||||
errors: entry.errors.map((error) => redactHomePathUserSegments(error)),
|
||||
text: redactHomePathUserSegments(entry.text, opts),
|
||||
subtype: redactHomePathUserSegments(entry.subtype, opts),
|
||||
errors: entry.errors.map((error) => redactHomePathUserSegments(error, opts)),
|
||||
};
|
||||
default:
|
||||
return entry;
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ Operational fields:
|
|||
Notes:
|
||||
- Prompts are piped via stdin (Codex receives "-" prompt argument).
|
||||
- Paperclip injects desired local skills into the active workspace's ".agents/skills" directory at execution time so Codex can discover "$paperclip" and related skills without coupling them to the user's login home.
|
||||
- Unless explicitly overridden in adapter config, Paperclip runs Codex with a per-company managed CODEX_HOME under the active Paperclip instance and seeds auth/config from the shared Codex home (the CODEX_HOME env var, when set, or ~/.codex).
|
||||
- Some model/tool combinations reject certain effort levels (for example minimal with web search enabled).
|
||||
- When Paperclip realizes a workspace/runtime for a run, it injects PAPERCLIP_WORKSPACE_* and PAPERCLIP_RUNTIME_* env vars for agent-side tooling.
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import type { AdapterExecutionContext } from "@paperclipai/adapter-utils";
|
|||
const TRUTHY_ENV_RE = /^(1|true|yes|on)$/i;
|
||||
const COPIED_SHARED_FILES = ["config.json", "config.toml", "instructions.md"] as const;
|
||||
const SYMLINKED_SHARED_FILES = ["auth.json"] as const;
|
||||
const DEFAULT_PAPERCLIP_INSTANCE_ID = "default";
|
||||
|
||||
function nonEmpty(value: string | undefined): string | null {
|
||||
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
||||
|
|
@ -15,35 +16,26 @@ export async function pathExists(candidate: string): Promise<boolean> {
|
|||
return fs.access(candidate).then(() => true).catch(() => false);
|
||||
}
|
||||
|
||||
export function resolveCodexHomeDir(
|
||||
export function resolveSharedCodexHomeDir(
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
companyId?: string,
|
||||
): string {
|
||||
const fromEnv = nonEmpty(env.CODEX_HOME);
|
||||
const baseHome = fromEnv ? path.resolve(fromEnv) : path.join(os.homedir(), ".codex");
|
||||
return companyId ? path.join(baseHome, "companies", companyId) : baseHome;
|
||||
return fromEnv ? path.resolve(fromEnv) : path.join(os.homedir(), ".codex");
|
||||
}
|
||||
|
||||
function isWorktreeMode(env: NodeJS.ProcessEnv): boolean {
|
||||
return TRUTHY_ENV_RE.test(env.PAPERCLIP_IN_WORKTREE ?? "");
|
||||
}
|
||||
|
||||
function resolveWorktreeCodexHomeDir(
|
||||
export function resolveManagedCodexHomeDir(
|
||||
env: NodeJS.ProcessEnv,
|
||||
companyId?: string,
|
||||
): string | null {
|
||||
if (!isWorktreeMode(env)) return null;
|
||||
const paperclipHome = nonEmpty(env.PAPERCLIP_HOME);
|
||||
if (!paperclipHome) return null;
|
||||
const instanceId = nonEmpty(env.PAPERCLIP_INSTANCE_ID);
|
||||
if (instanceId) {
|
||||
return companyId
|
||||
? path.resolve(paperclipHome, "instances", instanceId, "companies", companyId, "codex-home")
|
||||
: path.resolve(paperclipHome, "instances", instanceId, "codex-home");
|
||||
}
|
||||
): string {
|
||||
const paperclipHome = nonEmpty(env.PAPERCLIP_HOME) ?? path.resolve(os.homedir(), ".paperclip");
|
||||
const instanceId = nonEmpty(env.PAPERCLIP_INSTANCE_ID) ?? DEFAULT_PAPERCLIP_INSTANCE_ID;
|
||||
return companyId
|
||||
? path.resolve(paperclipHome, "companies", companyId, "codex-home")
|
||||
: path.resolve(paperclipHome, "codex-home");
|
||||
? path.resolve(paperclipHome, "instances", instanceId, "companies", companyId, "codex-home")
|
||||
: path.resolve(paperclipHome, "instances", instanceId, "codex-home");
|
||||
}
|
||||
|
||||
async function ensureParentDir(target: string): Promise<void> {
|
||||
|
|
@ -79,15 +71,14 @@ async function ensureCopiedFile(target: string, source: string): Promise<void> {
|
|||
await fs.copyFile(source, target);
|
||||
}
|
||||
|
||||
export async function prepareWorktreeCodexHome(
|
||||
export async function prepareManagedCodexHome(
|
||||
env: NodeJS.ProcessEnv,
|
||||
onLog: AdapterExecutionContext["onLog"],
|
||||
companyId?: string,
|
||||
): Promise<string | null> {
|
||||
const targetHome = resolveWorktreeCodexHomeDir(env, companyId);
|
||||
if (!targetHome) return null;
|
||||
): Promise<string> {
|
||||
const targetHome = resolveManagedCodexHomeDir(env, companyId);
|
||||
|
||||
const sourceHome = resolveCodexHomeDir(env);
|
||||
const sourceHome = resolveSharedCodexHomeDir(env);
|
||||
if (path.resolve(sourceHome) === path.resolve(targetHome)) return targetHome;
|
||||
|
||||
await fs.mkdir(targetHome, { recursive: true });
|
||||
|
|
@ -106,7 +97,7 @@ export async function prepareWorktreeCodexHome(
|
|||
|
||||
await onLog(
|
||||
"stdout",
|
||||
`[paperclip] Using worktree-isolated Codex home "${targetHome}" (seeded from "${sourceHome}").\n`,
|
||||
`[paperclip] Using ${isWorktreeMode(env) ? "worktree-isolated" : "Paperclip-managed"} Codex home "${targetHome}" (seeded from "${sourceHome}").\n`,
|
||||
);
|
||||
return targetHome;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ import {
|
|||
runChildProcess,
|
||||
} from "@paperclipai/adapter-utils/server-utils";
|
||||
import { parseCodexJsonl, isCodexUnknownSessionError } from "./parse.js";
|
||||
import { pathExists, prepareWorktreeCodexHome, resolveCodexHomeDir } from "./codex-home.js";
|
||||
import { pathExists, prepareManagedCodexHome, resolveManagedCodexHomeDir } from "./codex-home.js";
|
||||
import { resolveCodexDesiredSkillNames } from "./skills.js";
|
||||
|
||||
const __moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
|
@ -268,10 +268,11 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||
const codexSkillEntries = await readPaperclipRuntimeSkillEntries(config, __moduleDir);
|
||||
const desiredSkillNames = resolveCodexDesiredSkillNames(config, codexSkillEntries);
|
||||
await ensureAbsoluteDirectory(cwd, { createIfMissing: true });
|
||||
const preparedWorktreeCodexHome =
|
||||
configuredCodexHome ? null : await prepareWorktreeCodexHome(process.env, onLog, agent.companyId);
|
||||
const defaultCodexHome = resolveCodexHomeDir(process.env, agent.companyId);
|
||||
const effectiveCodexHome = configuredCodexHome ?? preparedWorktreeCodexHome ?? defaultCodexHome;
|
||||
const preparedManagedCodexHome =
|
||||
configuredCodexHome ? null : await prepareManagedCodexHome(process.env, onLog, agent.companyId);
|
||||
const defaultCodexHome = resolveManagedCodexHomeDir(process.env, agent.companyId);
|
||||
const effectiveCodexHome = configuredCodexHome ?? preparedManagedCodexHome ?? defaultCodexHome;
|
||||
await fs.mkdir(effectiveCodexHome, { recursive: true });
|
||||
const codexWorkspaceSkillsDir = resolveCodexWorkspaceSkillsDir(cwd);
|
||||
await ensureCodexSkillsInjected(
|
||||
onLog,
|
||||
|
|
|
|||
|
|
@ -1,8 +1,4 @@
|
|||
import {
|
||||
redactHomePathUserSegments,
|
||||
redactHomePathUserSegmentsInValue,
|
||||
type TranscriptEntry,
|
||||
} from "@paperclipai/adapter-utils";
|
||||
import { type TranscriptEntry } from "@paperclipai/adapter-utils";
|
||||
|
||||
function safeJsonParse(text: string): unknown {
|
||||
try {
|
||||
|
|
@ -43,12 +39,12 @@ function errorText(value: unknown): string {
|
|||
}
|
||||
|
||||
function stringifyUnknown(value: unknown): string {
|
||||
if (typeof value === "string") return redactHomePathUserSegments(value);
|
||||
if (typeof value === "string") return value;
|
||||
if (value === null || value === undefined) return "";
|
||||
try {
|
||||
return JSON.stringify(redactHomePathUserSegmentsInValue(value), null, 2);
|
||||
return JSON.stringify(value, null, 2);
|
||||
} catch {
|
||||
return redactHomePathUserSegments(String(value));
|
||||
return String(value);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -61,8 +57,8 @@ function parseCommandExecutionItem(
|
|||
const command = asString(item.command);
|
||||
const status = asString(item.status);
|
||||
const exitCode = typeof item.exit_code === "number" && Number.isFinite(item.exit_code) ? item.exit_code : null;
|
||||
const safeCommand = redactHomePathUserSegments(command);
|
||||
const output = redactHomePathUserSegments(asString(item.aggregated_output)).replace(/\s+$/, "");
|
||||
const safeCommand = command;
|
||||
const output = asString(item.aggregated_output).replace(/\s+$/, "");
|
||||
|
||||
if (phase === "started") {
|
||||
return [{
|
||||
|
|
@ -109,7 +105,7 @@ function parseFileChangeItem(item: Record<string, unknown>, ts: string): Transcr
|
|||
.filter((change): change is Record<string, unknown> => Boolean(change))
|
||||
.map((change) => {
|
||||
const kind = asString(change.kind, "update");
|
||||
const path = redactHomePathUserSegments(asString(change.path, "unknown"));
|
||||
const path = asString(change.path, "unknown");
|
||||
return `${kind} ${path}`;
|
||||
});
|
||||
|
||||
|
|
@ -131,13 +127,13 @@ function parseCodexItem(
|
|||
|
||||
if (itemType === "agent_message") {
|
||||
const text = asString(item.text);
|
||||
if (text) return [{ kind: "assistant", ts, text: redactHomePathUserSegments(text) }];
|
||||
if (text) return [{ kind: "assistant", ts, text }];
|
||||
return [];
|
||||
}
|
||||
|
||||
if (itemType === "reasoning") {
|
||||
const text = asString(item.text);
|
||||
if (text) return [{ kind: "thinking", ts, text: redactHomePathUserSegments(text) }];
|
||||
if (text) return [{ kind: "thinking", ts, text }];
|
||||
return [{ kind: "system", ts, text: phase === "started" ? "reasoning started" : "reasoning completed" }];
|
||||
}
|
||||
|
||||
|
|
@ -153,9 +149,9 @@ function parseCodexItem(
|
|||
return [{
|
||||
kind: "tool_call",
|
||||
ts,
|
||||
name: redactHomePathUserSegments(asString(item.name, "unknown")),
|
||||
name: asString(item.name, "unknown"),
|
||||
toolUseId: asString(item.id),
|
||||
input: redactHomePathUserSegmentsInValue(item.input ?? {}),
|
||||
input: item.input ?? {},
|
||||
}];
|
||||
}
|
||||
|
||||
|
|
@ -167,12 +163,12 @@ function parseCodexItem(
|
|||
asString(item.result) ||
|
||||
stringifyUnknown(item.content ?? item.output ?? item.result);
|
||||
const isError = item.is_error === true || asString(item.status) === "error";
|
||||
return [{ kind: "tool_result", ts, toolUseId, content: redactHomePathUserSegments(content), isError }];
|
||||
return [{ kind: "tool_result", ts, toolUseId, content, isError }];
|
||||
}
|
||||
|
||||
if (itemType === "error" && phase === "completed") {
|
||||
const text = errorText(item.message ?? item.error ?? item);
|
||||
return [{ kind: "stderr", ts, text: redactHomePathUserSegments(text || "error") }];
|
||||
return [{ kind: "stderr", ts, text: text || "error" }];
|
||||
}
|
||||
|
||||
const id = asString(item.id);
|
||||
|
|
@ -181,14 +177,14 @@ function parseCodexItem(
|
|||
return [{
|
||||
kind: "system",
|
||||
ts,
|
||||
text: redactHomePathUserSegments(`item ${phase}: ${itemType || "unknown"}${meta ? ` (${meta})` : ""}`),
|
||||
text: `item ${phase}: ${itemType || "unknown"}${meta ? ` (${meta})` : ""}`,
|
||||
}];
|
||||
}
|
||||
|
||||
export function parseCodexStdoutLine(line: string, ts: string): TranscriptEntry[] {
|
||||
const parsed = asRecord(safeJsonParse(line));
|
||||
if (!parsed) {
|
||||
return [{ kind: "stdout", ts, text: redactHomePathUserSegments(line) }];
|
||||
return [{ kind: "stdout", ts, text: line }];
|
||||
}
|
||||
|
||||
const type = asString(parsed.type);
|
||||
|
|
@ -198,8 +194,8 @@ export function parseCodexStdoutLine(line: string, ts: string): TranscriptEntry[
|
|||
return [{
|
||||
kind: "init",
|
||||
ts,
|
||||
model: redactHomePathUserSegments(asString(parsed.model, "codex")),
|
||||
sessionId: redactHomePathUserSegments(threadId),
|
||||
model: asString(parsed.model, "codex"),
|
||||
sessionId: threadId,
|
||||
}];
|
||||
}
|
||||
|
||||
|
|
@ -221,15 +217,15 @@ export function parseCodexStdoutLine(line: string, ts: string): TranscriptEntry[
|
|||
return [{
|
||||
kind: "result",
|
||||
ts,
|
||||
text: redactHomePathUserSegments(asString(parsed.result)),
|
||||
text: asString(parsed.result),
|
||||
inputTokens,
|
||||
outputTokens,
|
||||
cachedTokens,
|
||||
costUsd: asNumber(parsed.total_cost_usd),
|
||||
subtype: redactHomePathUserSegments(asString(parsed.subtype)),
|
||||
subtype: asString(parsed.subtype),
|
||||
isError: parsed.is_error === true,
|
||||
errors: Array.isArray(parsed.errors)
|
||||
? parsed.errors.map(errorText).map(redactHomePathUserSegments).filter(Boolean)
|
||||
? parsed.errors.map(errorText).filter(Boolean)
|
||||
: [],
|
||||
}];
|
||||
}
|
||||
|
|
@ -243,21 +239,21 @@ export function parseCodexStdoutLine(line: string, ts: string): TranscriptEntry[
|
|||
return [{
|
||||
kind: "result",
|
||||
ts,
|
||||
text: redactHomePathUserSegments(asString(parsed.result)),
|
||||
text: asString(parsed.result),
|
||||
inputTokens,
|
||||
outputTokens,
|
||||
cachedTokens,
|
||||
costUsd: asNumber(parsed.total_cost_usd),
|
||||
subtype: redactHomePathUserSegments(asString(parsed.subtype, "turn.failed")),
|
||||
subtype: asString(parsed.subtype, "turn.failed"),
|
||||
isError: true,
|
||||
errors: message ? [redactHomePathUserSegments(message)] : [],
|
||||
errors: message ? [message] : [],
|
||||
}];
|
||||
}
|
||||
|
||||
if (type === "error") {
|
||||
const message = errorText(parsed.message ?? parsed.error ?? parsed);
|
||||
return [{ kind: "stderr", ts, text: redactHomePathUserSegments(message || line) }];
|
||||
return [{ kind: "stderr", ts, text: message || line }];
|
||||
}
|
||||
|
||||
return [{ kind: "stdout", ts, text: redactHomePathUserSegments(line) }];
|
||||
return [{ kind: "stdout", ts, text: line }];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -80,12 +80,12 @@ async function ensurePiSkillsInjected(
|
|||
if (result === "skipped") continue;
|
||||
await onLog(
|
||||
"stderr",
|
||||
`[paperclip] ${result === "repaired" ? "Repaired" : "Injected"} Pi skill "${entry.key}" into ${PI_AGENT_SKILLS_DIR}\n`,
|
||||
`[paperclip] ${result === "repaired" ? "Repaired" : "Injected"} Pi skill "${entry.runtimeName}" into ${PI_AGENT_SKILLS_DIR}\n`,
|
||||
);
|
||||
} catch (err) {
|
||||
await onLog(
|
||||
"stderr",
|
||||
`[paperclip] Failed to inject Pi skill "${entry.key}" into ${PI_AGENT_SKILLS_DIR}: ${err instanceof Error ? err.message : String(err)}\n`,
|
||||
`[paperclip] Failed to inject Pi skill "${entry.runtimeName}" into ${PI_AGENT_SKILLS_DIR}: ${err instanceof Error ? err.message : String(err)}\n`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,43 +0,0 @@
|
|||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = 'public' AND table_name = 'company_skills'
|
||||
) THEN
|
||||
CREATE TABLE "company_skills" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"company_id" uuid NOT NULL,
|
||||
"slug" text NOT NULL,
|
||||
"name" text NOT NULL,
|
||||
"description" text,
|
||||
"markdown" text NOT NULL,
|
||||
"source_type" text DEFAULT 'local_path' NOT NULL,
|
||||
"source_locator" text,
|
||||
"source_ref" text,
|
||||
"trust_level" text DEFAULT 'markdown_only' NOT NULL,
|
||||
"compatibility" text DEFAULT 'compatible' NOT NULL,
|
||||
"file_inventory" jsonb DEFAULT '[]'::jsonb NOT NULL,
|
||||
"metadata" jsonb,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
END IF;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_constraint
|
||||
WHERE conname = 'company_skills_company_id_companies_id_fk'
|
||||
) THEN
|
||||
ALTER TABLE "company_skills"
|
||||
ADD CONSTRAINT "company_skills_company_id_companies_id_fk"
|
||||
FOREIGN KEY ("company_id") REFERENCES "public"."companies"("id")
|
||||
ON DELETE no action ON UPDATE no action;
|
||||
END IF;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "company_skills_company_slug_idx" ON "company_skills" USING btree ("company_id","slug");--> statement-breakpoint
|
||||
CREATE INDEX IF NOT EXISTS "company_skills_company_name_idx" ON "company_skills" USING btree ("company_id","name");
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
ALTER TABLE "company_skills" ADD COLUMN "key" text;--> statement-breakpoint
|
||||
UPDATE "company_skills"
|
||||
SET "key" = CASE
|
||||
WHEN COALESCE("metadata"->>'sourceKind', '') = 'paperclip_bundled' THEN 'paperclipai/paperclip/' || "slug"
|
||||
WHEN (COALESCE("metadata"->>'sourceKind', '') = 'github' OR "source_type" = 'github')
|
||||
AND COALESCE("metadata"->>'owner', '') <> ''
|
||||
AND COALESCE("metadata"->>'repo', '') <> ''
|
||||
THEN lower("metadata"->>'owner') || '/' || lower("metadata"->>'repo') || '/' || "slug"
|
||||
WHEN COALESCE("metadata"->>'sourceKind', '') = 'managed_local' THEN 'company/' || "company_id"::text || '/' || "slug"
|
||||
WHEN (COALESCE("metadata"->>'sourceKind', '') = 'url' OR "source_type" = 'url')
|
||||
THEN 'url/'
|
||||
|| COALESCE(
|
||||
NULLIF(regexp_replace(lower(regexp_replace(COALESCE("source_locator", ''), '^https?://([^/]+).*$','\1')), '[^a-z0-9._-]+', '-', 'g'), ''),
|
||||
'unknown'
|
||||
)
|
||||
|| '/'
|
||||
|| substr(md5(COALESCE("source_locator", "slug")), 1, 10)
|
||||
|| '/'
|
||||
|| "slug"
|
||||
WHEN "source_type" = 'local_path' AND COALESCE("source_locator", '') <> ''
|
||||
THEN 'local/' || substr(md5("source_locator"), 1, 10) || '/' || "slug"
|
||||
ELSE 'company/' || "company_id"::text || '/' || "slug"
|
||||
END
|
||||
WHERE "key" IS NULL;--> statement-breakpoint
|
||||
ALTER TABLE "company_skills" ALTER COLUMN "key" SET NOT NULL;--> statement-breakpoint
|
||||
DROP INDEX IF EXISTS "company_skills_company_slug_idx";--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "company_skills_company_key_idx" ON "company_skills" USING btree ("company_id","key");
|
||||
1
packages/db/src/migrations/0039_curly_maria_hill.sql
Normal file
1
packages/db/src/migrations/0039_curly_maria_hill.sql
Normal file
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE "instance_settings" ADD COLUMN "general" jsonb DEFAULT '{}'::jsonb NOT NULL;
|
||||
22
packages/db/src/migrations/0040_spotty_the_renegades.sql
Normal file
22
packages/db/src/migrations/0040_spotty_the_renegades.sql
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
CREATE TABLE "company_skills" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"company_id" uuid NOT NULL,
|
||||
"key" text NOT NULL,
|
||||
"slug" text NOT NULL,
|
||||
"name" text NOT NULL,
|
||||
"description" text,
|
||||
"markdown" text NOT NULL,
|
||||
"source_type" text DEFAULT 'local_path' NOT NULL,
|
||||
"source_locator" text,
|
||||
"source_ref" text,
|
||||
"trust_level" text DEFAULT 'markdown_only' NOT NULL,
|
||||
"compatibility" text DEFAULT 'compatible' NOT NULL,
|
||||
"file_inventory" jsonb DEFAULT '[]'::jsonb NOT NULL,
|
||||
"metadata" jsonb,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "company_skills" ADD CONSTRAINT "company_skills_company_id_companies_id_fk" FOREIGN KEY ("company_id") REFERENCES "public"."companies"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "company_skills_company_key_idx" ON "company_skills" USING btree ("company_id","key");--> statement-breakpoint
|
||||
CREATE INDEX "company_skills_company_name_idx" ON "company_skills" USING btree ("company_id","name");
|
||||
File diff suppressed because it is too large
Load diff
10481
packages/db/src/migrations/meta/0040_snapshot.json
Normal file
10481
packages/db/src/migrations/meta/0040_snapshot.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"id": "c49c6ac1-3acd-4a7b-91e5-5ad193b154a5",
|
||||
"prevId": "70a51031-63ca-4491-8794-54cae9e07c66",
|
||||
"prevId": "ff2d3ea8-018e-44ec-9e7d-dfa81b2ef772",
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"tables": {
|
||||
|
|
@ -11390,4 +11390,4 @@
|
|||
"schemas": {},
|
||||
"tables": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -292,9 +292,23 @@
|
|||
{
|
||||
"idx": 41,
|
||||
"version": "7",
|
||||
"when": 1774011294562,
|
||||
"tag": "0039_curly_maria_hill",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 42,
|
||||
"version": "7",
|
||||
"when": 1774031825634,
|
||||
"tag": "0040_spotty_the_renegades",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 43,
|
||||
"version": "7",
|
||||
"when": 1774008910991,
|
||||
"tag": "0041_reflective_captain_universe",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ export const instanceSettings = pgTable(
|
|||
{
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
singletonKey: text("singleton_key").notNull().default("default"),
|
||||
general: jsonb("general").$type<Record<string, unknown>>().notNull().default({}),
|
||||
experimental: jsonb("experimental").$type<Record<string, unknown>>().notNull().default({}),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
|
|
|
|||
|
|
@ -162,6 +162,7 @@ export type {
|
|||
AgentSkillSnapshot,
|
||||
AgentSkillSyncRequest,
|
||||
InstanceExperimentalSettings,
|
||||
InstanceGeneralSettings,
|
||||
InstanceSettings,
|
||||
Agent,
|
||||
AgentAccessState,
|
||||
|
|
@ -310,6 +311,9 @@ export type {
|
|||
} from "./types/index.js";
|
||||
|
||||
export {
|
||||
instanceGeneralSettingsSchema,
|
||||
patchInstanceGeneralSettingsSchema,
|
||||
type PatchInstanceGeneralSettings,
|
||||
instanceExperimentalSettingsSchema,
|
||||
patchInstanceExperimentalSettingsSchema,
|
||||
type PatchInstanceExperimentalSettings,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ export interface CompanyPortabilityInclude {
|
|||
agents: boolean;
|
||||
projects: boolean;
|
||||
issues: boolean;
|
||||
skills: boolean;
|
||||
}
|
||||
|
||||
export interface CompanyPortabilityEnvInput {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
export type CompanySkillSourceType = "local_path" | "github" | "url" | "catalog";
|
||||
export type CompanySkillSourceType = "local_path" | "github" | "url" | "catalog" | "skills_sh";
|
||||
|
||||
export type CompanySkillTrustLevel = "markdown_only" | "assets" | "scripts_executables";
|
||||
|
||||
export type CompanySkillCompatibility = "compatible" | "unknown" | "invalid";
|
||||
|
||||
export type CompanySkillSourceBadge = "paperclip" | "github" | "local" | "url" | "catalog";
|
||||
export type CompanySkillSourceBadge = "paperclip" | "github" | "local" | "url" | "catalog" | "skills_sh";
|
||||
|
||||
export interface CompanySkillFileInventoryEntry {
|
||||
path: string;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
export type { Company } from "./company.js";
|
||||
export type { InstanceExperimentalSettings, InstanceSettings } from "./instance.js";
|
||||
export type { InstanceExperimentalSettings, InstanceGeneralSettings, InstanceSettings } from "./instance.js";
|
||||
export type {
|
||||
CompanySkillSourceType,
|
||||
CompanySkillTrustLevel,
|
||||
|
|
|
|||
|
|
@ -1,9 +1,15 @@
|
|||
export interface InstanceGeneralSettings {
|
||||
censorUsernameInLogs: boolean;
|
||||
}
|
||||
|
||||
export interface InstanceExperimentalSettings {
|
||||
enableIsolatedWorkspaces: boolean;
|
||||
autoRestartDevServerWhenIdle: boolean;
|
||||
}
|
||||
|
||||
export interface InstanceSettings {
|
||||
id: string;
|
||||
general: InstanceGeneralSettings;
|
||||
experimental: InstanceExperimentalSettings;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ export const portabilityIncludeSchema = z
|
|||
agents: z.boolean().optional(),
|
||||
projects: z.boolean().optional(),
|
||||
issues: z.boolean().optional(),
|
||||
skills: z.boolean().optional(),
|
||||
})
|
||||
.partial();
|
||||
|
||||
|
|
@ -119,6 +120,7 @@ export const portabilityManifestSchema = z.object({
|
|||
agents: z.boolean(),
|
||||
projects: z.boolean(),
|
||||
issues: z.boolean(),
|
||||
skills: z.boolean(),
|
||||
}),
|
||||
company: portabilityCompanyManifestEntrySchema.nullable(),
|
||||
agents: z.array(portabilityAgentManifestEntrySchema),
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import { z } from "zod";
|
||||
|
||||
export const companySkillSourceTypeSchema = z.enum(["local_path", "github", "url", "catalog"]);
|
||||
export const companySkillSourceTypeSchema = z.enum(["local_path", "github", "url", "catalog", "skills_sh"]);
|
||||
export const companySkillTrustLevelSchema = z.enum(["markdown_only", "assets", "scripts_executables"]);
|
||||
export const companySkillCompatibilitySchema = z.enum(["compatible", "unknown", "invalid"]);
|
||||
export const companySkillSourceBadgeSchema = z.enum(["paperclip", "github", "local", "url", "catalog"]);
|
||||
export const companySkillSourceBadgeSchema = z.enum(["paperclip", "github", "local", "url", "catalog", "skills_sh"]);
|
||||
|
||||
export const companySkillFileInventoryEntrySchema = z.object({
|
||||
path: z.string().min(1),
|
||||
|
|
|
|||
|
|
@ -33,7 +33,11 @@ export const updateCompanyBrandingSchema = z
|
|||
})
|
||||
.strict()
|
||||
.refine(
|
||||
(value) => value.brandColor !== undefined || value.logoAssetId !== undefined,
|
||||
(value) =>
|
||||
value.name !== undefined
|
||||
|| value.description !== undefined
|
||||
|| value.brandColor !== undefined
|
||||
|| value.logoAssetId !== undefined,
|
||||
"At least one branding field must be provided",
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,8 @@
|
|||
export {
|
||||
instanceGeneralSettingsSchema,
|
||||
patchInstanceGeneralSettingsSchema,
|
||||
type InstanceGeneralSettings,
|
||||
type PatchInstanceGeneralSettings,
|
||||
instanceExperimentalSettingsSchema,
|
||||
patchInstanceExperimentalSettingsSchema,
|
||||
type InstanceExperimentalSettings,
|
||||
|
|
|
|||
|
|
@ -1,10 +1,19 @@
|
|||
import { z } from "zod";
|
||||
|
||||
export const instanceGeneralSettingsSchema = z.object({
|
||||
censorUsernameInLogs: z.boolean().default(false),
|
||||
}).strict();
|
||||
|
||||
export const patchInstanceGeneralSettingsSchema = instanceGeneralSettingsSchema.partial();
|
||||
|
||||
export const instanceExperimentalSettingsSchema = z.object({
|
||||
enableIsolatedWorkspaces: z.boolean().default(false),
|
||||
autoRestartDevServerWhenIdle: z.boolean().default(false),
|
||||
}).strict();
|
||||
|
||||
export const patchInstanceExperimentalSettingsSchema = instanceExperimentalSettingsSchema.partial();
|
||||
|
||||
export type InstanceGeneralSettings = z.infer<typeof instanceGeneralSettingsSchema>;
|
||||
export type PatchInstanceGeneralSettings = z.infer<typeof patchInstanceGeneralSettingsSchema>;
|
||||
export type InstanceExperimentalSettings = z.infer<typeof instanceExperimentalSettingsSchema>;
|
||||
export type PatchInstanceExperimentalSettings = z.infer<typeof patchInstanceExperimentalSettingsSchema>;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue