@netnodeag/kraftwerk 0.24.0 → 0.26.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.
@@ -8,9 +8,10 @@ import { listWorkflows, getWorkflow } from "./workflows.js";
8
8
  import { dockerStatus, triggerRun, stopRun } from "./runner.js";
9
9
  import { cancelChat, createChat, deleteChat, disposeAllBackends, getChat, listChats, postMessage, resolvePermission, subscribeChat, } from "./chat/sessions.js";
10
10
  import { bundleDetail, conceptDetail, createBundle, knowledgeIndex, putConcept, verifyFromUi, } from "./knowledge.js";
11
- import { deleteMember, getMember, listMembers, saveMember, teamRoot, } from "./team.js";
11
+ import { deleteMember, getMember, listMembers, saveMember, setMemberArchived, teamRoot, } from "./team.js";
12
12
  import { deleteAgentSkill, getSkill, listAgentSkills, listSkills, readSkill, saveAgentSkill, skillsRoot, } from "./skills.js";
13
13
  import { discoverInstances, registerInstance, unregisterInstance } from "./instances.js";
14
+ import { getSettings, saveSettings } from "./settings.js";
14
15
  import { deleteRoutine, routineStatuses, runRoutineNow, saveRoutine, startRoutineScheduler, } from "./routines.js";
