@geminixiang/mikan 1.0.0-beta.70 → 1.0.0-beta.72
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/CHANGELOG.md +18 -0
- package/dist/adapters/web/admin/portal.d.ts.map +1 -1
- package/dist/adapters/web/admin/portal.js +279 -47
- package/dist/adapters/web/admin/portal.js.map +1 -1
- package/dist/harness/index.d.ts +1 -1
- package/dist/harness/index.d.ts.map +1 -1
- package/dist/harness/index.js +1 -1
- package/dist/harness/index.js.map +1 -1
- package/dist/harness/prompt.d.ts.map +1 -1
- package/dist/harness/prompt.js +4 -13
- package/dist/harness/prompt.js.map +1 -1
- package/dist/harness/skills.d.ts +2 -0
- package/dist/harness/skills.d.ts.map +1 -1
- package/dist/harness/skills.js +2 -1
- package/dist/harness/skills.js.map +1 -1
- package/dist/harness/subagent-profiles.d.ts.map +1 -1
- package/dist/harness/subagent-profiles.js +18 -9
- package/dist/harness/subagent-profiles.js.map +1 -1
- package/dist/harness/tools/subagent.d.ts +0 -3
- package/dist/harness/tools/subagent.d.ts.map +1 -1
- package/dist/harness/tools/subagent.js +3 -7
- package/dist/harness/tools/subagent.js.map +1 -1
- package/dist/log.d.ts.map +1 -1
- package/dist/log.js +4 -1
- package/dist/log.js.map +1 -1
- package/dist/sandbox/provisioner.d.ts.map +1 -1
- package/dist/sandbox/provisioner.js +1 -4
- package/dist/sandbox/provisioner.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,24 @@ any release.
|
|
|
9
9
|
|
|
10
10
|
## [Unreleased]
|
|
11
11
|
|
|
12
|
+
## [1.0.0-beta.72]
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- Admin: the Skills panel (conversation and global) can now create, edit, and delete skills directly, not just list and preview them. Saving writes `SKILL.md` straight into the workspace skills tree the harness loader reads, so a new or edited skill is available on the conversation's next turn.
|
|
17
|
+
|
|
18
|
+
## [1.0.0-beta.71]
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- Built-in subagents now use the full 100-turn / $10 allowance, and the model-facing `subagent` tool can only request a larger token allowance instead of accidentally shrinking turn, cost, or duration budgets (for example, to $0.30).
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- Shared-memory guidance now keeps personal, project-specific, and tool-specific knowledge in its proper scope, prevents memory from overriding system mechanisms, and removes the obsolete shared `SYSTEM.md` environment log.
|
|
27
|
+
- Console usage summaries now show cache-read tokens alongside fresh input tokens.
|
|
28
|
+
- Sandbox provisioning no longer logs `Container … already running` on every readiness check; creation, restart, drift, and failure messages remain visible.
|
|
29
|
+
|
|
12
30
|
## [1.0.0-beta.70]
|
|
13
31
|
|
|
14
32
|
### Fixed
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"portal.d.ts","sourceRoot":"","sources":["../../../../src/adapters/web/admin/portal.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"portal.d.ts","sourceRoot":"","sources":["../../../../src/adapters/web/admin/portal.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAKjE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AAC3D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AACnD,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AACvD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAC7C,YAAY,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAI7C,qBAAa,uBAAwB,SAAQ,kBAAkB,CAAC,UAAU,CAAC;IACzE,MAAM,CAAC,IAAI,EAAE;QACX,QAAQ,EAAE,YAAY,CAAC;QACvB,cAAc,EAAE,MAAM,CAAC;QACvB,cAAc,EAAE,MAAM,CAAC;QACvB,gBAAgB,CAAC,EAAE,MAAM,CAAC;KAC3B,GAAG,UAAU,CAUb;CACF;AAoCD,YAAY,EAAE,kBAAkB,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAClF,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC9D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAYpD,wBAAsB,kBAAkB,CACtC,GAAG,EAAE,eAAe,EACpB,GAAG,EAAE,cAAc,EACnB,GAAG,EAAE,GAAG,EACR,QAAQ,EAAE,aAAa,GACtB,OAAO,CAAC,OAAO,CAAC,CA6BlB;AA2GD,UAAU,sBAAsB;IAC9B,OAAO,EAAE,aAAa,CAAC;IACvB,cAAc,EAAE,MAAM,CAAC;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;GAIG;AACH,wBAAgB,wBAAwB,CACtC,WAAW,EAAE,MAAM,EACnB,iBAAiB,EAAE,MAAM,EACzB,KAAK,EAAE,UAAU,GAChB,sBAAsB,CAexB;AA08BD,UAAU,UAAU;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,QAAQ,GAAG,cAAc,CAAC;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;CACnB;AAwBD,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,CAAC,QAAQ,CAAC,GAAG,UAAU,EAAE,CAuB/F;AA8XD;;;;GAIG;AACH,wBAAsB,gBAAgB,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CAmBjF"}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { validateEventFilename } from "../../../events/index.js";
|
|
2
|
-
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, } from "node:fs";
|
|
3
3
|
import { basename, join, resolve as pathResolve, sep as pathSep } from "node:path";
|
|
4
|
-
import {
|
|
4
|
+
import { atomicWritePrivateFile } from "../../../file-guards.js";
|
|
5
|
+
import { MikanModels, parseFrontmatter, validateSkill } from "../../../harness/index.js";
|
|
5
6
|
import { SessionStore } from "../../../sessions/session-store.js";
|
|
6
7
|
import { InMemoryTokenStore } from "../token-store.js";
|
|
7
8
|
const ADMIN_TOKEN_TTL_MS = 30 * 60 * 1000;
|
|
@@ -104,7 +105,8 @@ async function routeGetApiRequest(res, url, services) {
|
|
|
104
105
|
}
|
|
105
106
|
}
|
|
106
107
|
async function routePostApiRequest(req, res, url, services) {
|
|
107
|
-
const
|
|
108
|
+
const bodyLimit = url.pathname === "/admin/api/skills/mutate" ? 256 * 1024 : 32 * 1024;
|
|
109
|
+
const body = await readJsonBody(req, res, bodyLimit);
|
|
108
110
|
if (!body)
|
|
109
111
|
return;
|
|
110
112
|
const token = services.adminTokenStore.peek(typeof body.token === "string" ? body.token : "");
|
|
@@ -127,6 +129,8 @@ async function routePostApiRequest(req, res, url, services) {
|
|
|
127
129
|
return serveConversationEventDelete(res, body, services, token);
|
|
128
130
|
case "/admin/api/mcp-servers/mutate":
|
|
129
131
|
return serveMcpServerMutation(res, body, services, token);
|
|
132
|
+
case "/admin/api/skills/mutate":
|
|
133
|
+
return serveSkillMutation(res, body, services, token);
|
|
130
134
|
case "/admin/api/settings/model":
|
|
131
135
|
return serveGlobalModelUpdate(res, body, services);
|
|
132
136
|
case "/admin/api/settings/sandbox":
|
|
@@ -1221,6 +1225,96 @@ function serveSkillFile(res, url, services, token) {
|
|
|
1221
1225
|
}
|
|
1222
1226
|
servePreviewFile(res, safe.absolute, { source, directory }, "Skill file not found");
|
|
1223
1227
|
}
|
|
1228
|
+
const SKILL_DIRECTORY_PATTERN = /^[a-z0-9-]+$/;
|
|
1229
|
+
function resolveSkillsRoot(workspace, scope, source) {
|
|
1230
|
+
if (source !== "global" && source !== "conversation") {
|
|
1231
|
+
return { error: "Invalid skill source" };
|
|
1232
|
+
}
|
|
1233
|
+
return {
|
|
1234
|
+
root: source === "global" ? workspace.skillsDir : workspace.office(scope.address).skillsDir,
|
|
1235
|
+
source,
|
|
1236
|
+
};
|
|
1237
|
+
}
|
|
1238
|
+
/**
|
|
1239
|
+
* Create, overwrite, or delete one skill directory. The directory name is
|
|
1240
|
+
* fixed at creation (renaming would leave the old directory as an orphan);
|
|
1241
|
+
* editing an existing skill always targets its current directory. Writes go
|
|
1242
|
+
* straight to the workspace skills tree — the same location the harness
|
|
1243
|
+
* loader reads — so a saved skill is available on the conversation's next
|
|
1244
|
+
* turn without a restart.
|
|
1245
|
+
*/
|
|
1246
|
+
async function serveSkillMutation(res, body, services, token) {
|
|
1247
|
+
const action = body.action;
|
|
1248
|
+
if (action !== "save" && action !== "delete") {
|
|
1249
|
+
jsonRes(res, 400, { error: "action must be 'save' or 'delete'" });
|
|
1250
|
+
return;
|
|
1251
|
+
}
|
|
1252
|
+
const scope = resolveTargetConversation(body, token);
|
|
1253
|
+
if (scope.error) {
|
|
1254
|
+
jsonRes(res, 403, { error: scope.error });
|
|
1255
|
+
return;
|
|
1256
|
+
}
|
|
1257
|
+
const workspace = requireAdminWorkspace(res, services);
|
|
1258
|
+
if (!workspace)
|
|
1259
|
+
return;
|
|
1260
|
+
const resolved = resolveSkillsRoot(workspace, scope, body.source);
|
|
1261
|
+
if ("error" in resolved) {
|
|
1262
|
+
jsonRes(res, 400, { error: resolved.error });
|
|
1263
|
+
return;
|
|
1264
|
+
}
|
|
1265
|
+
const directory = typeof body.directory === "string" ? body.directory.trim() : "";
|
|
1266
|
+
if (!directory || !SKILL_DIRECTORY_PATTERN.test(directory)) {
|
|
1267
|
+
jsonRes(res, 400, {
|
|
1268
|
+
error: "directory must be lowercase a-z, 0-9 and hyphens",
|
|
1269
|
+
});
|
|
1270
|
+
return;
|
|
1271
|
+
}
|
|
1272
|
+
const safe = safeJoinUnderRoot(resolved.root, directory);
|
|
1273
|
+
if (safe.error) {
|
|
1274
|
+
jsonRes(res, 400, { error: safe.error });
|
|
1275
|
+
return;
|
|
1276
|
+
}
|
|
1277
|
+
if (action === "delete") {
|
|
1278
|
+
if (!existsSync(join(safe.absolute, "SKILL.md"))) {
|
|
1279
|
+
jsonRes(res, 404, { error: "Skill not found" });
|
|
1280
|
+
return;
|
|
1281
|
+
}
|
|
1282
|
+
try {
|
|
1283
|
+
rmSync(safe.absolute, { recursive: true, force: true });
|
|
1284
|
+
}
|
|
1285
|
+
catch (err) {
|
|
1286
|
+
jsonRes(res, 500, { error: err instanceof Error ? err.message : String(err) });
|
|
1287
|
+
return;
|
|
1288
|
+
}
|
|
1289
|
+
jsonRes(res, 200, { ok: true });
|
|
1290
|
+
return;
|
|
1291
|
+
}
|
|
1292
|
+
const name = typeof body.name === "string" ? body.name.trim() : directory;
|
|
1293
|
+
const description = typeof body.description === "string" ? body.description.trim() : "";
|
|
1294
|
+
const content = typeof body.content === "string" ? body.content : "";
|
|
1295
|
+
if (!description) {
|
|
1296
|
+
jsonRes(res, 400, { error: "description is required" });
|
|
1297
|
+
return;
|
|
1298
|
+
}
|
|
1299
|
+
const errors = validateSkill(name, description);
|
|
1300
|
+
if (errors.length > 0) {
|
|
1301
|
+
jsonRes(res, 400, { error: errors.join("; ") });
|
|
1302
|
+
return;
|
|
1303
|
+
}
|
|
1304
|
+
try {
|
|
1305
|
+
if (existsSync(safe.absolute) && lstatSync(safe.absolute).isSymbolicLink()) {
|
|
1306
|
+
throw new Error("Refusing to write through a symlinked skill directory");
|
|
1307
|
+
}
|
|
1308
|
+
mkdirSync(safe.absolute, { recursive: true });
|
|
1309
|
+
const frontmatter = ["---", `name: ${name}`, `description: ${description}`, "---", ""].join("\n");
|
|
1310
|
+
atomicWritePrivateFile(join(safe.absolute, "SKILL.md"), frontmatter + content.trim() + "\n");
|
|
1311
|
+
}
|
|
1312
|
+
catch (err) {
|
|
1313
|
+
jsonRes(res, 500, { error: err instanceof Error ? err.message : String(err) });
|
|
1314
|
+
return;
|
|
1315
|
+
}
|
|
1316
|
+
jsonRes(res, 200, { ok: true, name, directory, source: resolved.source });
|
|
1317
|
+
}
|
|
1224
1318
|
// ── Events ─────────────────────────────────────────────────────────────────────
|
|
1225
1319
|
/**
|
|
1226
1320
|
* List events through the owning store, whose payloads are validated by the
|
|
@@ -1362,7 +1456,10 @@ const adminViewBody = `<nav class="tab-nav" role="tablist" aria-label="Admin sec
|
|
|
1362
1456
|
<p class="eyebrow">Skills</p>
|
|
1363
1457
|
<h2 class="card-title">可用的 skills</h2>
|
|
1364
1458
|
</div></div>
|
|
1365
|
-
<
|
|
1459
|
+
<div class="sect-head-actions">
|
|
1460
|
+
<button class="primary-action-btn" onclick="event.stopPropagation(); openSkillDialog('conversation')">+ New skill</button>
|
|
1461
|
+
<button class="refresh-btn" onclick="event.stopPropagation(); loadSkills()">↻</button>
|
|
1462
|
+
</div>
|
|
1366
1463
|
</summary>
|
|
1367
1464
|
<div class="sect-body">
|
|
1368
1465
|
<div class="workspace-split">
|
|
@@ -1487,9 +1584,15 @@ const adminViewBody = `<nav class="tab-nav" role="tablist" aria-label="Admin sec
|
|
|
1487
1584
|
<p class="eyebrow">Global Skills</p>
|
|
1488
1585
|
<h2 class="card-title">全域 skills</h2>
|
|
1489
1586
|
</div>
|
|
1490
|
-
<
|
|
1587
|
+
<div class="sect-head-actions">
|
|
1588
|
+
<button class="primary-action-btn" onclick="openSkillDialog('global')">+ New global skill</button>
|
|
1589
|
+
<button class="refresh-btn" onclick="loadGlobalSkills()">↻</button>
|
|
1590
|
+
</div>
|
|
1491
1591
|
</header>
|
|
1492
|
-
<div
|
|
1592
|
+
<div class="workspace-split">
|
|
1593
|
+
<div id="global-skills-content" class="workspace-tree"><div class="loading-msg">Loading…</div></div>
|
|
1594
|
+
<div id="global-skills-preview" class="workspace-preview"><div class="placeholder-msg">Click a skill to preview SKILL.md</div></div>
|
|
1595
|
+
</div>
|
|
1493
1596
|
</section>
|
|
1494
1597
|
|
|
1495
1598
|
<section class="card sect">
|
|
@@ -1534,6 +1637,38 @@ const adminViewBody = `<nav class="tab-nav" role="tablist" aria-label="Admin sec
|
|
|
1534
1637
|
<button class="mcp-btn" type="button" onclick="closeMcpCustomDialog()">Cancel</button>
|
|
1535
1638
|
<button id="mcp-custom-dialog-submit" class="primary-action-btn" type="button" onclick="submitMcpCustomDialog(this)">新增並測試連線</button>
|
|
1536
1639
|
</div>
|
|
1640
|
+
</dialog>
|
|
1641
|
+
|
|
1642
|
+
<dialog id="skill-dialog" class="mcp-dialog" aria-labelledby="skill-dialog-title">
|
|
1643
|
+
<div class="mcp-dialog-head">
|
|
1644
|
+
<div>
|
|
1645
|
+
<p class="eyebrow" id="skill-dialog-eyebrow">Skill</p>
|
|
1646
|
+
<h2 id="skill-dialog-title" class="card-title">New skill</h2>
|
|
1647
|
+
</div>
|
|
1648
|
+
<button class="mcp-dialog-close" type="button" aria-label="Close" onclick="closeSkillDialog()">×</button>
|
|
1649
|
+
</div>
|
|
1650
|
+
<div class="mcp-field">
|
|
1651
|
+
<span>Directory</span>
|
|
1652
|
+
<input id="skill-dialog-directory" class="form-input mcp-json" placeholder="my-skill" autocomplete="off" />
|
|
1653
|
+
<small class="mcp-secret-note">Lowercase letters, digits, hyphens. Cannot be changed after creation.</small>
|
|
1654
|
+
</div>
|
|
1655
|
+
<div class="mcp-field">
|
|
1656
|
+
<span>Name</span>
|
|
1657
|
+
<input id="skill-dialog-name" class="form-input mcp-json" placeholder="my-skill" autocomplete="off" />
|
|
1658
|
+
</div>
|
|
1659
|
+
<div class="mcp-field">
|
|
1660
|
+
<span>Description</span>
|
|
1661
|
+
<input id="skill-dialog-description" class="form-input mcp-json" placeholder="Use when …" autocomplete="off" />
|
|
1662
|
+
</div>
|
|
1663
|
+
<div class="mcp-field">
|
|
1664
|
+
<span>Instructions (SKILL.md body)</span>
|
|
1665
|
+
<textarea id="skill-dialog-content" class="form-input mcp-json" spellcheck="false" rows="12" placeholder="# My skill Instructions here."></textarea>
|
|
1666
|
+
</div>
|
|
1667
|
+
<div id="skill-dialog-error" class="inline-result err" style="display:none"></div>
|
|
1668
|
+
<div class="mcp-dialog-actions">
|
|
1669
|
+
<button class="mcp-btn" type="button" onclick="closeSkillDialog()">Cancel</button>
|
|
1670
|
+
<button id="skill-dialog-submit" class="primary-action-btn" type="button" onclick="submitSkillDialog(this)">Save</button>
|
|
1671
|
+
</div>
|
|
1537
1672
|
</dialog>`;
|
|
1538
1673
|
const adminViewScript = ` let activeConversationKey = defaultConversationKey;
|
|
1539
1674
|
function scopeOf(key) {
|
|
@@ -2287,6 +2422,31 @@ const adminViewScript = ` let activeConversationKey = defaultConversationKey;
|
|
|
2287
2422
|
void mutateMcpServer(btn.dataset.mcpScope, btn.dataset.mcpAction, btn.dataset.mcpName);
|
|
2288
2423
|
});
|
|
2289
2424
|
|
|
2425
|
+
let skillsCache = [];
|
|
2426
|
+
|
|
2427
|
+
function renderSkillsInto(containerId, skills, allowedSource) {
|
|
2428
|
+
const container = document.getElementById(containerId);
|
|
2429
|
+
if (!container) return;
|
|
2430
|
+
const filtered = allowedSource ? skills.filter((s) => s.source === allowedSource) : skills;
|
|
2431
|
+
if (filtered.length === 0) {
|
|
2432
|
+
container.innerHTML = '<div class="empty-state">No skills available</div>';
|
|
2433
|
+
return;
|
|
2434
|
+
}
|
|
2435
|
+
container.innerHTML = '<div class="skills-list">' +
|
|
2436
|
+
filtered.map((s) =>
|
|
2437
|
+
'<div class="skill-row">' +
|
|
2438
|
+
'<button class="skill-row-btn" data-skill-source="' + escAttr(s.source) + '" data-skill-directory="' + escAttr(s.directory) + '" data-skill-name="' + escAttr(s.name) + '">' +
|
|
2439
|
+
'<div class="skill-name">' + escHtml(s.name) + '<span class="skill-source skill-source-' + s.source + '">' + s.source + '</span></div>' +
|
|
2440
|
+
(s.description ? '<div class="skill-desc">' + escHtml(s.description) + '</div>' : '') +
|
|
2441
|
+
'</button>' +
|
|
2442
|
+
'<div class="skill-row-actions">' +
|
|
2443
|
+
'<button class="mcp-btn" data-skill-edit-source="' + escAttr(s.source) + '" data-skill-edit-directory="' + escAttr(s.directory) + '">Edit</button>' +
|
|
2444
|
+
'<button class="mcp-btn mcp-btn-danger" data-skill-delete-source="' + escAttr(s.source) + '" data-skill-delete-directory="' + escAttr(s.directory) + '" data-skill-delete-name="' + escAttr(s.name) + '">Delete</button>' +
|
|
2445
|
+
'</div>' +
|
|
2446
|
+
'</div>'
|
|
2447
|
+
).join('') + '</div>';
|
|
2448
|
+
}
|
|
2449
|
+
|
|
2290
2450
|
async function loadSkills() {
|
|
2291
2451
|
const container = document.getElementById('skills-content');
|
|
2292
2452
|
const previewEl = document.getElementById('skills-preview');
|
|
@@ -2294,25 +2454,19 @@ const adminViewScript = ` let activeConversationKey = defaultConversationKey;
|
|
|
2294
2454
|
if (previewEl) previewEl.innerHTML = '<div class="placeholder-msg">Click a skill to preview SKILL.md</div>';
|
|
2295
2455
|
try {
|
|
2296
2456
|
const data = await apiGet('/admin/api/skills?' + scopeQuery());
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
}
|
|
2301
|
-
container.innerHTML = '<div class="skills-list">' +
|
|
2302
|
-
data.skills.map((s) =>
|
|
2303
|
-
'<button class="skill-row skill-row-btn" data-skill-source="' + escAttr(s.source) + '" data-skill-directory="' + escAttr(s.directory) + '" data-skill-name="' + escAttr(s.name) + '">' +
|
|
2304
|
-
'<div class="skill-name">' + escHtml(s.name) + '<span class="skill-source skill-source-' + s.source + '">' + s.source + '</span></div>' +
|
|
2305
|
-
(s.description ? '<div class="skill-desc">' + escHtml(s.description) + '</div>' : '') +
|
|
2306
|
-
'</button>'
|
|
2307
|
-
).join('') + '</div>';
|
|
2308
|
-
|
|
2457
|
+
skillsCache = Array.isArray(data.skills) ? data.skills : [];
|
|
2458
|
+
renderSkillsInto('skills-content', skillsCache);
|
|
2459
|
+
renderSkillsInto('global-skills-content', skillsCache, 'global');
|
|
2309
2460
|
} catch (err) {
|
|
2310
2461
|
container.innerHTML = '<div class="err-msg">' + escHtml(err.message) + '</div>';
|
|
2311
2462
|
}
|
|
2312
2463
|
}
|
|
2313
2464
|
|
|
2314
|
-
|
|
2315
|
-
|
|
2465
|
+
const loadGlobalSkills = loadSkills;
|
|
2466
|
+
|
|
2467
|
+
async function previewSkillInto(previewId, source, directory, name) {
|
|
2468
|
+
const previewEl = document.getElementById(previewId);
|
|
2469
|
+
if (!previewEl) return;
|
|
2316
2470
|
if (!source || !directory) {
|
|
2317
2471
|
previewEl.innerHTML = '<div class="err-msg">Missing skill source or directory</div>';
|
|
2318
2472
|
return;
|
|
@@ -2326,11 +2480,105 @@ const adminViewScript = ` let activeConversationKey = defaultConversationKey;
|
|
|
2326
2480
|
}
|
|
2327
2481
|
}
|
|
2328
2482
|
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2483
|
+
async function deleteSkill(source, directory, name) {
|
|
2484
|
+
if (!confirm('Delete skill "' + (name || directory) + '" (' + source + ')?')) return;
|
|
2485
|
+
try {
|
|
2486
|
+
await apiPost('/admin/api/skills/mutate', { action: 'delete', source: source, directory: directory, ...scopeBody() });
|
|
2487
|
+
await loadSkills();
|
|
2488
|
+
} catch (err) {
|
|
2489
|
+
alert(err.message);
|
|
2490
|
+
}
|
|
2491
|
+
}
|
|
2492
|
+
|
|
2493
|
+
function bindSkillListEvents(containerId, previewId) {
|
|
2494
|
+
const el = document.getElementById(containerId);
|
|
2495
|
+
if (!el) return;
|
|
2496
|
+
el.addEventListener('click', (event) => {
|
|
2497
|
+
const previewBtn = event.target.closest('[data-skill-source]');
|
|
2498
|
+
if (previewBtn) {
|
|
2499
|
+
previewSkillInto(previewId, previewBtn.dataset.skillSource, previewBtn.dataset.skillDirectory, previewBtn.dataset.skillName);
|
|
2500
|
+
return;
|
|
2501
|
+
}
|
|
2502
|
+
const editBtn = event.target.closest('[data-skill-edit-source]');
|
|
2503
|
+
if (editBtn) {
|
|
2504
|
+
const skill = skillsCache.find((s) => s.source === editBtn.dataset.skillEditSource && s.directory === editBtn.dataset.skillEditDirectory);
|
|
2505
|
+
if (skill) void openSkillDialog(skill.source, skill);
|
|
2506
|
+
return;
|
|
2507
|
+
}
|
|
2508
|
+
const deleteBtn = event.target.closest('[data-skill-delete-source]');
|
|
2509
|
+
if (deleteBtn) {
|
|
2510
|
+
void deleteSkill(deleteBtn.dataset.skillDeleteSource, deleteBtn.dataset.skillDeleteDirectory, deleteBtn.dataset.skillDeleteName);
|
|
2511
|
+
}
|
|
2512
|
+
});
|
|
2513
|
+
}
|
|
2514
|
+
bindSkillListEvents('skills-content', 'skills-preview');
|
|
2515
|
+
bindSkillListEvents('global-skills-content', 'global-skills-preview');
|
|
2516
|
+
|
|
2517
|
+
let editingSkill = null;
|
|
2518
|
+
let pendingSkillScope = null;
|
|
2519
|
+
|
|
2520
|
+
async function openSkillDialog(scope, existing) {
|
|
2521
|
+
editingSkill = existing ? { source: existing.source, directory: existing.directory } : null;
|
|
2522
|
+
document.getElementById('skill-dialog-eyebrow').textContent = scope === 'global' ? 'Global skill' : 'Conversation skill';
|
|
2523
|
+
document.getElementById('skill-dialog-title').textContent = existing ? 'Edit skill' : 'New skill';
|
|
2524
|
+
const dirInput = document.getElementById('skill-dialog-directory');
|
|
2525
|
+
dirInput.value = existing ? existing.directory : '';
|
|
2526
|
+
dirInput.disabled = Boolean(existing);
|
|
2527
|
+
document.getElementById('skill-dialog-name').value = existing ? existing.name : '';
|
|
2528
|
+
document.getElementById('skill-dialog-description').value = existing ? existing.description : '';
|
|
2529
|
+
const contentEl = document.getElementById('skill-dialog-content');
|
|
2530
|
+
contentEl.value = '';
|
|
2531
|
+
const dialogError = document.getElementById('skill-dialog-error');
|
|
2532
|
+
dialogError.style.display = 'none';
|
|
2533
|
+
dialogError.textContent = '';
|
|
2534
|
+
pendingSkillScope = scope;
|
|
2535
|
+
if (existing) {
|
|
2536
|
+
contentEl.value = 'Loading…';
|
|
2537
|
+
try {
|
|
2538
|
+
const data = await apiGet('/admin/api/skills/file?' + scopeQuery() + '&source=' + encodeURIComponent(existing.source) + '&directory=' + encodeURIComponent(existing.directory));
|
|
2539
|
+
contentEl.value = data.binary ? '' : (data.content || '').replace(/^---[\\s\\S]*?---\\n?/, '').trim();
|
|
2540
|
+
} catch (err) {
|
|
2541
|
+
contentEl.value = '';
|
|
2542
|
+
dialogError.textContent = 'Failed to load current content: ' + err.message;
|
|
2543
|
+
dialogError.style.display = 'block';
|
|
2544
|
+
}
|
|
2545
|
+
}
|
|
2546
|
+
document.getElementById('skill-dialog').showModal();
|
|
2547
|
+
}
|
|
2548
|
+
|
|
2549
|
+
function closeSkillDialog() {
|
|
2550
|
+
editingSkill = null;
|
|
2551
|
+
pendingSkillScope = null;
|
|
2552
|
+
document.getElementById('skill-dialog').close();
|
|
2553
|
+
}
|
|
2554
|
+
|
|
2555
|
+
async function submitSkillDialog(btn) {
|
|
2556
|
+
const dialogError = document.getElementById('skill-dialog-error');
|
|
2557
|
+
dialogError.style.display = 'none';
|
|
2558
|
+
dialogError.textContent = '';
|
|
2559
|
+
const directory = document.getElementById('skill-dialog-directory').value.trim();
|
|
2560
|
+
const name = document.getElementById('skill-dialog-name').value.trim() || directory;
|
|
2561
|
+
const description = document.getElementById('skill-dialog-description').value.trim();
|
|
2562
|
+
const content = document.getElementById('skill-dialog-content').value;
|
|
2563
|
+
if (!directory) { dialogError.textContent = 'Directory is required'; dialogError.style.display = 'block'; return; }
|
|
2564
|
+
if (!description) { dialogError.textContent = 'Description is required'; dialogError.style.display = 'block'; return; }
|
|
2565
|
+
const source = editingSkill ? editingSkill.source : pendingSkillScope;
|
|
2566
|
+
btn.disabled = true;
|
|
2567
|
+
btn.textContent = 'Saving…';
|
|
2568
|
+
try {
|
|
2569
|
+
await apiPost('/admin/api/skills/mutate', {
|
|
2570
|
+
action: 'save', source: source, directory: directory, name: name, description: description, content: content, ...scopeBody(),
|
|
2571
|
+
});
|
|
2572
|
+
closeSkillDialog();
|
|
2573
|
+
await loadSkills();
|
|
2574
|
+
} catch (err) {
|
|
2575
|
+
dialogError.textContent = err.message;
|
|
2576
|
+
dialogError.style.display = 'block';
|
|
2577
|
+
} finally {
|
|
2578
|
+
btn.disabled = false;
|
|
2579
|
+
btn.textContent = 'Save';
|
|
2580
|
+
}
|
|
2581
|
+
}
|
|
2334
2582
|
|
|
2335
2583
|
// ── Vault (Login link) ───────────────────────────────────────────────────────
|
|
2336
2584
|
|
|
@@ -2673,26 +2921,6 @@ const adminViewScript = ` let activeConversationKey = defaultConversationKey;
|
|
|
2673
2921
|
await saveSetting(btn, result, 'settings/slack', { replyMode }, 'Save Slack');
|
|
2674
2922
|
}
|
|
2675
2923
|
|
|
2676
|
-
async function loadGlobalSkills() {
|
|
2677
|
-
const container = document.getElementById('global-skills-content');
|
|
2678
|
-
container.innerHTML = '<div class="loading-msg">Loading…</div>';
|
|
2679
|
-
try {
|
|
2680
|
-
// Reuse skills endpoint scoped to a conversation that doesn't have any of its own; the global half is what we want.
|
|
2681
|
-
const data = await apiGet('/admin/api/skills?' + scopeQuery());
|
|
2682
|
-
const globals = data.skills.filter((s) => s.source === 'global');
|
|
2683
|
-
if (globals.length === 0) {
|
|
2684
|
-
container.innerHTML = '<div class="empty-state">No global skills</div>';
|
|
2685
|
-
return;
|
|
2686
|
-
}
|
|
2687
|
-
container.innerHTML = '<div class="skills-list">' + globals.map((s) =>
|
|
2688
|
-
'<div class="skill-row"><div class="skill-name">' + escHtml(s.name) + '</div>' +
|
|
2689
|
-
(s.description ? '<div class="skill-desc">' + escHtml(s.description) + '</div>' : '') + '</div>'
|
|
2690
|
-
).join('') + '</div>';
|
|
2691
|
-
} catch (err) {
|
|
2692
|
-
container.innerHTML = '<div class="err-msg">' + escHtml(err.message) + '</div>';
|
|
2693
|
-
}
|
|
2694
|
-
}
|
|
2695
|
-
|
|
2696
2924
|
// ── Init ─────────────────────────────────────────────────────────────────────
|
|
2697
2925
|
|
|
2698
2926
|
// Sections load on demand (see initSections); loadSettingsPanel already
|
|
@@ -2831,6 +3059,7 @@ const adminViewStyles = `
|
|
|
2831
3059
|
font: 500 0.84rem/1.2 'DM Sans', sans-serif; cursor: pointer;
|
|
2832
3060
|
}
|
|
2833
3061
|
.refresh-btn:hover { background: rgba(0,0,0,0.06); color: var(--text); }
|
|
3062
|
+
.sect-head-actions { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
|
|
2834
3063
|
|
|
2835
3064
|
/* ── Workspace ──────────────────────────────────────────────────────── */
|
|
2836
3065
|
|
|
@@ -2881,11 +3110,14 @@ const adminViewStyles = `
|
|
|
2881
3110
|
.skill-row {
|
|
2882
3111
|
padding: 10px 12px; border: 1px solid var(--border); border-radius: 10px;
|
|
2883
3112
|
background: rgba(0,0,0,0.02);
|
|
3113
|
+
display: flex; align-items: flex-start; justify-content: space-between; gap: 10px;
|
|
2884
3114
|
}
|
|
2885
3115
|
.skill-row-btn {
|
|
2886
|
-
width:
|
|
3116
|
+
flex: 1 1 auto; min-width: 0; text-align: left; cursor: pointer; font-family: inherit;
|
|
3117
|
+
background: transparent; border: none; padding: 0;
|
|
2887
3118
|
}
|
|
2888
|
-
.skill-row-btn:hover {
|
|
3119
|
+
.skill-row-btn:hover .skill-name { color: var(--accent); }
|
|
3120
|
+
.skill-row-actions { display: flex; gap: 6px; flex-shrink: 0; }
|
|
2889
3121
|
.skill-name {
|
|
2890
3122
|
font-weight: 650; font-size: 0.9rem; color: var(--text);
|
|
2891
3123
|
display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
|