orionfold-relay 0.26.0 → 0.27.0
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/dist/cli.js +4 -4
- package/package.json +1 -1
- package/src/app/profiles/page.tsx +7 -1
- package/src/app/tables/page.tsx +10 -1
- package/src/components/profiles/profile-browser.tsx +36 -1
- package/src/components/profiles/profile-card.tsx +9 -2
- package/src/components/schedules/schedule-list.tsx +30 -1
- package/src/components/shared/pack-pill.tsx +39 -0
- package/src/components/tables/table-browser.tsx +25 -1
- package/src/components/tables/table-grid.tsx +16 -4
- package/src/components/tables/table-list-table.tsx +12 -1
- package/src/components/workflows/blueprint-gallery.tsx +68 -4
- package/src/lib/apps/pack-of.ts +83 -0
- package/src/lib/data/seed-data/installed-packs.ts +97 -0
- package/src/lib/data/seed.ts +24 -0
- package/src/lib/packs/templates/relay-agency-pro/base/manifest.yaml +8 -2
- package/src/lib/packs/templates/relay-agency-pro/base/seed/tables/engagements.json +28 -0
- package/src/lib/packs/templates/relay-agency-pro/pack.yaml +5 -1
- package/src/lib/plugins/examples/echo-server/plugin.yaml +1 -1
- package/src/lib/plugins/examples/finance-pack/plugin.yaml +1 -1
- package/src/lib/plugins/examples/reading-radar/plugin.yaml +1 -1
- package/src/lib/plugins/registry.ts +1 -1
- package/src/lib/plugins/sdk/types.ts +1 -1
package/dist/cli.js
CHANGED
|
@@ -1186,7 +1186,7 @@ var CURRENT_PLUGIN_API_VERSION, CAPABILITY_VALUES, ORIGIN_VALUES, PrimitivesBund
|
|
|
1186
1186
|
var init_types = __esm({
|
|
1187
1187
|
"src/lib/plugins/sdk/types.ts"() {
|
|
1188
1188
|
"use strict";
|
|
1189
|
-
CURRENT_PLUGIN_API_VERSION = "0.
|
|
1189
|
+
CURRENT_PLUGIN_API_VERSION = "0.27";
|
|
1190
1190
|
CAPABILITY_VALUES = ["fs", "net", "child_process", "env"];
|
|
1191
1191
|
ORIGIN_VALUES = ["ainative-internal", "third-party"];
|
|
1192
1192
|
PrimitivesBundleManifestSchema = z.object({
|
|
@@ -12982,7 +12982,7 @@ var init_registry6 = __esm({
|
|
|
12982
12982
|
init_registry5();
|
|
12983
12983
|
init_installer();
|
|
12984
12984
|
init_schedule_spec();
|
|
12985
|
-
SUPPORTED_API_VERSIONS = /* @__PURE__ */ new Set([CURRENT_PLUGIN_API_VERSION, "0.
|
|
12985
|
+
SUPPORTED_API_VERSIONS = /* @__PURE__ */ new Set([CURRENT_PLUGIN_API_VERSION, "0.26"]);
|
|
12986
12986
|
pluginCache = null;
|
|
12987
12987
|
lastLoadedPluginIds = /* @__PURE__ */ new Set();
|
|
12988
12988
|
PluginTableSchema = z16.object({
|
|
@@ -25913,8 +25913,8 @@ import { execFileSync as execFileSync3 } from "child_process";
|
|
|
25913
25913
|
import yaml12 from "js-yaml";
|
|
25914
25914
|
import semver from "semver";
|
|
25915
25915
|
function relayCoreVersion() {
|
|
25916
|
-
if (semver.valid("0.
|
|
25917
|
-
return "0.
|
|
25916
|
+
if (semver.valid("0.27.0")) {
|
|
25917
|
+
return "0.27.0";
|
|
25918
25918
|
}
|
|
25919
25919
|
try {
|
|
25920
25920
|
const root = getAppRoot(import.meta.dirname, 3);
|
package/package.json
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { listProfiles, isBuiltin } from "@/lib/agents/profiles/registry";
|
|
2
2
|
import { sortProfilesByName } from "@/lib/agents/profiles/sort";
|
|
3
|
+
import { listApps } from "@/lib/apps/registry";
|
|
3
4
|
import { ProfileBrowser } from "@/components/profiles/profile-browser";
|
|
4
5
|
import { PageShell } from "@/components/shared/page-shell";
|
|
5
6
|
|
|
@@ -13,12 +14,17 @@ export default async function ProfilesPage() {
|
|
|
13
14
|
}))
|
|
14
15
|
);
|
|
15
16
|
|
|
17
|
+
// Installed packs — the source-of-truth set the client resolves each
|
|
18
|
+
// profile's pack provenance against (FEAT-8 pill / FEAT-7 filter via
|
|
19
|
+
// packOf). Just {id, name}; the pill renders the name, packOf gates on the id.
|
|
20
|
+
const installedPacks = listApps().map((a) => ({ id: a.id, name: a.name }));
|
|
21
|
+
|
|
16
22
|
return (
|
|
17
23
|
<PageShell
|
|
18
24
|
title="Profiles"
|
|
19
25
|
description="Browse and inspect agent profiles without blur-heavy detail surfaces."
|
|
20
26
|
>
|
|
21
|
-
<ProfileBrowser initialProfiles={profiles} />
|
|
27
|
+
<ProfileBrowser initialProfiles={profiles} installedPacks={installedPacks} />
|
|
22
28
|
</PageShell>
|
|
23
29
|
);
|
|
24
30
|
}
|
package/src/app/tables/page.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { listTables } from "@/lib/data/tables";
|
|
2
2
|
import { db } from "@/lib/db";
|
|
3
3
|
import { projects } from "@/lib/db/schema";
|
|
4
|
+
import { listApps } from "@/lib/apps/registry";
|
|
4
5
|
import { TableBrowser } from "@/components/tables/table-browser";
|
|
5
6
|
import { PageShell } from "@/components/shared/page-shell";
|
|
6
7
|
|
|
@@ -13,9 +14,17 @@ export default async function TablesPage() {
|
|
|
13
14
|
.select({ id: projects.id, name: projects.name })
|
|
14
15
|
.from(projects);
|
|
15
16
|
|
|
17
|
+
// Installed packs — mark tables whose projectId is a pack with a pack pill
|
|
18
|
+
// (FEAT-8) instead of the plain project name.
|
|
19
|
+
const installedPacks = listApps().map((a) => ({ id: a.id, name: a.name }));
|
|
20
|
+
|
|
16
21
|
return (
|
|
17
22
|
<PageShell title="Tables">
|
|
18
|
-
<TableBrowser
|
|
23
|
+
<TableBrowser
|
|
24
|
+
initialTables={tables}
|
|
25
|
+
projects={projectList}
|
|
26
|
+
installedPacks={installedPacks}
|
|
27
|
+
/>
|
|
19
28
|
</PageShell>
|
|
20
29
|
);
|
|
21
30
|
}
|
|
@@ -17,18 +17,52 @@ import { ProfileCard } from "@/components/profiles/profile-card";
|
|
|
17
17
|
import { ProfileImportDialog } from "@/components/profiles/profile-import-dialog";
|
|
18
18
|
import { RepoImportWizard } from "@/components/profiles/repo-import-wizard";
|
|
19
19
|
import type { AgentProfile } from "@/lib/agents/profiles/types";
|
|
20
|
+
import { packOf } from "@/lib/apps/pack-of";
|
|
20
21
|
|
|
21
22
|
interface ProfileWithBuiltin extends AgentProfile {
|
|
22
23
|
isBuiltin?: boolean;
|
|
23
24
|
}
|
|
24
25
|
|
|
26
|
+
/** The installed-pack identity a profile carries (FEAT-8). {id, name} only. */
|
|
27
|
+
export interface InstalledPackRef {
|
|
28
|
+
id: string;
|
|
29
|
+
name: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
25
32
|
interface ProfileBrowserProps {
|
|
26
33
|
initialProfiles: AgentProfile[];
|
|
34
|
+
/** Installed packs — resolves each profile's pack provenance (FEAT-8). */
|
|
35
|
+
installedPacks?: InstalledPackRef[];
|
|
27
36
|
}
|
|
28
37
|
|
|
29
|
-
export function ProfileBrowser({
|
|
38
|
+
export function ProfileBrowser({
|
|
39
|
+
initialProfiles,
|
|
40
|
+
installedPacks = [],
|
|
41
|
+
}: ProfileBrowserProps) {
|
|
30
42
|
const router = useRouter();
|
|
31
43
|
const [profiles, setProfiles] = useState<ProfileWithBuiltin[]>(initialProfiles);
|
|
44
|
+
|
|
45
|
+
// Stable {id → display name} lookup + the gated id-set for packOf. Rebuilt
|
|
46
|
+
// only when the installed packs change (never on a profile refresh), so the
|
|
47
|
+
// pill survives refreshProfiles — which recomputes from the refreshed id.
|
|
48
|
+
const packNameById = useMemo(
|
|
49
|
+
() => new Map(installedPacks.map((p) => [p.id, p.name])),
|
|
50
|
+
[installedPacks]
|
|
51
|
+
);
|
|
52
|
+
const installedPackIds = useMemo(
|
|
53
|
+
() => new Set(installedPacks.map((p) => p.id)),
|
|
54
|
+
[installedPacks]
|
|
55
|
+
);
|
|
56
|
+
const packNameFor = useCallback(
|
|
57
|
+
(profile: AgentProfile): string | null => {
|
|
58
|
+
const packId = packOf(
|
|
59
|
+
{ kind: "profile", id: profile.id },
|
|
60
|
+
installedPackIds
|
|
61
|
+
);
|
|
62
|
+
return packId ? packNameById.get(packId) ?? null : null;
|
|
63
|
+
},
|
|
64
|
+
[installedPackIds, packNameById]
|
|
65
|
+
);
|
|
32
66
|
const [search, setSearch] = useState("");
|
|
33
67
|
const [domainFilter, setDomainFilter] = useState<
|
|
34
68
|
"all" | "work" | "personal"
|
|
@@ -201,6 +235,7 @@ export function ProfileBrowser({ initialProfiles }: ProfileBrowserProps) {
|
|
|
201
235
|
key={profile.id}
|
|
202
236
|
profile={profile}
|
|
203
237
|
isBuiltin={profile.isBuiltin}
|
|
238
|
+
packName={packNameFor(profile)}
|
|
204
239
|
onClick={() => router.push(`/profiles/${profile.id}`)}
|
|
205
240
|
/>
|
|
206
241
|
))}
|
|
@@ -6,11 +6,14 @@ import { Download } from "lucide-react";
|
|
|
6
6
|
import type { AgentRuntimeId } from "@/lib/agents/runtime/catalog";
|
|
7
7
|
import { getSupportedRuntimes } from "@/lib/agents/profiles/compatibility";
|
|
8
8
|
import { IconCircle, getProfileIcon, getDomainColors } from "@/lib/constants/card-icons";
|
|
9
|
+
import { PackPill } from "@/components/shared/pack-pill";
|
|
9
10
|
import type { AgentProfile } from "@/lib/agents/profiles/types";
|
|
10
11
|
|
|
11
12
|
interface ProfileCardProps {
|
|
12
13
|
profile: AgentProfile;
|
|
13
14
|
isBuiltin?: boolean;
|
|
15
|
+
/** Display name of the pack that installed this profile, or null (FEAT-8). */
|
|
16
|
+
packName?: string | null;
|
|
14
17
|
onClick: () => void;
|
|
15
18
|
}
|
|
16
19
|
|
|
@@ -30,7 +33,7 @@ const RUNTIME_SHORT_LABEL: Record<AgentRuntimeId, string> = {
|
|
|
30
33
|
ollama: "Ollama (Local)",
|
|
31
34
|
};
|
|
32
35
|
|
|
33
|
-
export function ProfileCard({ profile, isBuiltin = false, onClick }: ProfileCardProps) {
|
|
36
|
+
export function ProfileCard({ profile, isBuiltin = false, packName = null, onClick }: ProfileCardProps) {
|
|
34
37
|
|
|
35
38
|
return (
|
|
36
39
|
<Card
|
|
@@ -83,7 +86,11 @@ export function ProfileCard({ profile, isBuiltin = false, onClick }: ProfileCard
|
|
|
83
86
|
</div>
|
|
84
87
|
|
|
85
88
|
<div className="flex items-center gap-3 text-xs text-muted-foreground">
|
|
86
|
-
{
|
|
89
|
+
{/* Pack provenance outranks every other origin: a pack-installed
|
|
90
|
+
profile is never "Custom"/"Discovered" — it belongs to its pack. */}
|
|
91
|
+
{packName ? (
|
|
92
|
+
<PackPill packName={packName} />
|
|
93
|
+
) : profile.importMeta ? (
|
|
87
94
|
<span className="flex items-center gap-1.5">
|
|
88
95
|
<Badge variant="outline" className="border-purple-200 text-purple-600 dark:border-purple-800 dark:text-purple-400">
|
|
89
96
|
<Download className="mr-1 h-3 w-3" />
|
|
@@ -11,6 +11,8 @@ import { ScheduleStatusBadge } from "./schedule-status-badge";
|
|
|
11
11
|
import { ConfirmDialog } from "@/components/shared/confirm-dialog";
|
|
12
12
|
import { EmptyState } from "@/components/shared/empty-state";
|
|
13
13
|
import { describeCron } from "@/lib/schedules/interval-parser";
|
|
14
|
+
import { PackPill } from "@/components/shared/pack-pill";
|
|
15
|
+
import { packOf } from "@/lib/apps/pack-of";
|
|
14
16
|
import { Clock, Heart, Pause, Play, Trash2 } from "lucide-react";
|
|
15
17
|
import { toast } from "sonner";
|
|
16
18
|
|
|
@@ -39,6 +41,9 @@ interface ScheduleListProps {
|
|
|
39
41
|
|
|
40
42
|
export function ScheduleList({ projects, initialSelectedId }: ScheduleListProps) {
|
|
41
43
|
const [schedules, setSchedules] = useState<Schedule[]>([]);
|
|
44
|
+
const [installedPacks, setInstalledPacks] = useState<
|
|
45
|
+
{ id: string; name: string }[]
|
|
46
|
+
>([]);
|
|
42
47
|
const [loaded, setLoaded] = useState(false);
|
|
43
48
|
const [confirmDeleteId, setConfirmDeleteId] = useState<string | null>(null);
|
|
44
49
|
const [selectedScheduleId, setSelectedScheduleId] = useState<string | null>(
|
|
@@ -57,6 +62,24 @@ export function ScheduleList({ projects, initialSelectedId }: ScheduleListProps)
|
|
|
57
62
|
refresh();
|
|
58
63
|
}, [refresh]);
|
|
59
64
|
|
|
65
|
+
// Installed packs — a pack schedule's id is `app:<packId>:<sid>`, so packOf
|
|
66
|
+
// resolves provenance from the id alone (FEAT-8).
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
fetch("/api/apps")
|
|
69
|
+
.then((r) => (r.ok ? r.json() : []))
|
|
70
|
+
.then((apps: Array<{ id: string; name: string }>) =>
|
|
71
|
+
setInstalledPacks(apps.map((a) => ({ id: a.id, name: a.name })))
|
|
72
|
+
)
|
|
73
|
+
.catch(() => {});
|
|
74
|
+
}, []);
|
|
75
|
+
|
|
76
|
+
const installedPackIds = new Set(installedPacks.map((p) => p.id));
|
|
77
|
+
const packNameById = new Map(installedPacks.map((p) => [p.id, p.name]));
|
|
78
|
+
const packNameForSchedule = (id: string): string | null => {
|
|
79
|
+
const packId = packOf({ kind: "schedule", id }, installedPackIds);
|
|
80
|
+
return packId ? packNameById.get(packId) ?? null : null;
|
|
81
|
+
};
|
|
82
|
+
|
|
60
83
|
async function handlePauseResume(id: string, currentStatus: string) {
|
|
61
84
|
const newStatus = currentStatus === "active" ? "paused" : "active";
|
|
62
85
|
const res = await fetch(`/api/schedules/${id}`, {
|
|
@@ -163,7 +186,13 @@ export function ScheduleList({ projects, initialSelectedId }: ScheduleListProps)
|
|
|
163
186
|
)}
|
|
164
187
|
{sched.name}
|
|
165
188
|
</CardTitle>
|
|
166
|
-
<
|
|
189
|
+
<div className="flex shrink-0 items-center gap-1.5">
|
|
190
|
+
{(() => {
|
|
191
|
+
const packName = packNameForSchedule(sched.id);
|
|
192
|
+
return packName ? <PackPill packName={packName} /> : null;
|
|
193
|
+
})()}
|
|
194
|
+
<ScheduleStatusBadge status={sched.status} />
|
|
195
|
+
</div>
|
|
167
196
|
</div>
|
|
168
197
|
</CardHeader>
|
|
169
198
|
<CardContent>
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Package } from "lucide-react";
|
|
2
|
+
import { Badge } from "@/components/ui/badge";
|
|
3
|
+
import { cn } from "@/lib/utils";
|
|
4
|
+
|
|
5
|
+
interface PackPillProps {
|
|
6
|
+
/** The installed pack's display name (e.g. "Relay Agency"), from its manifest. */
|
|
7
|
+
packName: string;
|
|
8
|
+
className?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* PackPill — provenance label marking a primitive (profile, blueprint, table,
|
|
13
|
+
* schedule) as installed by a pack (FEAT-8, spec:
|
|
14
|
+
* features/fix-app-shell-activation-redesign.md).
|
|
15
|
+
*
|
|
16
|
+
* Deliberately NOT a StatusChip / status-family: pack provenance is an open-set
|
|
17
|
+
* identity ("which pack"), not one of the 5 fixed status dimensions. It shares
|
|
18
|
+
* the visual system by building on the same `Badge` primitive StatusChip uses,
|
|
19
|
+
* with its own icon (package) and a distinct amber color family so it reads as
|
|
20
|
+
* a provenance marker — the sibling of the card's existing Imported/Discovered/
|
|
21
|
+
* Built-in badges, and outranks the "Custom" fallback (a pack-installed
|
|
22
|
+
* primitive is never "Custom").
|
|
23
|
+
*/
|
|
24
|
+
export function PackPill({ packName, className }: PackPillProps) {
|
|
25
|
+
return (
|
|
26
|
+
<Badge
|
|
27
|
+
data-testid="pack-pill"
|
|
28
|
+
variant="outline"
|
|
29
|
+
title={`Installed by the ${packName} pack`}
|
|
30
|
+
className={cn(
|
|
31
|
+
"border-amber-200 text-amber-700 dark:border-amber-800 dark:text-amber-400",
|
|
32
|
+
className
|
|
33
|
+
)}
|
|
34
|
+
>
|
|
35
|
+
<Package className="mr-1 h-3 w-3" />
|
|
36
|
+
{packName}
|
|
37
|
+
</Badge>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
@@ -19,15 +19,37 @@ import { TableCreateSheet } from "./table-create-sheet";
|
|
|
19
19
|
import { FilterBar } from "@/components/shared/filter-bar";
|
|
20
20
|
import { EmptyState } from "@/components/shared/empty-state";
|
|
21
21
|
import { Table2 } from "lucide-react";
|
|
22
|
+
import { packOf } from "@/lib/apps/pack-of";
|
|
22
23
|
import type { TableWithRelations } from "./types";
|
|
23
24
|
|
|
24
25
|
interface TableBrowserProps {
|
|
25
26
|
initialTables: TableWithRelations[];
|
|
26
27
|
projects: { id: string; name: string }[];
|
|
28
|
+
/** Installed packs — marks tables whose project is a pack (FEAT-8). */
|
|
29
|
+
installedPacks?: { id: string; name: string }[];
|
|
27
30
|
}
|
|
28
31
|
|
|
29
|
-
export function TableBrowser({
|
|
32
|
+
export function TableBrowser({
|
|
33
|
+
initialTables,
|
|
34
|
+
projects,
|
|
35
|
+
installedPacks = [],
|
|
36
|
+
}: TableBrowserProps) {
|
|
30
37
|
const [tables, setTables] = useState(initialTables);
|
|
38
|
+
// {projectId → pack name} for pack-installed projects, via the shared
|
|
39
|
+
// resolver (tables associate to a pack by projectId === packId).
|
|
40
|
+
const installedPackIds = new Set(installedPacks.map((p) => p.id));
|
|
41
|
+
const packNameById = new Map(installedPacks.map((p) => [p.id, p.name]));
|
|
42
|
+
const packNameForProject = useCallback(
|
|
43
|
+
(projectId: string | null | undefined): string | null => {
|
|
44
|
+
const packId = packOf(
|
|
45
|
+
{ kind: "table", id: "", projectId: projectId ?? undefined },
|
|
46
|
+
installedPackIds
|
|
47
|
+
);
|
|
48
|
+
return packId ? packNameById.get(packId) ?? null : null;
|
|
49
|
+
},
|
|
50
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
51
|
+
[installedPacks]
|
|
52
|
+
);
|
|
31
53
|
const [view, setView] = useState<"table" | "grid">("table");
|
|
32
54
|
const [search, setSearch] = useState("");
|
|
33
55
|
const [sourceFilter, setSourceFilter] = useState<string>("all");
|
|
@@ -214,12 +236,14 @@ export function TableBrowser({ initialTables, projects }: TableBrowserProps) {
|
|
|
214
236
|
onToggleSelectAll={toggleSelectAll}
|
|
215
237
|
onSelect={navigate}
|
|
216
238
|
onOpen={navigate}
|
|
239
|
+
packNameForProject={packNameForProject}
|
|
217
240
|
/>
|
|
218
241
|
) : (
|
|
219
242
|
<TableGrid
|
|
220
243
|
tables={filtered}
|
|
221
244
|
onSelect={navigate}
|
|
222
245
|
onOpen={navigate}
|
|
246
|
+
packNameForProject={packNameForProject}
|
|
223
247
|
/>
|
|
224
248
|
)}
|
|
225
249
|
|
|
@@ -4,6 +4,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
|
4
4
|
import { Badge } from "@/components/ui/badge";
|
|
5
5
|
import { Table2 } from "lucide-react";
|
|
6
6
|
import { tableSourceVariant } from "@/lib/constants/table-status";
|
|
7
|
+
import { PackPill } from "@/components/shared/pack-pill";
|
|
7
8
|
import { formatRowCount, formatColumnCount } from "./utils";
|
|
8
9
|
import type { TableWithRelations } from "./types";
|
|
9
10
|
|
|
@@ -11,9 +12,16 @@ interface TableGridProps {
|
|
|
11
12
|
tables: TableWithRelations[];
|
|
12
13
|
onSelect: (id: string) => void;
|
|
13
14
|
onOpen: (id: string) => void;
|
|
15
|
+
/** Resolves a projectId to its pack display name, or null (FEAT-8). */
|
|
16
|
+
packNameForProject?: (projectId: string | null | undefined) => string | null;
|
|
14
17
|
}
|
|
15
18
|
|
|
16
|
-
export function TableGrid({
|
|
19
|
+
export function TableGrid({
|
|
20
|
+
tables,
|
|
21
|
+
onSelect,
|
|
22
|
+
onOpen,
|
|
23
|
+
packNameForProject,
|
|
24
|
+
}: TableGridProps) {
|
|
17
25
|
return (
|
|
18
26
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
19
27
|
{tables.map((t) => (
|
|
@@ -49,9 +57,13 @@ export function TableGrid({ tables, onSelect, onOpen }: TableGridProps) {
|
|
|
49
57
|
<div className="flex items-center gap-3 text-xs text-muted-foreground">
|
|
50
58
|
<span>{formatColumnCount(t.columnCount)}</span>
|
|
51
59
|
<span>{formatRowCount(t.rowCount)}</span>
|
|
52
|
-
{
|
|
53
|
-
|
|
54
|
-
|
|
60
|
+
{(() => {
|
|
61
|
+
const packName = packNameForProject?.(t.projectId);
|
|
62
|
+
if (packName) return <PackPill packName={packName} />;
|
|
63
|
+
return t.projectName ? (
|
|
64
|
+
<span className="truncate">{t.projectName}</span>
|
|
65
|
+
) : null;
|
|
66
|
+
})()}
|
|
55
67
|
</div>
|
|
56
68
|
</CardContent>
|
|
57
69
|
</Card>
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
TableRow,
|
|
12
12
|
} from "@/components/ui/table";
|
|
13
13
|
import { tableSourceVariant } from "@/lib/constants/table-status";
|
|
14
|
+
import { PackPill } from "@/components/shared/pack-pill";
|
|
14
15
|
import { formatRowCount } from "./utils";
|
|
15
16
|
import type { TableWithRelations } from "./types";
|
|
16
17
|
|
|
@@ -21,6 +22,8 @@ interface TableListTableProps {
|
|
|
21
22
|
onToggleSelectAll: () => void;
|
|
22
23
|
onSelect: (id: string) => void;
|
|
23
24
|
onOpen: (id: string) => void;
|
|
25
|
+
/** Resolves a projectId to its pack display name, or null (FEAT-8). */
|
|
26
|
+
packNameForProject?: (projectId: string | null | undefined) => string | null;
|
|
24
27
|
}
|
|
25
28
|
|
|
26
29
|
export function TableListTable({
|
|
@@ -30,6 +33,7 @@ export function TableListTable({
|
|
|
30
33
|
onToggleSelectAll,
|
|
31
34
|
onSelect,
|
|
32
35
|
onOpen,
|
|
36
|
+
packNameForProject,
|
|
33
37
|
}: TableListTableProps) {
|
|
34
38
|
return (
|
|
35
39
|
<div className="rounded-lg border">
|
|
@@ -68,7 +72,14 @@ export function TableListTable({
|
|
|
68
72
|
</TableCell>
|
|
69
73
|
<TableCell className="font-medium">{t.name}</TableCell>
|
|
70
74
|
<TableCell className="text-muted-foreground">
|
|
71
|
-
{
|
|
75
|
+
{(() => {
|
|
76
|
+
const packName = packNameForProject?.(t.projectId);
|
|
77
|
+
return packName ? (
|
|
78
|
+
<PackPill packName={packName} />
|
|
79
|
+
) : (
|
|
80
|
+
(t.projectName ?? "—")
|
|
81
|
+
);
|
|
82
|
+
})()}
|
|
72
83
|
</TableCell>
|
|
73
84
|
<TableCell className="text-right text-muted-foreground">
|
|
74
85
|
{t.columnCount}
|
|
@@ -12,8 +12,16 @@ import { Button } from "@/components/ui/button";
|
|
|
12
12
|
import { Search, Layers, Plus } from "lucide-react";
|
|
13
13
|
import { patternLabels } from "@/lib/constants/status-colors";
|
|
14
14
|
import { IconCircle, getWorkflowIconFromName } from "@/lib/constants/card-icons";
|
|
15
|
+
import { PackPill } from "@/components/shared/pack-pill";
|
|
16
|
+
import { packOf } from "@/lib/apps/pack-of";
|
|
15
17
|
import type { WorkflowBlueprint } from "@/lib/workflows/blueprints/types";
|
|
16
18
|
|
|
19
|
+
/** {id, name} of an installed pack — fetched from /api/apps for provenance. */
|
|
20
|
+
interface InstalledPack {
|
|
21
|
+
id: string;
|
|
22
|
+
name: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
17
25
|
const difficultyColors: Record<string, string> = {
|
|
18
26
|
beginner: "border-green-500/30 bg-green-500/10 text-green-700 dark:text-green-400",
|
|
19
27
|
intermediate: "border-yellow-500/30 bg-yellow-500/10 text-yellow-700 dark:text-yellow-400",
|
|
@@ -23,21 +31,51 @@ const difficultyColors: Record<string, string> = {
|
|
|
23
31
|
export function BlueprintGallery() {
|
|
24
32
|
const router = useRouter();
|
|
25
33
|
const [blueprints, setBlueprints] = useState<WorkflowBlueprint[]>([]);
|
|
34
|
+
const [installedPacks, setInstalledPacks] = useState<InstalledPack[]>([]);
|
|
26
35
|
const [loaded, setLoaded] = useState(false);
|
|
27
36
|
const [search, setSearch] = useState("");
|
|
28
37
|
const [domainFilter, setDomainFilter] = useState<"all" | "work" | "personal">("all");
|
|
38
|
+
// FEAT-7 — "all" or a specific installed pack id.
|
|
39
|
+
const [packFilter, setPackFilter] = useState<string>("all");
|
|
29
40
|
|
|
30
41
|
useEffect(() => {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
42
|
+
// Blueprints + installed packs in parallel; the pack list resolves each
|
|
43
|
+
// blueprint's provenance (packOf) for the pill (FEAT-8) and filter (FEAT-7).
|
|
44
|
+
Promise.all([
|
|
45
|
+
fetch("/api/blueprints").then((r) => (r.ok ? r.json() : [])),
|
|
46
|
+
fetch("/api/apps").then((r) => (r.ok ? r.json() : [])),
|
|
47
|
+
])
|
|
48
|
+
.then(([bps, apps]) => {
|
|
49
|
+
setBlueprints(bps);
|
|
50
|
+
setInstalledPacks(
|
|
51
|
+
(apps as Array<{ id: string; name: string }>).map((a) => ({
|
|
52
|
+
id: a.id,
|
|
53
|
+
name: a.name,
|
|
54
|
+
}))
|
|
55
|
+
);
|
|
56
|
+
})
|
|
34
57
|
.finally(() => setLoaded(true));
|
|
35
58
|
}, []);
|
|
36
59
|
|
|
60
|
+
const packNameById = useMemo(
|
|
61
|
+
() => new Map(installedPacks.map((p) => [p.id, p.name])),
|
|
62
|
+
[installedPacks]
|
|
63
|
+
);
|
|
64
|
+
const installedPackIds = useMemo(
|
|
65
|
+
() => new Set(installedPacks.map((p) => p.id)),
|
|
66
|
+
[installedPacks]
|
|
67
|
+
);
|
|
68
|
+
const packIdFor = (bp: WorkflowBlueprint) =>
|
|
69
|
+
packOf({ kind: "blueprint", id: bp.id }, installedPackIds);
|
|
70
|
+
|
|
37
71
|
const filtered = useMemo(() => {
|
|
38
72
|
const q = search.toLowerCase();
|
|
39
73
|
return blueprints.filter((bp) => {
|
|
40
74
|
if (domainFilter !== "all" && bp.domain !== domainFilter) return false;
|
|
75
|
+
if (packFilter !== "all") {
|
|
76
|
+
if (packOf({ kind: "blueprint", id: bp.id }, installedPackIds) !== packFilter)
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
41
79
|
if (!q) return true;
|
|
42
80
|
return (
|
|
43
81
|
bp.name.toLowerCase().includes(q) ||
|
|
@@ -45,7 +83,7 @@ export function BlueprintGallery() {
|
|
|
45
83
|
bp.tags.some((t) => t.toLowerCase().includes(q))
|
|
46
84
|
);
|
|
47
85
|
});
|
|
48
|
-
}, [blueprints, search, domainFilter]);
|
|
86
|
+
}, [blueprints, search, domainFilter, packFilter, installedPackIds]);
|
|
49
87
|
|
|
50
88
|
return (
|
|
51
89
|
<div className="space-y-6">
|
|
@@ -84,6 +122,23 @@ export function BlueprintGallery() {
|
|
|
84
122
|
<TabsTrigger value="personal">Personal</TabsTrigger>
|
|
85
123
|
</TabsList>
|
|
86
124
|
</Tabs>
|
|
125
|
+
{/* FEAT-7 — filter by installed pack. Only shown when a pack is
|
|
126
|
+
installed, so the control never appears empty on a fresh instance. */}
|
|
127
|
+
{installedPacks.length > 0 && (
|
|
128
|
+
<select
|
|
129
|
+
value={packFilter}
|
|
130
|
+
onChange={(e) => setPackFilter(e.target.value)}
|
|
131
|
+
aria-label="Filter by pack"
|
|
132
|
+
className="h-9 rounded-md border border-input bg-background px-3 text-sm"
|
|
133
|
+
>
|
|
134
|
+
<option value="all">All packs</option>
|
|
135
|
+
{installedPacks.map((p) => (
|
|
136
|
+
<option key={p.id} value={p.id}>
|
|
137
|
+
{p.name}
|
|
138
|
+
</option>
|
|
139
|
+
))}
|
|
140
|
+
</select>
|
|
141
|
+
)}
|
|
87
142
|
</div>
|
|
88
143
|
|
|
89
144
|
{/* Grid */}
|
|
@@ -141,6 +196,15 @@ export function BlueprintGallery() {
|
|
|
141
196
|
<p className="text-xs text-muted-foreground line-clamp-2">
|
|
142
197
|
{bp.description}
|
|
143
198
|
</p>
|
|
199
|
+
{(() => {
|
|
200
|
+
const packId = packIdFor(bp);
|
|
201
|
+
const packName = packId ? packNameById.get(packId) : null;
|
|
202
|
+
return packName ? (
|
|
203
|
+
<div className="mt-2">
|
|
204
|
+
<PackPill packName={packName} />
|
|
205
|
+
</div>
|
|
206
|
+
) : null;
|
|
207
|
+
})()}
|
|
144
208
|
<div className="flex flex-wrap items-center gap-2 mt-2 text-xs text-muted-foreground">
|
|
145
209
|
<span>{patternLabels[bp.pattern] ?? bp.pattern}</span>
|
|
146
210
|
<span>·</span>
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { extractAppIdFromArtifactId } from "./composition-detector";
|
|
2
|
+
import { parseAppScheduleId } from "./app-schedule-id";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Primitive → pack source-of-truth resolver (spec:
|
|
6
|
+
* features/fix-app-shell-activation-redesign.md → "Grooming decision (S40)").
|
|
7
|
+
*
|
|
8
|
+
* The pack that installed a primitive is NOT recorded as a first-class field —
|
|
9
|
+
* it is encoded by convention, differently per kind:
|
|
10
|
+
*
|
|
11
|
+
* - profiles / blueprints : the `<packId>--<name>` id prefix (file-dropped
|
|
12
|
+
* verbatim from the pack manifest, where `prefix === pack.meta.id`).
|
|
13
|
+
* - tables : `projectId === pack.meta.id` (a DB column set at
|
|
14
|
+
* install; the id itself is a fresh UUID).
|
|
15
|
+
* - schedules : the `app:<packId>:<sid>` composite id (most
|
|
16
|
+
* specific) AND `projectId === pack.meta.id`.
|
|
17
|
+
*
|
|
18
|
+
* This resolver unifies those signals behind ONE function so the four listing
|
|
19
|
+
* views (FEAT-7 filter) and the provenance pill (FEAT-8) never branch on a raw
|
|
20
|
+
* id shape, and so the pack-aware seed gate (BUG-6) has a single question to
|
|
21
|
+
* ask. Chosen over adding a persisted `packId` field because the prefix already
|
|
22
|
+
* encodes the answer AND is load-bearing for uninstall (`deleteAppCascade`) —
|
|
23
|
+
* a new field would force an uninstall rewrite + a backfill migration for zero
|
|
24
|
+
* functional gain (Principles #5/#7).
|
|
25
|
+
*
|
|
26
|
+
* PURE by design: the installed-pack set is passed in, never read here. That
|
|
27
|
+
* keeps the resolver testable without a filesystem/DB and usable both in a
|
|
28
|
+
* server component (pass `new Set(listApps().map(a => a.id))`) and client-side
|
|
29
|
+
* (pass a prefetched set). No I/O, no runtime-registry-adjacent imports.
|
|
30
|
+
*
|
|
31
|
+
* The installed-set gate is the whole point: a `--` id or a `projectId` alone
|
|
32
|
+
* is ambiguous — a user's hand-authored `my-notes--triage` profile or a normal
|
|
33
|
+
* project must NOT be mis-attributed to a pack. A candidate pack id is only
|
|
34
|
+
* returned when it is a member of the installed set.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
export type PackableKind = "profile" | "blueprint" | "table" | "schedule";
|
|
38
|
+
|
|
39
|
+
export interface PackablePrimitive {
|
|
40
|
+
kind: PackableKind;
|
|
41
|
+
/** The primitive's id: `<pack>--<name>` (files), a UUID (tables), or `app:<pack>:<sid>` (schedules). */
|
|
42
|
+
id: string;
|
|
43
|
+
/** DB column for tables/schedules; equals the pack id when pack-installed. Absent on file kinds. */
|
|
44
|
+
projectId?: string | null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Resolve the id of the pack that installed `primitive`, or `null` when it is
|
|
49
|
+
* not attributable to any installed pack. `installedPackIds` is the gate — a
|
|
50
|
+
* candidate is returned only if it is a member.
|
|
51
|
+
*/
|
|
52
|
+
export function packOf(
|
|
53
|
+
primitive: PackablePrimitive,
|
|
54
|
+
installedPackIds: ReadonlySet<string>
|
|
55
|
+
): string | null {
|
|
56
|
+
const candidate = candidatePackId(primitive);
|
|
57
|
+
if (candidate && installedPackIds.has(candidate)) return candidate;
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The best pack-id candidate from the primitive's own signals, BEFORE the
|
|
63
|
+
* installed-set gate. Order per kind is deliberate: the most specific,
|
|
64
|
+
* least-mutable signal wins (schedules prefer the composite id over the
|
|
65
|
+
* user-reassignable projectId).
|
|
66
|
+
*/
|
|
67
|
+
function candidatePackId(primitive: PackablePrimitive): string | null {
|
|
68
|
+
switch (primitive.kind) {
|
|
69
|
+
case "profile":
|
|
70
|
+
case "blueprint":
|
|
71
|
+
return extractAppIdFromArtifactId(primitive.id);
|
|
72
|
+
case "table":
|
|
73
|
+
return normalizeProjectId(primitive.projectId);
|
|
74
|
+
case "schedule": {
|
|
75
|
+
const fromId = parseAppScheduleId(primitive.id)?.appId ?? null;
|
|
76
|
+
return fromId ?? normalizeProjectId(primitive.projectId);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function normalizeProjectId(projectId: string | null | undefined): string | null {
|
|
82
|
+
return projectId && projectId.length > 0 ? projectId : null;
|
|
83
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pack-aware seed step (BUG-6).
|
|
3
|
+
*
|
|
4
|
+
* `clearAllData()` wipes ALL user tables — including the ones a paid pack
|
|
5
|
+
* materialized at install (e.g. Agency Pro's `Engagements` ledger). The generic
|
|
6
|
+
* seed generators only recreate demo tables, so after a seed the just-installed
|
|
7
|
+
* pack's cockpit reads an empty table ("No transactions yet") and every windowed
|
|
8
|
+
* KPI is zero. That is the reported symptom.
|
|
9
|
+
*
|
|
10
|
+
* Fix: after the generic seed, re-apply every installed pack via the SAME
|
|
11
|
+
* idempotent `installPack()` path that install uses. It rebuilds the pack's
|
|
12
|
+
* tables from its bundled manifest and re-seeds them from `seed/tables/*.json`
|
|
13
|
+
* (the ledger sample ships there). Reusing `installPack` — rather than
|
|
14
|
+
* hand-rolling create-table + addRows here — keeps the seed and install paths
|
|
15
|
+
* from ever drifting: whatever a pack ships as seed data, both surfaces produce
|
|
16
|
+
* identically.
|
|
17
|
+
*
|
|
18
|
+
* Best-effort per pack: one pack failing (e.g. an unlicensed premium pack whose
|
|
19
|
+
* license gate refuses) must not abort the whole seed. Failures are reported,
|
|
20
|
+
* never swallowed (engineering principle #1).
|
|
21
|
+
*
|
|
22
|
+
* `installPack` is runtime-registry-adjacent (CLAUDE.md smoke budget), so it is
|
|
23
|
+
* dynamically imported inside the function body, never a top-level static import.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
export interface PackReseedResult {
|
|
27
|
+
packId: string;
|
|
28
|
+
tablesCreated: number;
|
|
29
|
+
rowsSeeded: number;
|
|
30
|
+
error?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ReseedInstalledPacksOptions {
|
|
34
|
+
/** Override the apps dir scanned for installed packs (defaults to data dir). */
|
|
35
|
+
appsDir?: string;
|
|
36
|
+
/** Override where a bare pack id resolves to its bundled template (tests). */
|
|
37
|
+
templatesDir?: string;
|
|
38
|
+
/** Override the profiles drop dir (threaded to installPack). */
|
|
39
|
+
profilesDir?: string;
|
|
40
|
+
/** Override the blueprints drop dir (threaded to installPack). */
|
|
41
|
+
blueprintsDir?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Re-apply seed data for every installed pack. Returns one result per pack so
|
|
46
|
+
* the caller can fold the counts into the seed report and surface failures.
|
|
47
|
+
*
|
|
48
|
+
* Options exist mainly for tests (point `templatesDir`/`appsDir` at a fixture)
|
|
49
|
+
* and for non-default-data-dir instances; production passes none and the
|
|
50
|
+
* install machinery falls back to the configured data dir.
|
|
51
|
+
*/
|
|
52
|
+
export async function reseedInstalledPacks(
|
|
53
|
+
options: ReseedInstalledPacksOptions = {}
|
|
54
|
+
): Promise<PackReseedResult[]> {
|
|
55
|
+
const { listApps } = await import("@/lib/apps/registry");
|
|
56
|
+
const { installPack } = await import("@/lib/packs/install");
|
|
57
|
+
|
|
58
|
+
const installed = listApps(options.appsDir);
|
|
59
|
+
const results: PackReseedResult[] = [];
|
|
60
|
+
|
|
61
|
+
for (const app of installed) {
|
|
62
|
+
try {
|
|
63
|
+
// A bare pack id resolves to its bundled template (which carries the
|
|
64
|
+
// seed/tables/*.json files). installPack is idempotent: it reuses tables
|
|
65
|
+
// by name, dedupes rows by hash, and upserts schedules/profiles — so
|
|
66
|
+
// re-running it against an already-installed pack repopulates the tables
|
|
67
|
+
// clearAllData wiped and no-ops on everything already present.
|
|
68
|
+
const report = await installPack(app.id, {
|
|
69
|
+
appsDir: options.appsDir,
|
|
70
|
+
profilesDir: options.profilesDir,
|
|
71
|
+
blueprintsDir: options.blueprintsDir,
|
|
72
|
+
templatesDir: options.templatesDir,
|
|
73
|
+
});
|
|
74
|
+
results.push({
|
|
75
|
+
packId: app.id,
|
|
76
|
+
tablesCreated: report.tablesCreated,
|
|
77
|
+
rowsSeeded: report.rowsSeeded,
|
|
78
|
+
});
|
|
79
|
+
} catch (err) {
|
|
80
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
81
|
+
// Named, visible failure — a premium pack without a valid license, a
|
|
82
|
+
// core-version mismatch, or a malformed manifest lands here. The seed
|
|
83
|
+
// continues; the reason travels back in the report.
|
|
84
|
+
console.error(
|
|
85
|
+
`[seed] pack re-seed failed for "${app.id}": ${message}`
|
|
86
|
+
);
|
|
87
|
+
results.push({
|
|
88
|
+
packId: app.id,
|
|
89
|
+
tablesCreated: 0,
|
|
90
|
+
rowsSeeded: 0,
|
|
91
|
+
error: message,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return results;
|
|
97
|
+
}
|
package/src/lib/data/seed.ts
CHANGED
|
@@ -373,10 +373,34 @@ export async function seedSampleData() {
|
|
|
373
373
|
tableIds: tableIdList,
|
|
374
374
|
});
|
|
375
375
|
|
|
376
|
+
// 25. Pack-aware seed — repopulate every installed pack's tables (BUG-6).
|
|
377
|
+
// clearAllData() wiped the pack tables the customer just installed; re-apply
|
|
378
|
+
// each pack's bundled seed data via the idempotent install path so e.g. the
|
|
379
|
+
// Agency Pro ledger cockpit reads non-zero instead of "No transactions yet".
|
|
380
|
+
const { reseedInstalledPacks } = await import(
|
|
381
|
+
"./seed-data/installed-packs"
|
|
382
|
+
);
|
|
383
|
+
const packReseeds = await reseedInstalledPacks();
|
|
384
|
+
const packTablesSeeded = packReseeds.reduce(
|
|
385
|
+
(sum, p) => sum + p.tablesCreated,
|
|
386
|
+
0
|
|
387
|
+
);
|
|
388
|
+
const packRowsSeeded = packReseeds.reduce((sum, p) => sum + p.rowsSeeded, 0);
|
|
389
|
+
const packReseedErrors = packReseeds.filter((p) => p.error);
|
|
390
|
+
|
|
376
391
|
// Quiet the unused-`now` flag; the helpers above reuse Date.now() inline.
|
|
377
392
|
void now;
|
|
378
393
|
|
|
379
394
|
return {
|
|
395
|
+
packsReseeded: packReseeds.length,
|
|
396
|
+
packTablesSeeded,
|
|
397
|
+
packRowsSeeded,
|
|
398
|
+
// Surface any per-pack failures (unlicensed premium pack, bad manifest)
|
|
399
|
+
// rather than swallowing them — the route/UI can show what didn't seed.
|
|
400
|
+
packReseedErrors: packReseedErrors.map((p) => ({
|
|
401
|
+
packId: p.packId,
|
|
402
|
+
error: p.error as string,
|
|
403
|
+
})),
|
|
380
404
|
profiles: profileCount,
|
|
381
405
|
projects: projectSeeds.length,
|
|
382
406
|
tasks: taskSeeds.length,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
id: relay-agency-pro
|
|
2
|
-
version: "0.
|
|
2
|
+
version: "0.4.0"
|
|
3
3
|
name: Relay Agency Pro
|
|
4
4
|
description: >
|
|
5
5
|
The agency operating system on top of the free Relay Agency verbs: a
|
|
@@ -54,10 +54,16 @@ tables:
|
|
|
54
54
|
# The engagements ledger: one row per billing/cost line, signed amount
|
|
55
55
|
# (+billing, -cost), so the ledger kit's inflow/outflow/margin math works
|
|
56
56
|
# without configuration. The month-end close writes draft invoice lines here.
|
|
57
|
+
# SEEDED: seed/tables/engagements.json ships a current-month sample ledger so
|
|
58
|
+
# the finance cockpit's MTD KPIs (billed/costs/margin) read non-zero the moment
|
|
59
|
+
# the pack installs. Only THIS table is seeded — intake/grants below carry
|
|
60
|
+
# row-insert triggers, and seeding them would dispatch their pipeline
|
|
61
|
+
# blueprints on install/seed (see src/lib/data/tables.ts addRows). Work queues
|
|
62
|
+
# are meant to be filled by the operator to TRIGGER work, so they ship empty.
|
|
57
63
|
- id: engagements
|
|
58
64
|
columns: [client, date, category, description, amount, status]
|
|
59
65
|
# The intake work queue: drop a row, the intake pipeline fires (row-insert
|
|
60
|
-
# trigger) and routes by `kind` under the named client.
|
|
66
|
+
# trigger) and routes by `kind` under the named client. Ships empty by design.
|
|
61
67
|
- id: intake
|
|
62
68
|
columns: [client, kind, source, status, notes]
|
|
63
69
|
# The grant pipeline: drop an opportunity row, the deep grant pipeline
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
[
|
|
2
|
+
{ "client": "Meridian Commercial Realty", "date": "2026-07-01", "category": "Retainer", "description": "July retainer — monthly CRE marketing", "amount": "1200", "status": "billed" },
|
|
3
|
+
{ "client": "Meridian Commercial Realty", "date": "2026-07-02", "category": "Project", "description": "Listing photography package — 3 properties", "amount": "2400", "status": "billed" },
|
|
4
|
+
{ "client": "Meridian Commercial Realty", "date": "2026-07-02", "category": "Labor", "description": "Account team hours — Meridian", "amount": "-1450", "status": "posted" },
|
|
5
|
+
{ "client": "Meridian Commercial Realty", "date": "2026-07-02", "category": "AI cost", "description": "Prospect research + proposal drafting agents", "amount": "-190", "status": "posted" },
|
|
6
|
+
{ "client": "Summit CRE Advisors", "date": "2026-07-01", "category": "Retainer", "description": "July retainer — advisory + content", "amount": "600", "status": "billed" },
|
|
7
|
+
{ "client": "Summit CRE Advisors", "date": "2026-07-03", "category": "Project", "description": "Q3 market report production", "amount": "1800", "status": "billed" },
|
|
8
|
+
{ "client": "Summit CRE Advisors", "date": "2026-07-03", "category": "Labor", "description": "Analyst + editor hours — market report", "amount": "-980", "status": "posted" },
|
|
9
|
+
{ "client": "Summit CRE Advisors", "date": "2026-07-03", "category": "AI cost", "description": "Market-report research agent runs", "amount": "-140", "status": "posted" },
|
|
10
|
+
{ "client": "Parkview Property Management", "date": "2026-07-01", "category": "Retainer", "description": "July retainer — leasing marketing", "amount": "500", "status": "billed" },
|
|
11
|
+
{ "client": "Parkview Property Management", "date": "2026-07-04", "category": "Project", "description": "Tenant renewal campaign build", "amount": "1500", "status": "draft" },
|
|
12
|
+
{ "client": "Parkview Property Management", "date": "2026-07-02", "category": "Subcontractor", "description": "Freelance copywriter — renewal emails", "amount": "-350", "status": "posted" },
|
|
13
|
+
{ "client": "Parkview Property Management", "date": "2026-07-02", "category": "Labor", "description": "Campaign build hours", "amount": "-720", "status": "posted" },
|
|
14
|
+
{ "client": "Community Impact Alliance", "date": "2026-07-01", "category": "Retainer", "description": "July retainer — grants + comms", "amount": "450", "status": "billed" },
|
|
15
|
+
{ "client": "Community Impact Alliance", "date": "2026-07-03", "category": "Project", "description": "Annual report design + copy", "amount": "2200", "status": "billed" },
|
|
16
|
+
{ "client": "Community Impact Alliance", "date": "2026-07-03", "category": "Labor", "description": "Design + copy hours — annual report", "amount": "-1180", "status": "posted" },
|
|
17
|
+
{ "client": "Community Impact Alliance", "date": "2026-07-03", "category": "AI cost", "description": "Grant-fit scoring + LOI drafting agents", "amount": "-160", "status": "posted" },
|
|
18
|
+
{ "client": "Cornerstone Community Foundation", "date": "2026-07-01", "category": "Retainer", "description": "July retainer — donor communications", "amount": "400", "status": "billed" },
|
|
19
|
+
{ "client": "Cornerstone Community Foundation", "date": "2026-07-04", "category": "Project", "description": "Year-end giving campaign kickoff", "amount": "1900", "status": "draft" },
|
|
20
|
+
{ "client": "Cornerstone Community Foundation", "date": "2026-07-02", "category": "Labor", "description": "Strategy + kickoff hours", "amount": "-850", "status": "posted" },
|
|
21
|
+
{ "client": "Cornerstone Community Foundation", "date": "2026-07-02", "category": "Software", "description": "Email platform pass-through — July", "amount": "-90", "status": "posted" },
|
|
22
|
+
{ "client": "Lakeshore Family Services", "date": "2026-07-01", "category": "Retainer", "description": "July retainer — program marketing", "amount": "200", "status": "billed" },
|
|
23
|
+
{ "client": "Lakeshore Family Services", "date": "2026-07-02", "category": "Project", "description": "Volunteer recruitment landing page", "amount": "800", "status": "billed" },
|
|
24
|
+
{ "client": "Lakeshore Family Services", "date": "2026-07-02", "category": "Labor", "description": "Landing page build hours", "amount": "-410", "status": "posted" },
|
|
25
|
+
{ "client": "Lakeshore Family Services", "date": "2026-07-02", "category": "AI cost", "description": "Intake routing + response drafting", "amount": "-70", "status": "posted" },
|
|
26
|
+
{ "client": "Agency overhead", "date": "2026-07-01", "category": "Software", "description": "Design + scheduling tool subscriptions", "amount": "-420", "status": "posted" },
|
|
27
|
+
{ "client": "Agency overhead", "date": "2026-07-01", "category": "AI cost", "description": "Month-end close automation runs", "amount": "-110", "status": "posted" }
|
|
28
|
+
]
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
id: relay-agency-pro
|
|
2
|
-
version: "0.
|
|
2
|
+
version: "0.4.0"
|
|
3
3
|
name: Relay Agency Pro
|
|
4
4
|
author: Orionfold
|
|
5
5
|
# This description renders as the what-you-get preview on the locked /packs
|
|
@@ -65,4 +65,8 @@ changelog:
|
|
|
65
65
|
A new home screen that shows all six workflows as cards you can run with
|
|
66
66
|
one click. It tells you where to start, and each card shows its last run.
|
|
67
67
|
No more hunting through menus to find what your app can do.
|
|
68
|
+
"0.4.0": >-
|
|
69
|
+
Your finance cockpit now arrives with a month of sample billing and cost
|
|
70
|
+
entries, so you see real numbers the moment you install. Billed, costs, and
|
|
71
|
+
margin light up right away instead of a blank ledger.
|
|
68
72
|
customers: []
|
|
@@ -53,7 +53,7 @@ import type { ScheduleSpec } from "@/lib/validators/schedule-spec";
|
|
|
53
53
|
// unfixed from 0.15.0 through 0.16.0 — treat the window test's failure as
|
|
54
54
|
// a release blocker, not noise). The 0.13→0.14 three-MINOR bridge is over;
|
|
55
55
|
// this is the standard 2-MINOR window now.
|
|
56
|
-
const SUPPORTED_API_VERSIONS = new Set([CURRENT_PLUGIN_API_VERSION, "0.
|
|
56
|
+
const SUPPORTED_API_VERSIONS = new Set([CURRENT_PLUGIN_API_VERSION, "0.26"]);
|
|
57
57
|
|
|
58
58
|
/** Test-helper export so the window-enforcement test can read state. */
|
|
59
59
|
export function isSupportedApiVersion(apiVersion: string): boolean {
|
|
@@ -6,7 +6,7 @@ import { z } from "zod";
|
|
|
6
6
|
// (a hardcoded copy there once drifted to "0.14" — scaffolded plugins would
|
|
7
7
|
// have been disabled on load the moment the window tightened). Bump on every
|
|
8
8
|
// MINOR release; api-version-window.test.ts fails if this goes stale.
|
|
9
|
-
export const CURRENT_PLUGIN_API_VERSION = "0.
|
|
9
|
+
export const CURRENT_PLUGIN_API_VERSION = "0.27";
|
|
10
10
|
|
|
11
11
|
// Shared capability tuple — single source of truth used by Zod schema and
|
|
12
12
|
// capability-check.ts hash derivation. Exported so consumers don't need a
|