15
16
  /**
16
17
  * The inspector server: a plain node:http server with no dependencies.
@@ -118,6 +119,20 @@ async function handleApi(req, res, url) {
118
119
  switcher,
119
120
  });
120
121
  }
122
+ // GET /api/settings — kraftwerk.yml (parsed + resolved paths) for the settings page
123
+ if (seg.length === 2 && seg[1] === "settings" && method === "GET") {
124
+ return json(res, await getSettings());
125
+ }
126
+ // PUT /api/settings — write the UI-editable subset (name, icon, switcher) back
127
+ if (seg.length === 2 && seg[1] === "settings" && method === "PUT") {
128
+ try {
129
+ const input = JSON.parse(await readBody(req));
130
+ return json(res, await saveSettings(input));
131
+ }
132
+ catch (err) {
133
+ return json(res, { error: err.message }, 400);
134
+ }
135
+ }
121
136
  // GET /api/update-check — ask the npm registry for the latest published version
122
137
  if (seg.length === 2 && seg[1] === "update-check" && method === "GET") {
123
138
  const name = await getPkgName();
@@ -343,6 +358,16 @@ async function handleApi(req, res, url) {
343
358
  }
344
359
  }
345
360
  }
361
+ // POST /api/team/:slug/archive — archive/unarchive a member ({ archived: boolean })
362
+ if (seg.length === 4 && seg[1] === "team" && seg[3] === "archive" && method === "POST") {
363
+ try {
364
+ const body = JSON.parse(await readBody(req));
365
+ return json(res, await setMemberArchived(decodeURIComponent(seg[2]), body.archived === true));
366
+ }
367
+ catch (err) {
368
+ return json(res, { error: err.message }, 400);
369
+ }
370
+ }
346
371
  // GET/POST /api/team/:slug/skills — the agent's own skills / create or update one
347
372
  if (seg.length === 4 && seg[1] === "team" && seg[3] === "skills") {
348
373
  const slug = decodeURIComponent(seg[2]);
@@ -0,0 +1,27 @@
1
+ import { type ProjectConfig, type SwitcherEntry } from "../config.js";
2
+ /**
3
+ * Workspace settings: read kraftwerk.yml for the UI and write the
4
+ * UI-editable subset (name, icon, switcher) back. Edits go through the
5
+ * yaml Document API so comments and every other key survive untouched;
6
+ * a missing kraftwerk.yml is created at the project root on first save.
7
+ */
8
+ export interface SettingsView {
9
+ root: string;
10
+ configPath: string;
11
+ exists: boolean;
12
+ config: ProjectConfig;
13
+ resolved: {
14
+ workflowsRoot: string | null;
15
+ outputDir: string;
16
+ port: number;
17
+ };
18
+ }
19
+ export interface SaveSettingsInput {
20
+ /** Omitted = untouched; empty string = remove the key. */
21
+ name?: string;
22
+ icon?: string;
23
+ /** Omitted = untouched; empty list = remove the key. */
24
+ switcher?: SwitcherEntry[];
25
+ }
26
+ export declare function getSettings(): Promise<SettingsView>;
27
+ export declare function saveSettings(input: SaveSettingsInput): Promise<SettingsView>;
@@ -0,0 +1,59 @@
1
+ import { promises as fs } from "node:fs";
2
+ import path from "node:path";
3
+ import { parseDocument } from "yaml";
4
+ import { resolveProject } from "../config.js";
5
+ import { getProjectRoot } from "./context.js";
6
+ export async function getSettings() {
7
+ const project = await resolveProject(getProjectRoot());
8
+ return {
9
+ root: project.root,
10
+ configPath: project.configPath ?? path.join(project.root, "kraftwerk.yml"),
11
+ exists: !!project.configPath,
12
+ config: project.config,
13
+ resolved: {
14
+ workflowsRoot: project.workflowsRoot ?? null,
15
+ outputDir: project.outputDir,
16
+ port: project.config.port ?? 1981,
17
+ },
18
+ };
19
+ }
20
+ function cleanSwitcher(value) {
21
+ if (!Array.isArray(value))
22
+ throw new Error("switcher must be a list");
23
+ return value.map((entry, i) => {
24
+ const name = typeof entry?.name === "string" ? entry.name.trim() : "";
25
+ const url = typeof entry?.url === "string" ? entry.url.trim() : "";
26
+ if (!name)
27
+ throw new Error(`switcher entry ${i + 1}: name is required`);
28
+ if (!/^https?:\/\//.test(url))
29
+ throw new Error(`switcher entry ${i + 1}: url must start with http:// or https://`);
30
+ const icon = typeof entry.icon === "string" ? entry.icon.trim() : "";
31
+ return icon ? { name, url, icon } : { name, url };
32
+ });
33
+ }
34
+ export async function saveSettings(input) {
35
+ if (input.name !== undefined && typeof input.name !== "string")
36
+ throw new Error("name must be a string");
37
+ if (input.icon !== undefined && typeof input.icon !== "string")
38
+ throw new Error("icon must be a string");
39
+ const switcher = input.switcher === undefined ? undefined : cleanSwitcher(input.switcher);
40
+ const project = await resolveProject(getProjectRoot());
41
+ const configPath = project.configPath ?? path.join(project.root, "kraftwerk.yml");
42
+ const raw = project.configPath ? await fs.readFile(configPath, "utf8") : "";
43
+ const doc = parseDocument(raw);
44
+ if (doc.errors.length > 0)
45
+ throw new Error(`${path.basename(configPath)}: ${doc.errors[0].message}`);
46
+ const setOrDelete = (key, value) => {
47
+ if (value === undefined)
48
+ return; // untouched
49
+ if (value === "" || (Array.isArray(value) && value.length === 0))
50
+ doc.delete(key);
51
+ else
52
+ doc.set(key, value);
53
+ };
54
+ setOrDelete("name", input.name?.trim());
55
+ setOrDelete("icon", input.icon?.trim());
56
+ setOrDelete("switcher", switcher);
57
+ await fs.writeFile(configPath, doc.toString());
58
+ return getSettings();
59
+ }
@@ -30,6 +30,8 @@ export interface TeamMember {
30
30
  * Absent = all discovered skills; empty list = no skills.
31
31
  */
32
32
  skills?: string[];
33
+ /** Archived: hidden from the active roster, restorable any time. */
34
+ archived?: boolean;
33
35
  }
34
36
  export interface TeamMemberDetail extends TeamMember {
35
37
  /** Contents of system.md — the member's system prompt. */
@@ -58,4 +60,6 @@ export interface SaveMemberInput {
58
60
  system?: string;
59
61
  }
60
62
  export declare function saveMember(input: SaveMemberInput): Promise<TeamMemberDetail>;
63
+ /** Archive/unarchive a member: toggles `archived:` in agent.yml, nothing else. */
64
+ export declare function setMemberArchived(slug: string, archived: boolean): Promise<TeamMemberDetail>;
61
65
  export declare function deleteMember(slug: string): Promise<void>;
@@ -52,6 +52,7 @@ function normalize(slug, raw) {
52
52
  workflows: Array.isArray(raw.workflows) ? raw.workflows.map(String) : [],
53
53
  knowledge: Array.isArray(raw.knowledge) ? raw.knowledge.map(String) : [],
54
54
  ...(Array.isArray(raw.skills) ? { skills: raw.skills.map(String) } : {}),
55
+ ...(raw.archived === true ? { archived: true } : {}),
55
56
  };
56
57
  }
57
58
  export async function listMembers() {
@@ -101,6 +102,11 @@ export async function saveMember(input) {
101
102
  const slug = input.slug ? safeMemberSlug(input.slug) : slugFromName(name);
102
103
  const dir = path.join(await teamRoot(), slug);
103
104
  await fs.mkdir(dir, { recursive: true });
105
+ // Profile edits must not silently unarchive: carry the flag over.
106
+ const existing = await fs
107
+ .readFile(path.join(dir, "agent.yml"), "utf8")
108
+ .then((raw) => (parse(raw) ?? {}))
109
+ .catch(() => null);
104
110
  const yml = {
105
111
  name,
106
112
  emoji: input.emoji?.trim() || "🤖",
@@ -112,11 +118,30 @@ export async function saveMember(input) {
112
118
  workflows: (input.workflows ?? []).map(String),
113
119
  knowledge: (input.knowledge ?? []).map(String),
114
120
  ...(input.skills ? { skills: input.skills.map(String) } : {}),
121
+ ...(existing?.archived === true ? { archived: true } : {}),
115
122
  };
116
123
  await fs.writeFile(path.join(dir, "agent.yml"), stringify(yml));
117
124
  await fs.writeFile(path.join(dir, "system.md"), (input.system ?? "").trim() + "\n");
118
125
  return (await getMember(slug));
119
126
  }
127
+ /** Archive/unarchive a member: toggles `archived:` in agent.yml, nothing else. */
128
+ export async function setMemberArchived(slug, archived) {
129
+ const dir = path.join(await teamRoot(), safeMemberSlug(slug));
130
+ let raw;
131
+ try {
132
+ raw = (parse(await fs.readFile(path.join(dir, "agent.yml"), "utf8")) ?? {});
133
+ }
134
+ catch {
135
+ throw new Error("agent not found");
136
+ }
137
+ const yml = { ...raw };
138
+ if (archived)
139
+ yml.archived = true;
140
+ else
141
+ delete yml.archived;
142
+ await fs.writeFile(path.join(dir, "agent.yml"), stringify(yml));
143
+ return (await getMember(slug));
144
+ }
120
145
  export async function deleteMember(slug) {
121
146
  const dir = path.join(await teamRoot(), safeMemberSlug(slug));
122
147
  await fs.rm(dir, { recursive: true, force: true });