@netnodeag/kraftwerk 0.79.0 → 0.80.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/README.md +83 -0
- package/dist/cli/daemon.js +10 -9
- package/dist/cli/doctor.js +1 -1
- package/dist/cli/entities.d.ts +8 -0
- package/dist/cli/entities.js +222 -0
- package/dist/cli/init.js +3 -0
- package/dist/cli/kraftwerk.js +7 -0
- package/dist/cli/ui.js +2 -1
- package/dist/client/routes.d.ts +16 -0
- package/dist/client/routes.js +16 -0
- package/dist/config.d.ts +25 -0
- package/dist/config.js +17 -6
- package/dist/core/chat/context.js +5 -2
- package/dist/core/entities.d.ts +171 -0
- package/dist/core/entities.js +441 -0
- package/dist/core/git.js +3 -1
- package/dist/core/journal.d.ts +12 -2
- package/dist/core/journal.js +50 -5
- package/dist/core/log.d.ts +18 -0
- package/dist/core/log.js +23 -0
- package/dist/core/projects.d.ts +22 -33
- package/dist/core/projects.js +40 -134
- package/dist/core/records.d.ts +28 -0
- package/dist/core/records.js +71 -0
- package/dist/core/refs.d.ts +74 -0
- package/dist/core/refs.js +143 -0
- package/dist/core/search.d.ts +10 -0
- package/dist/core/search.js +12 -0
- package/dist/core/settings.d.ts +4 -0
- package/dist/core/settings.js +24 -3
- package/dist/core/slug.d.ts +9 -0
- package/dist/core/slug.js +18 -0
- package/dist/core/trash.d.ts +1 -1
- package/dist/server/api/devices.js +2 -1
- package/dist/server/api/entities.d.ts +74 -0
- package/dist/server/api/entities.js +60 -0
- package/dist/server/api/index.d.ts +66 -4
- package/dist/server/api/index.js +5 -0
- package/dist/server/api/projects.d.ts +3 -0
- package/dist/server/api/projects.js +3 -0
- package/dist/server/api/workspace.d.ts +15 -0
- package/dist/server/api/workspace.js +6 -0
- package/dist/server/cloud.js +3 -2
- package/dist/server/relay.d.ts +3 -2
- package/dist/server/relay.js +7 -7
- package/dist/server/server.js +3 -2
- package/package.json +1 -1
- package/schema/kraftwerk.schema.json +54 -0
- package/web/dist/assets/{dist-cNijUfNl.js → dist-1PajaYPg.js} +1 -1
- package/web/dist/assets/{dist-BTD8k55t.js → dist-B4dqwo7_.js} +1 -1
- package/web/dist/assets/dist-Bp1iMuQ7.js +1 -0
- package/web/dist/assets/{dist-A0LN_JdM.js → dist-CK2u1OXb.js} +1 -1
- package/web/dist/assets/{dist-DdOsafun.js → dist-D-f0nszo.js} +1 -1
- package/web/dist/assets/{dist-xLZN2eGO.js → dist-DKduisGW.js} +1 -1
- package/web/dist/assets/{dist-BasQkRXA.js → dist-DgKgeYFJ.js} +1 -1
- package/web/dist/assets/{dist-wtIUmbnn.js → dist-DlZPeneT.js} +1 -1
- package/web/dist/assets/{dist-E48sO9Gc.js → dist-K_3Y5Nbl.js} +1 -1
- package/web/dist/assets/{dist-DgP31lcC.js → dist-NX4_yvho.js} +1 -1
- package/web/dist/assets/{dist-D93lSM0a.js → dist-u_oAKIhZ.js} +1 -1
- package/web/dist/assets/{editor-BQlHN7qT.js → editor-CE9TlKkJ.js} +3 -3
- package/web/dist/assets/index-Dvt-vE1h.css +2 -0
- package/web/dist/assets/index-IAuwD22j.js +231 -0
- package/web/dist/index.html +2 -2
- package/web/dist/assets/dist-B04mMuvH.js +0 -1
- package/web/dist/assets/index-BKDJocDd.css +0 -2
- package/web/dist/assets/index-C7ddpWNC.js +0 -231
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The reference registry: everything in a workspace that can be pointed
|
|
3
|
+
* at by name — knowledge bundles, vibeables, repositories, workflows,
|
|
4
|
+
* agents, projects, contacts, places and events. Each kind registers only
|
|
5
|
+
* a lookup (its storage and routes stay where they are); one resolver then
|
|
6
|
+
* serves every place a reference appears: a project's link lists, an
|
|
7
|
+
* event's place and contacts, a `contact:anna-meier` token in text, the
|
|
8
|
+
* ⌘K palette and the context an agent starts with.
|
|
9
|
+
*
|
|
10
|
+
* A reference is `<token>:<slug>` — the kind's singular token (contact,
|
|
11
|
+
* place, event, project, agent, workflow, repo, vibeable, knowledge) and
|
|
12
|
+
* the target's slug. Only registered tokens match, so `https:` and the
|
|
13
|
+
* like never do. A target that does not exist is reported (found: false),
|
|
14
|
+
* never an error — the same rule project links have always followed.
|
|
15
|
+
*/
|
|
16
|
+
export declare const REF_KIND_NAMES: readonly ["knowledge", "vibeables", "repos", "workflows", "agents", "projects", "contacts", "places", "events"];
|
|
17
|
+
/** The kind's plural: the link-list name in project.yml and the folder/route name. */
|
|
18
|
+
export type RefKind = (typeof REF_KIND_NAMES)[number];
|
|
19
|
+
/** One existing target. */
|
|
20
|
+
export interface RefTarget {
|
|
21
|
+
slug: string;
|
|
22
|
+
/** Display text next to the slug (an agent's name, a bundle's size); "" when there is nothing to add. */
|
|
23
|
+
label: string;
|
|
24
|
+
/** Where the web UI opens it (a hash route). */
|
|
25
|
+
href: string;
|
|
26
|
+
}
|
|
27
|
+
export interface RefKindDef {
|
|
28
|
+
kind: RefKind;
|
|
29
|
+
/** The token in text: contact in contact:anna-meier. */
|
|
30
|
+
token: string;
|
|
31
|
+
/** How the kind reads in a sentence ("knowledge bundle", "contact"). */
|
|
32
|
+
noun: string;
|
|
33
|
+
/** Every target that exists right now; [] when the feature is off. */
|
|
34
|
+
list(): Promise<RefTarget[]>;
|
|
35
|
+
}
|
|
36
|
+
/** One referenced slug and whether its target exists right now. */
|
|
37
|
+
export interface LinkState {
|
|
38
|
+
slug: string;
|
|
39
|
+
found: boolean;
|
|
40
|
+
/** Display name of the target when found (an agent's name, a workflow's description). */
|
|
41
|
+
label?: string;
|
|
42
|
+
}
|
|
43
|
+
/** A `kind:slug` token, resolved. */
|
|
44
|
+
export interface ResolvedRef extends LinkState {
|
|
45
|
+
kind: RefKind;
|
|
46
|
+
token: string;
|
|
47
|
+
href: string;
|
|
48
|
+
}
|
|
49
|
+
export declare const REF_KINDS: readonly RefKindDef[];
|
|
50
|
+
/** The kind registered under a plural name or a token; undefined for anything else. */
|
|
51
|
+
export declare const refKind: (name: string) => RefKindDef | undefined;
|
|
52
|
+
/** Which of the slugs exist right now, per kind. Kinds without slugs are not listed at all. */
|
|
53
|
+
export declare function resolveRefs<K extends RefKind>(byKind: Record<K, string[]>): Promise<Record<K, LinkState[]>>;
|
|
54
|
+
/** A `token:slug` reference in text: a registered token, a colon, a slug — not inside a word, a path or a URL. */
|
|
55
|
+
export declare const REF_TOKEN_RE: RegExp;
|
|
56
|
+
/** Every distinct reference in a text, in order of appearance. */
|
|
57
|
+
export declare function findRefs(text: string): Array<{
|
|
58
|
+
token: string;
|
|
59
|
+
kind: RefKind;
|
|
60
|
+
slug: string;
|
|
61
|
+
}>;
|
|
62
|
+
/** Parse one reference ("contact:anna-meier"); null when it is not one. */
|
|
63
|
+
export declare function parseRef(text: string): {
|
|
64
|
+
token: string;
|
|
65
|
+
kind: RefKind;
|
|
66
|
+
slug: string;
|
|
67
|
+
} | null;
|
|
68
|
+
/** Resolve references: each with whether its target exists and where it opens. */
|
|
69
|
+
export declare function resolveTokens(refs: Array<{
|
|
70
|
+
kind: RefKind;
|
|
71
|
+
slug: string;
|
|
72
|
+
}>): Promise<ResolvedRef[]>;
|
|
73
|
+
/** Resolve one reference written as text; null when the text is not a reference. */
|
|
74
|
+
export declare function resolveRef(text: string): Promise<ResolvedRef | null>;
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { listAgents } from "./agents.js";
|
|
2
|
+
import { CONTACTS, EVENTS, listEntities, PLACES } from "./entities.js";
|
|
3
|
+
import { knowledgeIndex } from "./knowledge.js";
|
|
4
|
+
import { listProjects } from "./projects.js";
|
|
5
|
+
import { listRepos } from "./repos.js";
|
|
6
|
+
import { listVibeables } from "./vibeables.js";
|
|
7
|
+
import { listWorkflows } from "./workflows.js";
|
|
8
|
+
/**
|
|
9
|
+
* The reference registry: everything in a workspace that can be pointed
|
|
10
|
+
* at by name — knowledge bundles, vibeables, repositories, workflows,
|
|
11
|
+
* agents, projects, contacts, places and events. Each kind registers only
|
|
12
|
+
* a lookup (its storage and routes stay where they are); one resolver then
|
|
13
|
+
* serves every place a reference appears: a project's link lists, an
|
|
14
|
+
* event's place and contacts, a `contact:anna-meier` token in text, the
|
|
15
|
+
* ⌘K palette and the context an agent starts with.
|
|
16
|
+
*
|
|
17
|
+
* A reference is `<token>:<slug>` — the kind's singular token (contact,
|
|
18
|
+
* place, event, project, agent, workflow, repo, vibeable, knowledge) and
|
|
19
|
+
* the target's slug. Only registered tokens match, so `https:` and the
|
|
20
|
+
* like never do. A target that does not exist is reported (found: false),
|
|
21
|
+
* never an error — the same rule project links have always followed.
|
|
22
|
+
*/
|
|
23
|
+
export const REF_KIND_NAMES = ["knowledge", "vibeables", "repos", "workflows", "agents", "projects", "contacts", "places", "events"];
|
|
24
|
+
/** A list that fails to read is an empty list: a reference into it is "not found", never an error. */
|
|
25
|
+
const safe = async (p, fallback) => p.catch(() => fallback);
|
|
26
|
+
export const REF_KINDS = [
|
|
27
|
+
{
|
|
28
|
+
kind: "knowledge",
|
|
29
|
+
token: "knowledge",
|
|
30
|
+
noun: "knowledge bundle",
|
|
31
|
+
list: async () => (await safe(knowledgeIndex(), { bundles: [] })).bundles.map((b) => ({ slug: b.name, label: `${b.concepts} concepts`, href: `/knowledge/${b.name}` })),
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
kind: "vibeables",
|
|
35
|
+
token: "vibeable",
|
|
36
|
+
noun: "vibeable",
|
|
37
|
+
list: async () => (await safe(listVibeables(), { vibeables: [] })).vibeables.map((v) => ({ slug: v.slug, label: v.dev ? "dev" : "static", href: `/vibeables/${v.slug}` })),
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
kind: "repos",
|
|
41
|
+
token: "repo",
|
|
42
|
+
noun: "repository",
|
|
43
|
+
list: async () => (await safe(listRepos(), { repos: [] })).repos.map((r) => ({ slug: r.slug, label: r.branch ?? "", href: `/repos/${r.slug}` })),
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
kind: "workflows",
|
|
47
|
+
token: "workflow",
|
|
48
|
+
noun: "workflow",
|
|
49
|
+
list: async () => (await safe(listWorkflows(), { workflows: [] })).workflows.map((w) => ({ slug: w.slug, label: w.description ?? w.name ?? "", href: `/workflows/${w.slug}` })),
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
kind: "agents",
|
|
53
|
+
token: "agent",
|
|
54
|
+
noun: "agent",
|
|
55
|
+
list: async () => (await safe(listAgents(), []))
|
|
56
|
+
.filter((a) => !a.archived)
|
|
57
|
+
.map((a) => ({ slug: a.slug, label: `${a.emoji} ${a.name}`, href: `/agents/${a.slug}` })),
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
kind: "projects",
|
|
61
|
+
token: "project",
|
|
62
|
+
noun: "project",
|
|
63
|
+
list: async () => (await safe(listProjects(), { projects: [] })).projects.map((p) => ({ slug: p.slug, label: p.title, href: `/projects/${p.slug}` })),
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
kind: "contacts",
|
|
67
|
+
token: "contact",
|
|
68
|
+
noun: "contact",
|
|
69
|
+
list: async () => (await safe(listEntities(CONTACTS), { items: [] })).items.map((c) => ({ slug: c.slug, label: CONTACTS.describe(c), href: `/contacts/${c.slug}` })),
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
kind: "places",
|
|
73
|
+
token: "place",
|
|
74
|
+
noun: "place",
|
|
75
|
+
list: async () => (await safe(listEntities(PLACES), { items: [] })).items.map((p) => ({ slug: p.slug, label: PLACES.describe(p), href: `/places/${p.slug}` })),
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
kind: "events",
|
|
79
|
+
token: "event",
|
|
80
|
+
noun: "event",
|
|
81
|
+
list: async () => (await safe(listEntities(EVENTS), { items: [] })).items.map((e) => ({ slug: e.slug, label: EVENTS.describe(e), href: `/events/${e.slug}` })),
|
|
82
|
+
},
|
|
83
|
+
];
|
|
84
|
+
const byKind = new Map(REF_KINDS.map((d) => [d.kind, d]));
|
|
85
|
+
const byToken = new Map(REF_KINDS.map((d) => [d.token, d]));
|
|
86
|
+
/** The kind registered under a plural name or a token; undefined for anything else. */
|
|
87
|
+
export const refKind = (name) => byKind.get(name) ?? byToken.get(name);
|
|
88
|
+
/** The link states of some slugs against one list of targets. */
|
|
89
|
+
const states = (slugs, targets) => slugs.map((slug) => {
|
|
90
|
+
const t = targets.find((x) => x.slug === slug);
|
|
91
|
+
return t ? { slug, found: true, ...(t.label ? { label: t.label } : {}) } : { slug, found: false };
|
|
92
|
+
});
|
|
93
|
+
/** Which of the slugs exist right now, per kind. Kinds without slugs are not listed at all. */
|
|
94
|
+
export async function resolveRefs(byKind) {
|
|
95
|
+
const out = {};
|
|
96
|
+
await Promise.all(Object.keys(byKind).map(async (kind) => {
|
|
97
|
+
const slugs = byKind[kind];
|
|
98
|
+
const def = refKind(kind);
|
|
99
|
+
out[kind] = !def || slugs.length === 0 ? [] : states(slugs, await def.list());
|
|
100
|
+
}));
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
103
|
+
const SLUG = "[a-z0-9][a-z0-9-]{0,48}";
|
|
104
|
+
/** A `token:slug` reference in text: a registered token, a colon, a slug — not inside a word, a path or a URL. */
|
|
105
|
+
export const REF_TOKEN_RE = new RegExp(`(?<![\\w/:.@-])(${REF_KINDS.map((d) => d.token).join("|")}):(${SLUG})(?![\\w-])`, "g");
|
|
106
|
+
/** Every distinct reference in a text, in order of appearance. */
|
|
107
|
+
export function findRefs(text) {
|
|
108
|
+
const seen = new Set();
|
|
109
|
+
const out = [];
|
|
110
|
+
for (const m of text.matchAll(REF_TOKEN_RE)) {
|
|
111
|
+
const token = m[0];
|
|
112
|
+
if (seen.has(token))
|
|
113
|
+
continue;
|
|
114
|
+
seen.add(token);
|
|
115
|
+
out.push({ token, kind: byToken.get(m[1]).kind, slug: m[2] });
|
|
116
|
+
}
|
|
117
|
+
return out;
|
|
118
|
+
}
|
|
119
|
+
/** Parse one reference ("contact:anna-meier"); null when it is not one. */
|
|
120
|
+
export function parseRef(text) {
|
|
121
|
+
const m = new RegExp(`^(${REF_KINDS.map((d) => d.token).join("|")}|${REF_KIND_NAMES.join("|")}):(${SLUG})$`).exec(text.trim());
|
|
122
|
+
if (!m)
|
|
123
|
+
return null;
|
|
124
|
+
const def = refKind(m[1]);
|
|
125
|
+
return { token: `${def.token}:${m[2]}`, kind: def.kind, slug: m[2] };
|
|
126
|
+
}
|
|
127
|
+
/** Resolve references: each with whether its target exists and where it opens. */
|
|
128
|
+
export async function resolveTokens(refs) {
|
|
129
|
+
const kinds = [...new Set(refs.map((r) => r.kind))];
|
|
130
|
+
const lists = new Map(await Promise.all(kinds.map(async (k) => [k, await refKind(k).list()])));
|
|
131
|
+
return refs.map((r) => {
|
|
132
|
+
const def = refKind(r.kind);
|
|
133
|
+
const t = lists.get(r.kind).find((x) => x.slug === r.slug);
|
|
134
|
+
return { kind: r.kind, token: `${def.token}:${r.slug}`, slug: r.slug, found: !!t, ...(t?.label ? { label: t.label } : {}), href: t?.href ?? `/${r.kind}/${r.slug}` };
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
/** Resolve one reference written as text; null when the text is not a reference. */
|
|
138
|
+
export async function resolveRef(text) {
|
|
139
|
+
const ref = parseRef(text);
|
|
140
|
+
if (!ref)
|
|
141
|
+
return null;
|
|
142
|
+
return (await resolveTokens([ref]))[0];
|
|
143
|
+
}
|
package/dist/core/search.d.ts
CHANGED
|
@@ -25,6 +25,16 @@ export interface WorkspaceAgents {
|
|
|
25
25
|
channels: ChannelSummary[];
|
|
26
26
|
/** Projects that are not archived; empty when the feature is off. */
|
|
27
27
|
projects: ProjectHit[];
|
|
28
|
+
/** Contacts, places and events of this instance (the registry does not carry them); empty for other workspaces and while the features are off. */
|
|
29
|
+
entities: EntityHit[];
|
|
30
|
+
}
|
|
31
|
+
/** What the palette needs of a contact, place or event. */
|
|
32
|
+
export interface EntityHit {
|
|
33
|
+
kind: "contacts" | "places" | "events";
|
|
34
|
+
slug: string;
|
|
35
|
+
title: string;
|
|
36
|
+
/** The rest of its one-line description (role and org, address, date). */
|
|
37
|
+
detail?: string;
|
|
28
38
|
}
|
|
29
39
|
export interface AgentSearch {
|
|
30
40
|
workspaces: WorkspaceAgents[];
|
package/dist/core/search.js
CHANGED
|
@@ -4,7 +4,17 @@ import { workspaceRoot } from "./context.js";
|
|
|
4
4
|
import { listAgents, toSummary } from "./agents.js";
|
|
5
5
|
import { listChannels, toChannelSummary } from "./channels.js";
|
|
6
6
|
import { currentInstanceUrl, discoverWorkspaces, listWorkspaceRecords } from "./instances.js";
|
|
7
|
+
import { CONTACTS, EVENTS, listEntities, PLACES } from "./entities.js";
|
|
7
8
|
import { listProjects, toProjectHit } from "./projects.js";
|
|
9
|
+
async function entityHits(def) {
|
|
10
|
+
const view = await listEntities(def).catch(() => ({ items: [] }));
|
|
11
|
+
return view.items
|
|
12
|
+
.filter((e) => !e.configError)
|
|
13
|
+
.map((e) => {
|
|
14
|
+
const detail = def.detail(e);
|
|
15
|
+
return { kind: def.kind, slug: e.slug, title: e.title, ...(detail ? { detail } : {}) };
|
|
16
|
+
});
|
|
17
|
+
}
|
|
8
18
|
export async function searchAgents() {
|
|
9
19
|
const project = await resolveWorkspace(workspaceRoot()).catch(() => null);
|
|
10
20
|
const self = {
|
|
@@ -17,6 +27,7 @@ export async function searchAgents() {
|
|
|
17
27
|
agents: (await listAgents().catch(() => [])).filter((a) => !a.archived).map(toSummary),
|
|
18
28
|
channels: (await listChannels().catch(() => [])).map(toChannelSummary),
|
|
19
29
|
projects: (await listProjects().catch(() => ({ projects: [] }))).projects.filter((p) => p.status !== "archived").map(toProjectHit),
|
|
30
|
+
entities: (await Promise.all([CONTACTS, PLACES, EVENTS].map(entityHits))).flat(),
|
|
20
31
|
};
|
|
21
32
|
const [workspaces, known] = await Promise.all([discoverWorkspaces(), listWorkspaceRecords()]);
|
|
22
33
|
const records = new Map(known.map((p) => [p.root, p]));
|
|
@@ -33,6 +44,7 @@ export async function searchAgents() {
|
|
|
33
44
|
agents: records.get(w.root)?.agents ?? [],
|
|
34
45
|
channels: records.get(w.root)?.channels ?? [],
|
|
35
46
|
projects: records.get(w.root)?.projects ?? [],
|
|
47
|
+
entities: [],
|
|
36
48
|
}));
|
|
37
49
|
return { workspaces: [self, ...others] };
|
|
38
50
|
}
|
package/dist/core/settings.d.ts
CHANGED
|
@@ -32,6 +32,10 @@ export interface SaveSettingsInput {
|
|
|
32
32
|
vibeables?: ReposSettingsInput;
|
|
33
33
|
/** Omitted = untouched. Same shape and rules as repos. */
|
|
34
34
|
projects?: ReposSettingsInput;
|
|
35
|
+
/** Omitted = untouched. Same shape and rules as repos. */
|
|
36
|
+
contacts?: ReposSettingsInput;
|
|
37
|
+
places?: ReposSettingsInput;
|
|
38
|
+
events?: ReposSettingsInput;
|
|
35
39
|
}
|
|
36
40
|
export interface ReposSettingsInput {
|
|
37
41
|
enabled: boolean;
|
package/dist/core/settings.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { promises as fs } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { parseDocument } from "yaml";
|
|
4
|
-
import { ignoreEntryFor, PROJECTS_DEFAULT_ROOT, REPOS_DEFAULT_ROOT, resolveWorkspace, VIBEABLES_DEFAULT_ROOT, isSafeGitName } from "../config.js";
|
|
4
|
+
import { ENTITY_KINDS, entityDefaultRoot, ignoreEntryFor, PROJECTS_DEFAULT_ROOT, REPOS_DEFAULT_ROOT, resolveWorkspace, VIBEABLES_DEFAULT_ROOT, isSafeGitName } from "../config.js";
|
|
5
|
+
import { ensureEntitiesRoot } from "./entities.js";
|
|
5
6
|
import { ensureProjectsRoot } from "./projects.js";
|
|
6
7
|
import { disposeAllDevs, ensureVibeablesRoot } from "./vibeables.js";
|
|
7
8
|
import { workspaceRoot } from "./context.js";
|
|
@@ -76,8 +77,15 @@ function cleanGit(value) {
|
|
|
76
77
|
return null;
|
|
77
78
|
return { enabled: false, ...out };
|
|
78
79
|
}
|
|
79
|
-
const ROOT_DEFAULTS = {
|
|
80
|
-
|
|
80
|
+
const ROOT_DEFAULTS = {
|
|
81
|
+
repos: REPOS_DEFAULT_ROOT,
|
|
82
|
+
vibeables: VIBEABLES_DEFAULT_ROOT,
|
|
83
|
+
projects: PROJECTS_DEFAULT_ROOT,
|
|
84
|
+
contacts: entityDefaultRoot("contacts"),
|
|
85
|
+
places: entityDefaultRoot("places"),
|
|
86
|
+
events: entityDefaultRoot("events"),
|
|
87
|
+
};
|
|
88
|
+
/** The repos, vibeables, projects, contacts, places or events block to write; null when the feature is off and nothing else is set. */
|
|
81
89
|
function cleanRootBlock(block, value, projectRoot) {
|
|
82
90
|
if (typeof value !== "object" || value === null)
|
|
83
91
|
throw new Error(`${block} must be an object`);
|
|
@@ -163,6 +171,16 @@ export async function saveSettings(input) {
|
|
|
163
171
|
else
|
|
164
172
|
doc.set("projects", projects);
|
|
165
173
|
}
|
|
174
|
+
for (const kind of ENTITY_KINDS) {
|
|
175
|
+
const given = input[kind];
|
|
176
|
+
if (given === undefined)
|
|
177
|
+
continue;
|
|
178
|
+
const block = cleanRootBlock(kind, given, project.root);
|
|
179
|
+
if (block === null)
|
|
180
|
+
doc.delete(kind);
|
|
181
|
+
else
|
|
182
|
+
doc.set(kind, block);
|
|
183
|
+
}
|
|
166
184
|
await fs.writeFile(configPath, doc.toString());
|
|
167
185
|
// Turning repositories on prepares the folder so the first clone and the
|
|
168
186
|
// workspace git both find it in the right state.
|
|
@@ -172,5 +190,8 @@ export async function saveSettings(input) {
|
|
|
172
190
|
await ensureVibeablesRoot().catch(() => { });
|
|
173
191
|
if (input.projects?.enabled)
|
|
174
192
|
await ensureProjectsRoot().catch(() => { });
|
|
193
|
+
for (const kind of ENTITY_KINDS)
|
|
194
|
+
if (input[kind]?.enabled)
|
|
195
|
+
await ensureEntitiesRoot(kind).catch(() => { });
|
|
175
196
|
return getSettings();
|
|
176
197
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one slug rule for folders and files kraftwerk names after a title:
|
|
3
|
+
* lowercase letters, digits and dashes, at most 49 characters, starting
|
|
4
|
+
* with a letter or digit. Projects, contacts, places and events share it,
|
|
5
|
+
* so a `kind:slug` reference has one shape everywhere.
|
|
6
|
+
*/
|
|
7
|
+
export declare const SLUG_RE: RegExp;
|
|
8
|
+
/** Derive a slug from a title: "Relaunch netnode.ch" -> "relaunch-netnode-ch". */
|
|
9
|
+
export declare function slugify(title: string, max?: number): string;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one slug rule for folders and files kraftwerk names after a title:
|
|
3
|
+
* lowercase letters, digits and dashes, at most 49 characters, starting
|
|
4
|
+
* with a letter or digit. Projects, contacts, places and events share it,
|
|
5
|
+
* so a `kind:slug` reference has one shape everywhere.
|
|
6
|
+
*/
|
|
7
|
+
export const SLUG_RE = /^[a-z0-9][a-z0-9-]{0,48}$/;
|
|
8
|
+
/** Derive a slug from a title: "Relaunch netnode.ch" -> "relaunch-netnode-ch". */
|
|
9
|
+
export function slugify(title, max = 48) {
|
|
10
|
+
return title
|
|
11
|
+
.normalize("NFKD")
|
|
12
|
+
.replace(/[̀-ͯ]/g, "")
|
|
13
|
+
.toLowerCase()
|
|
14
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
15
|
+
.replace(/^-+|-+$/g, "")
|
|
16
|
+
.slice(0, max)
|
|
17
|
+
.replace(/-+$/, "");
|
|
18
|
+
}
|
package/dist/core/trash.d.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
export declare const TRASH_DIR = "kraftwerk-data/trash";
|
|
10
10
|
/** What can be in the trash; also the folder under trash/ it lands in. */
|
|
11
|
-
export type TrashKind = "agents" | "agent-skills" | "projects" | "workflows" | "knowledge" | "vibeables" | "channels" | "repos" | "chats" | "runs" | "files";
|
|
11
|
+
export type TrashKind = "agents" | "agent-skills" | "projects" | "contacts" | "places" | "events" | "workflows" | "knowledge" | "vibeables" | "channels" | "repos" | "chats" | "runs" | "files";
|
|
12
12
|
export interface TrashEntry {
|
|
13
13
|
/** Path inside the trash, forward slashes — the entry's handle. */
|
|
14
14
|
id: string;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as z from "zod";
|
|
2
|
+
import { log } from "../../core/log.js";
|
|
2
3
|
import { createPairCode, listDevices, redeemPairCode, revokeDevice, setDevicePush } from "../../core/devices.js";
|
|
3
4
|
import { networkUrls } from "../bind.js";
|
|
4
5
|
import { currentHub } from "../hub-context.js";
|
|
@@ -108,7 +109,7 @@ export const deviceRoutes = [
|
|
|
108
109
|
const { enabled, url } = await c.body();
|
|
109
110
|
const s = await setRelay(enabled, url);
|
|
110
111
|
const hub = currentHub();
|
|
111
|
-
const status = hub.daemon ? await applyRelay(hub.port, await getPkgVersion(), (l) =>
|
|
112
|
+
const status = hub.daemon ? await applyRelay(hub.port, await getPkgVersion(), (level, l) => log(level, "relay", l)) : relayStatus();
|
|
112
113
|
return { enabled: s.enabled, url: s.url, fingerprint: keyFingerprint(s.publicKey), daemon: hub.daemon === true, ...status };
|
|
113
114
|
}),
|
|
114
115
|
route({ name: "devices.revoke", method: "DELETE", path: "/api/devices/:id", summary: "unpair a device: its token stops working at once", access: "local" }, async (c) => (await revokeDevice(c.params.id)) ? { ok: true } : fail(404, "no such device")),
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import * as z from "zod";
|
|
2
|
+
import type { EntityKind } from "../../config.js";
|
|
3
|
+
import { type EntityBase, type EntityKindDef } from "../../core/entities.js";
|
|
4
|
+
/**
|
|
5
|
+
* Contacts, places and events over the API. One factory, three kinds: the
|
|
6
|
+
* routes are named per kind (`contacts.list`, `places.get`, `events.save`)
|
|
7
|
+
* with the kind's own result type, so the typed client and the UI see
|
|
8
|
+
* three ordinary route families — not one generic entities endpoint whose
|
|
9
|
+
* results would be a union. Adding a kind is one definition in
|
|
10
|
+
* core/entities.ts and one factory call below.
|
|
11
|
+
*/
|
|
12
|
+
export declare function entityRoutes<D extends EntityBase, K extends EntityKind>(def: EntityKindDef<D, K>): readonly [import("./router.js").Route<`${K}.list`, import("../../core/entities.js").EntitiesView<D>, `/api/${K}`, undefined>, import("./router.js").Route<`${K}.create`, import("./router.js").Reply<import("../../core/entities.js").EntityDetail<D>>, `/api/${K}`, z.ZodObject<{
|
|
13
|
+
notes: z.ZodOptional<z.ZodString>;
|
|
14
|
+
tags: z.ZodOptional<z.ZodUnknown>;
|
|
15
|
+
records: z.ZodOptional<z.ZodUnknown>;
|
|
16
|
+
title: z.ZodString;
|
|
17
|
+
slug: z.ZodOptional<z.ZodString>;
|
|
18
|
+
}, z.core.$loose>>, import("./router.js").Route<`${K}.get`, import("../../core/entities.js").EntityDetail<D>, `/api/${K}/:slug`, undefined>, import("./router.js").Route<`${K}.save`, import("../../core/entities.js").EntityDetail<D>, `/api/${K}/:slug`, z.ZodObject<{
|
|
19
|
+
title: z.ZodOptional<z.ZodString>;
|
|
20
|
+
notes: z.ZodOptional<z.ZodString>;
|
|
21
|
+
tags: z.ZodOptional<z.ZodUnknown>;
|
|
22
|
+
records: z.ZodOptional<z.ZodUnknown>;
|
|
23
|
+
}, z.core.$loose>>, import("./router.js").Route<`${K}.delete`, {
|
|
24
|
+
ok: boolean;
|
|
25
|
+
}, `/api/${K}/:slug`, undefined>];
|
|
26
|
+
export declare const contactRoutes: readonly [import("./router.js").Route<"contacts.list", import("../../core/entities.js").EntitiesView<import("../../core/entities.js").ContactDef>, "/api/contacts", undefined>, import("./router.js").Route<"contacts.create", import("./router.js").Reply<import("../../core/entities.js").EntityDetail<import("../../core/entities.js").ContactDef>>, "/api/contacts", z.ZodObject<{
|
|
27
|
+
notes: z.ZodOptional<z.ZodString>;
|
|
28
|
+
tags: z.ZodOptional<z.ZodUnknown>;
|
|
29
|
+
records: z.ZodOptional<z.ZodUnknown>;
|
|
30
|
+
title: z.ZodString;
|
|
31
|
+
slug: z.ZodOptional<z.ZodString>;
|
|
32
|
+
}, z.core.$loose>>, import("./router.js").Route<"contacts.get", import("../../core/entities.js").EntityDetail<import("../../core/entities.js").ContactDef>, "/api/contacts/:slug", undefined>, import("./router.js").Route<"contacts.save", import("../../core/entities.js").EntityDetail<import("../../core/entities.js").ContactDef>, "/api/contacts/:slug", z.ZodObject<{
|
|
33
|
+
title: z.ZodOptional<z.ZodString>;
|
|
34
|
+
notes: z.ZodOptional<z.ZodString>;
|
|
35
|
+
tags: z.ZodOptional<z.ZodUnknown>;
|
|
36
|
+
records: z.ZodOptional<z.ZodUnknown>;
|
|
37
|
+
}, z.core.$loose>>, import("./router.js").Route<"contacts.delete", {
|
|
38
|
+
ok: boolean;
|
|
39
|
+
}, "/api/contacts/:slug", undefined>];
|
|
40
|
+
export declare const placeRoutes: readonly [import("./router.js").Route<"places.list", import("../../core/entities.js").EntitiesView<import("../../core/entities.js").PlaceDef>, "/api/places", undefined>, import("./router.js").Route<"places.create", import("./router.js").Reply<import("../../core/entities.js").EntityDetail<import("../../core/entities.js").PlaceDef>>, "/api/places", z.ZodObject<{
|
|
41
|
+
notes: z.ZodOptional<z.ZodString>;
|
|
42
|
+
tags: z.ZodOptional<z.ZodUnknown>;
|
|
43
|
+
records: z.ZodOptional<z.ZodUnknown>;
|
|
44
|
+
title: z.ZodString;
|
|
45
|
+
slug: z.ZodOptional<z.ZodString>;
|
|
46
|
+
}, z.core.$loose>>, import("./router.js").Route<"places.get", import("../../core/entities.js").EntityDetail<import("../../core/entities.js").PlaceDef>, "/api/places/:slug", undefined>, import("./router.js").Route<"places.save", import("../../core/entities.js").EntityDetail<import("../../core/entities.js").PlaceDef>, "/api/places/:slug", z.ZodObject<{
|
|
47
|
+
title: z.ZodOptional<z.ZodString>;
|
|
48
|
+
notes: z.ZodOptional<z.ZodString>;
|
|
49
|
+
tags: z.ZodOptional<z.ZodUnknown>;
|
|
50
|
+
records: z.ZodOptional<z.ZodUnknown>;
|
|
51
|
+
}, z.core.$loose>>, import("./router.js").Route<"places.delete", {
|
|
52
|
+
ok: boolean;
|
|
53
|
+
}, "/api/places/:slug", undefined>];
|
|
54
|
+
export declare const eventRoutes: readonly [import("./router.js").Route<"events.list", import("../../core/entities.js").EntitiesView<import("../../core/entities.js").EventDef>, "/api/events", undefined>, import("./router.js").Route<"events.create", import("./router.js").Reply<import("../../core/entities.js").EntityDetail<import("../../core/entities.js").EventDef>>, "/api/events", z.ZodObject<{
|
|
55
|
+
notes: z.ZodOptional<z.ZodString>;
|
|
56
|
+
tags: z.ZodOptional<z.ZodUnknown>;
|
|
57
|
+
records: z.ZodOptional<z.ZodUnknown>;
|
|
58
|
+
title: z.ZodString;
|
|
59
|
+
slug: z.ZodOptional<z.ZodString>;
|
|
60
|
+
}, z.core.$loose>>, import("./router.js").Route<"events.get", import("../../core/entities.js").EntityDetail<import("../../core/entities.js").EventDef>, "/api/events/:slug", undefined>, import("./router.js").Route<"events.save", import("../../core/entities.js").EntityDetail<import("../../core/entities.js").EventDef>, "/api/events/:slug", z.ZodObject<{
|
|
61
|
+
title: z.ZodOptional<z.ZodString>;
|
|
62
|
+
notes: z.ZodOptional<z.ZodString>;
|
|
63
|
+
tags: z.ZodOptional<z.ZodUnknown>;
|
|
64
|
+
records: z.ZodOptional<z.ZodUnknown>;
|
|
65
|
+
}, z.core.$loose>>, import("./router.js").Route<"events.delete", {
|
|
66
|
+
ok: boolean;
|
|
67
|
+
}, "/api/events/:slug", undefined>];
|
|
68
|
+
/** References: resolve `kind:slug` tokens — given as a list, or found in a text — against the registry. */
|
|
69
|
+
export declare const refRoutes: import("./router.js").Route<"refs.resolve", {
|
|
70
|
+
refs: import("../../core/refs.js").ResolvedRef[];
|
|
71
|
+
}, "/api/refs/resolve", z.ZodObject<{
|
|
72
|
+
tokens: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
73
|
+
text: z.ZodDefault<z.ZodString>;
|
|
74
|
+
}, z.core.$strip>>[];
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import * as z from "zod";
|
|
2
|
+
import { CONTACTS, createEntity, deleteEntity, EVENTS, getEntity, listEntities, openEntities, PLACES, saveEntity } from "../../core/entities.js";
|
|
3
|
+
import { findRefs, parseRef, resolveTokens } from "../../core/refs.js";
|
|
4
|
+
import { fail, reply, route, statusByMessage } from "./router.js";
|
|
5
|
+
/**
|
|
6
|
+
* Contacts, places and events over the API. One factory, three kinds: the
|
|
7
|
+
* routes are named per kind (`contacts.list`, `places.get`, `events.save`)
|
|
8
|
+
* with the kind's own result type, so the typed client and the UI see
|
|
9
|
+
* three ordinary route families — not one generic entities endpoint whose
|
|
10
|
+
* results would be a union. Adding a kind is one definition in
|
|
11
|
+
* core/entities.ts and one factory call below.
|
|
12
|
+
*/
|
|
13
|
+
export function entityRoutes(def) {
|
|
14
|
+
const errors = statusByMessage(new RegExp(`^no ${def.singular}`));
|
|
15
|
+
const kind = def.kind;
|
|
16
|
+
/** The editable fields. Loose: the domain checks values and keeps what it knows (unknown keys stay in the frontmatter). */
|
|
17
|
+
const Fields = z.looseObject({
|
|
18
|
+
title: z.string().optional(),
|
|
19
|
+
notes: z.string().optional(),
|
|
20
|
+
tags: z.unknown().optional(),
|
|
21
|
+
records: z.unknown().optional(),
|
|
22
|
+
...Object.fromEntries(def.fields.map((f) => [f.name, z.unknown().optional()])),
|
|
23
|
+
});
|
|
24
|
+
const Create = Fields.extend({ title: z.string(), slug: z.string().optional() });
|
|
25
|
+
const fieldNames = def.fields.map((f) => f.name).join(", ");
|
|
26
|
+
return [
|
|
27
|
+
route({ name: `${kind}.list`, method: "GET", path: `/api/${kind}`, summary: `every ${def.singular} file under the root${def.span ? " (?from=&to= narrow by date; an entry counts while it runs)" : ""}` }, async (c) => listEntities(def, { from: c.query.get("from") ?? undefined, to: c.query.get("to") ?? undefined })),
|
|
28
|
+
route({ name: `${kind}.create`, method: "POST", path: `/api/${kind}`, summary: `create one {title, slug?, notes?, tags?, records?, ${fieldNames}}`, errors: 400, body: Create }, async (c) => {
|
|
29
|
+
if ((await openEntities(kind)).off)
|
|
30
|
+
fail(409, `${kind} are off`);
|
|
31
|
+
return reply(201, await createEntity(def, (await c.body())));
|
|
32
|
+
}),
|
|
33
|
+
route({ name: `${kind}.get`, method: "GET", path: `/api/${kind}/:slug`, summary: "fields, notes and link states", errors }, async (c) => (await getEntity(def, c.params.slug)) ?? fail(404, `no ${def.singular} "${c.params.slug}"`)),
|
|
34
|
+
route({ name: `${kind}.save`, method: "PUT", path: `/api/${kind}/:slug`, summary: 'save fields and notes ("" clears a field)', errors, body: Fields }, async (c) => saveEntity(def, c.params.slug, (await c.body()))),
|
|
35
|
+
route({ name: `${kind}.delete`, method: "DELETE", path: `/api/${kind}/:slug`, summary: "move the file to the trash", errors }, async (c) => {
|
|
36
|
+
await deleteEntity(def, c.params.slug);
|
|
37
|
+
return { ok: true };
|
|
38
|
+
}),
|
|
39
|
+
];
|
|
40
|
+
}
|
|
41
|
+
export const contactRoutes = entityRoutes(CONTACTS);
|
|
42
|
+
export const placeRoutes = entityRoutes(PLACES);
|
|
43
|
+
export const eventRoutes = entityRoutes(EVENTS);
|
|
44
|
+
/** References: resolve `kind:slug` tokens — given as a list, or found in a text — against the registry. */
|
|
45
|
+
export const refRoutes = [
|
|
46
|
+
route({
|
|
47
|
+
name: "refs.resolve",
|
|
48
|
+
method: "POST",
|
|
49
|
+
path: "/api/refs/resolve",
|
|
50
|
+
summary: "resolve references {tokens?: [kind:slug], text?} to whether each target exists and where it opens",
|
|
51
|
+
errors: 400,
|
|
52
|
+
body: z.object({ tokens: z.array(z.string()).default([]), text: z.string().default("") }),
|
|
53
|
+
}, async (c) => {
|
|
54
|
+
const { tokens, text } = await c.body();
|
|
55
|
+
const given = tokens.map((t) => parseRef(t) ?? fail(400, `not a reference: "${t}" (expected <kind>:<slug>, e.g. contact:anna-meier)`));
|
|
56
|
+
const found = findRefs(text);
|
|
57
|
+
const all = [...given, ...found].filter((r, i, arr) => arr.findIndex((x) => x.kind === r.kind && x.slug === r.slug) === i);
|
|
58
|
+
return { refs: await resolveTokens(all) };
|
|
59
|
+
}),
|
|
60
|
+
];
|