shadok-ai 0.11.6 → 0.11.7
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 +7 -5
- package/package.json +1 -1
- package/public/index.html +144 -14
package/README.md
CHANGED
|
@@ -92,11 +92,13 @@ Enter to skip; you can add it later from the web UI).
|
|
|
92
92
|
and three go-to-market roles (paid, organic, support). Each one is yours to
|
|
93
93
|
edit, and a role you delete stays deleted.
|
|
94
94
|
- **Secret vault** — stored under `~/.shadok-ai`, never in your repo, injected
|
|
95
|
-
as env vars into the agents that need them.
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
95
|
+
as env vars into the agents that need them. Add or **update** a secret from the
|
|
96
|
+
panel, tick the **profiles** it should feed right there, and reload the running
|
|
97
|
+
agents on those profiles in one click so a rotated value takes effect now. An
|
|
98
|
+
agent that **obtains** a credential (a CLI login, a key it just provisioned)
|
|
99
|
+
can add it to the vault itself so the next agent doesn't start from nothing —
|
|
100
|
+
write-only, the value piped in rather than typed as an argument, and never
|
|
101
|
+
overwriting an existing name without you saying so.
|
|
100
102
|
- **Scheduled prompts** — give a channel a recurring prompt (every N minutes, or
|
|
101
103
|
daily at HH:MM in a time zone you choose) for monitoring and reporting. Each
|
|
102
104
|
schedule can carry a **deterministic guard command** that runs *without the
|
package/package.json
CHANGED
package/public/index.html
CHANGED
|
@@ -1497,7 +1497,11 @@
|
|
|
1497
1497
|
#secretsList li { display: flex; justify-content: space-between; align-items: center; padding: 6px 8px; border-radius: 6px; }
|
|
1498
1498
|
#secretsList li:nth-child(odd) { background: rgba(255,255,255,.04); }
|
|
1499
1499
|
#secretsList code { font-size: 13px; }
|
|
1500
|
-
#secretsList
|
|
1500
|
+
#secretsList .s-actions { display: flex; gap: 2px; flex: none; }
|
|
1501
|
+
#secretsList button { background: none; border: none; cursor: pointer; font-size: 14px; padding: 0 4px; }
|
|
1502
|
+
#secretsList .s-del { color: #e26; }
|
|
1503
|
+
#secretsList .s-edit { color: var(--text-dim); }
|
|
1504
|
+
#secretsList .s-edit:hover { color: var(--amber); }
|
|
1501
1505
|
/* Le panneau Users n'avait AUCUN style : d'où les puces brutes et le nom, le
|
|
1502
1506
|
rôle et les boutons empilés les uns sous les autres. Même traitement que
|
|
1503
1507
|
#profilesList, dont il est le frère. */
|
|
@@ -1518,9 +1522,21 @@
|
|
|
1518
1522
|
#inviteBox .ib-url { margin-top: 6px; font-family: var(--mono); font-size: 12px; line-height: 1.45; word-break: break-all; user-select: all; }
|
|
1519
1523
|
#inviteBox button { background: none; border: 1px solid rgba(255,255,255,.15); border-radius: 5px; color: inherit; cursor: pointer; font-size: 12px; padding: 2px 8px; flex: none; }
|
|
1520
1524
|
#inviteBox button:hover { border-color: var(--amber); color: var(--amber); }
|
|
1521
|
-
#secretsAdd { display: flex; gap: 8px; }
|
|
1522
|
-
#secretsAdd
|
|
1523
|
-
#secretsAdd
|
|
1525
|
+
#secretsAdd { display: flex; flex-direction: column; gap: 8px; }
|
|
1526
|
+
#secretsAdd .sec-add-row { display: flex; gap: 8px; }
|
|
1527
|
+
#secretsAdd input { flex: 1; min-width: 0; padding: 6px 8px; border-radius: 6px; border: 1px solid rgba(255,255,255,.15); background: rgba(0,0,0,.25); color: inherit; font-size: 16px; }
|
|
1528
|
+
#secretsAdd #secretSubmit { flex: none; padding: 6px 12px; border-radius: 6px; border: 1px solid rgba(255,255,255,.15); background: rgba(255,255,255,.08); color: inherit; cursor: pointer; }
|
|
1529
|
+
#secretKey[readonly] { opacity: .65; }
|
|
1530
|
+
.sec-attach { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 10px; }
|
|
1531
|
+
.sec-attach-label { font-size: 12px; color: var(--text-dim); }
|
|
1532
|
+
.sec-profiles { display: flex; flex-wrap: wrap; gap: 6px; }
|
|
1533
|
+
.sec-profiles label { display: inline-flex; align-items: center; gap: 5px; font-size: 12px; padding: 3px 9px; border: 1px solid var(--line); border-radius: 14px; cursor: pointer; }
|
|
1534
|
+
.sec-profiles label:hover { border-color: var(--amber); }
|
|
1535
|
+
.sec-profiles input { accent-color: var(--amber); margin: 0; }
|
|
1536
|
+
.sec-reload { margin-top: 10px; border: 1px solid var(--amber); border-radius: 8px; padding: 9px 11px; background: color-mix(in srgb, var(--amber) 8%, transparent); font-size: 12.5px; display: flex; align-items: center; gap: 10px; }
|
|
1537
|
+
.sec-reload span { flex: 1; }
|
|
1538
|
+
.sec-reload button { flex: none; padding: 5px 11px; border-radius: 6px; border: 1px solid var(--amber); background: none; color: var(--amber); cursor: pointer; font: inherit; font-size: 12px; }
|
|
1539
|
+
.sec-reload button:hover { background: color-mix(in srgb, var(--amber) 15%, transparent); }
|
|
1524
1540
|
</style>
|
|
1525
1541
|
</head>
|
|
1526
1542
|
<body>
|
|
@@ -1630,10 +1646,18 @@
|
|
|
1630
1646
|
<div class="pin-body">
|
|
1631
1647
|
<ul id="secretsList"></ul>
|
|
1632
1648
|
<form id="secretsAdd">
|
|
1633
|
-
<
|
|
1634
|
-
|
|
1635
|
-
|
|
1649
|
+
<div class="sec-add-row">
|
|
1650
|
+
<input id="secretKey" placeholder="NAME" autocomplete="off" spellcheck="false" />
|
|
1651
|
+
<input id="secretVal" placeholder="value" autocomplete="off" spellcheck="false" />
|
|
1652
|
+
<button type="submit" id="secretSubmit">Add</button>
|
|
1653
|
+
</div>
|
|
1654
|
+
<div id="secretAttach" class="sec-attach" hidden>
|
|
1655
|
+
<span class="sec-attach-label">Inject into profiles:</span>
|
|
1656
|
+
<div id="secretProfiles" class="sec-profiles"></div>
|
|
1657
|
+
<button type="button" id="secretEditCancel" class="linkish" hidden>cancel edit</button>
|
|
1658
|
+
</div>
|
|
1636
1659
|
</form>
|
|
1660
|
+
<div id="secretsReload" class="sec-reload" hidden></div>
|
|
1637
1661
|
</div>
|
|
1638
1662
|
</div>
|
|
1639
1663
|
</div>
|
|
@@ -5842,24 +5866,122 @@
|
|
|
5842
5866
|
}
|
|
5843
5867
|
code.appendChild(tag);
|
|
5844
5868
|
}
|
|
5869
|
+
const actions = document.createElement("div");
|
|
5870
|
+
actions.className = "s-actions";
|
|
5871
|
+
const edit = document.createElement("button");
|
|
5872
|
+
edit.type = "button"; edit.className = "s-edit"; edit.textContent = "✎";
|
|
5873
|
+
edit.title = "Update this secret's value / the profiles it feeds";
|
|
5874
|
+
edit.addEventListener("click", () => startEditSecret(k));
|
|
5845
5875
|
const del = document.createElement("button");
|
|
5846
|
-
del.textContent = "✕"; del.title = "Remove from vault";
|
|
5876
|
+
del.type = "button"; del.className = "s-del"; del.textContent = "✕"; del.title = "Remove from vault";
|
|
5847
5877
|
del.addEventListener("click", async () => {
|
|
5848
5878
|
await fetch("/secrets?name=" + encodeURIComponent(k), { method: "DELETE" });
|
|
5879
|
+
if (editingSecret === k) resetSecretForm();
|
|
5849
5880
|
await loadVault(); renderSecrets();
|
|
5850
5881
|
});
|
|
5851
|
-
|
|
5882
|
+
actions.append(edit, del);
|
|
5883
|
+
li.append(code, actions);
|
|
5852
5884
|
ul.appendChild(li);
|
|
5853
5885
|
}
|
|
5854
5886
|
}
|
|
5887
|
+
|
|
5888
|
+
// ── Add / update a secret, and wire it to profiles ──────────────────────
|
|
5889
|
+
// The vault add form doubles as an editor: ✎ on a row loads the name (locked)
|
|
5890
|
+
// and the profiles it already feeds, so you can rotate the value or change
|
|
5891
|
+
// which profiles get it in one place.
|
|
5892
|
+
let editingSecret = null;
|
|
5893
|
+
/** Paint a checkbox per (non-managed) profile; `checked` is the set to tick. */
|
|
5894
|
+
function renderSecretProfiles(checked) {
|
|
5895
|
+
const box = $("secretProfiles");
|
|
5896
|
+
box.replaceChildren();
|
|
5897
|
+
const names = profileCache
|
|
5898
|
+
.map((p) => p.name)
|
|
5899
|
+
.filter((n) => !(window.isManagedProfile && window.isManagedProfile(n)))
|
|
5900
|
+
.sort();
|
|
5901
|
+
$("secretAttach").hidden = names.length === 0;
|
|
5902
|
+
for (const n of names) {
|
|
5903
|
+
const label = document.createElement("label");
|
|
5904
|
+
const cb = document.createElement("input");
|
|
5905
|
+
cb.type = "checkbox"; cb.value = n; cb.checked = checked.has(n);
|
|
5906
|
+
const span = document.createElement("span");
|
|
5907
|
+
span.textContent = n;
|
|
5908
|
+
label.append(cb, span);
|
|
5909
|
+
box.appendChild(label);
|
|
5910
|
+
}
|
|
5911
|
+
}
|
|
5912
|
+
function checkedProfiles() {
|
|
5913
|
+
return [...$("secretProfiles").querySelectorAll("input:checked")].map((c) => c.value);
|
|
5914
|
+
}
|
|
5915
|
+
function resetSecretForm() {
|
|
5916
|
+
editingSecret = null;
|
|
5917
|
+
$("secretKey").value = ""; $("secretKey").readOnly = false;
|
|
5918
|
+
$("secretVal").value = ""; $("secretVal").placeholder = "value";
|
|
5919
|
+
$("secretSubmit").textContent = "Add";
|
|
5920
|
+
$("secretEditCancel").hidden = true;
|
|
5921
|
+
renderSecretProfiles(new Set());
|
|
5922
|
+
}
|
|
5923
|
+
function startEditSecret(name) {
|
|
5924
|
+
editingSecret = name;
|
|
5925
|
+
$("secretKey").value = name; $("secretKey").readOnly = true;
|
|
5926
|
+
$("secretVal").value = ""; $("secretVal").placeholder = "new value (blank = keep)";
|
|
5927
|
+
$("secretSubmit").textContent = "Update";
|
|
5928
|
+
$("secretEditCancel").hidden = false;
|
|
5929
|
+
const users = window.secretUsers ? window.secretUsers(profileCache, name) : [];
|
|
5930
|
+
renderSecretProfiles(new Set(users));
|
|
5931
|
+
$("secretVal").focus();
|
|
5932
|
+
}
|
|
5933
|
+
/** Write the checkbox choices back onto every affected profile (add where now
|
|
5934
|
+
* ticked, remove where now unticked), preserving each profile's other fields —
|
|
5935
|
+
* crucially its systemPrompt, or omitting it would silently un-edit the role. */
|
|
5936
|
+
async function syncSecretToProfiles(name, wanted) {
|
|
5937
|
+
const want = new Set(wanted);
|
|
5938
|
+
const changed = [];
|
|
5939
|
+
for (const p of profileCache) {
|
|
5940
|
+
if (window.isManagedProfile && window.isManagedProfile(p.name)) continue;
|
|
5941
|
+
const has = (p.secrets || []).includes(name);
|
|
5942
|
+
if (want.has(p.name) === has) continue; // no change for this profile
|
|
5943
|
+
const secrets = want.has(p.name)
|
|
5944
|
+
? [...(p.secrets || []), name]
|
|
5945
|
+
: (p.secrets || []).filter((s) => s !== name);
|
|
5946
|
+
const body = { name: p.name, deny: p.deny, allow: p.allow, model: p.model, secrets };
|
|
5947
|
+
if (typeof p.systemPrompt === "string") body.systemPrompt = p.systemPrompt; // keep an edited prompt
|
|
5948
|
+
await fetch("/profiles", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) });
|
|
5949
|
+
changed.push(p.name);
|
|
5950
|
+
}
|
|
5951
|
+
if (changed.length) await loadProfilesInto();
|
|
5952
|
+
return changed;
|
|
5953
|
+
}
|
|
5954
|
+
/** Offer to reload the LIVE agents whose profile now feeds this secret — the
|
|
5955
|
+
* value only reaches a process at spawn, so a running one needs a respawn. */
|
|
5956
|
+
function offerReload(secretName) {
|
|
5957
|
+
const users = new Set(window.secretUsers ? window.secretUsers(profileCache, secretName) : []);
|
|
5958
|
+
// Only LIVE agents: a stopped one will get the value when it next launches.
|
|
5959
|
+
const agents = tabs.filter((t) => users.has(t.profile) && t.ws && t.ws.readyState === WebSocket.OPEN);
|
|
5960
|
+
const box = $("secretsReload");
|
|
5961
|
+
box.replaceChildren();
|
|
5962
|
+
if (!agents.length) { box.hidden = true; return; }
|
|
5963
|
+
const txt = document.createElement("span");
|
|
5964
|
+
txt.textContent =
|
|
5965
|
+
agents.length === 1
|
|
5966
|
+
? `1 running agent (${agents[0].nameEl.textContent}) uses “${secretName}”. Reload it to pick up the new value?`
|
|
5967
|
+
: `${agents.length} running agents use “${secretName}”. Reload them to pick up the new value?`;
|
|
5968
|
+
const go = document.createElement("button");
|
|
5969
|
+
go.type = "button"; go.textContent = "Reload " + (agents.length === 1 ? "it" : "all");
|
|
5970
|
+
go.addEventListener("click", () => { agents.forEach(reloadTab); box.hidden = true; });
|
|
5971
|
+
box.append(txt, go);
|
|
5972
|
+
box.hidden = false;
|
|
5973
|
+
}
|
|
5855
5974
|
// profileCache feeds the "no profile uses it" warning: it must be fresh when
|
|
5856
5975
|
// the panel opens, or we would show a stale diagnosis.
|
|
5857
5976
|
$("secretsBtn").addEventListener("click", async () => {
|
|
5858
5977
|
await Promise.all([loadVault(), loadProfilesInto()]);
|
|
5859
5978
|
renderSecrets();
|
|
5979
|
+
resetSecretForm();
|
|
5980
|
+
$("secretsReload").hidden = true;
|
|
5860
5981
|
$("secretsOverlay").hidden = false;
|
|
5861
5982
|
$("secretKey").focus();
|
|
5862
5983
|
});
|
|
5984
|
+
$("secretEditCancel").addEventListener("click", resetSecretForm);
|
|
5863
5985
|
// The missing link: from the secrets, go and attach them to a profile.
|
|
5864
5986
|
$("secretsToProfiles").addEventListener("click", () => {
|
|
5865
5987
|
$("secretsOverlay").hidden = true;
|
|
@@ -5924,13 +6046,21 @@
|
|
|
5924
6046
|
e.preventDefault();
|
|
5925
6047
|
const name = $("secretKey").value.trim();
|
|
5926
6048
|
const value = $("secretVal").value;
|
|
6049
|
+
const wanted = checkedProfiles();
|
|
5927
6050
|
if (!name) return;
|
|
5928
|
-
//
|
|
5929
|
-
//
|
|
5930
|
-
//
|
|
5931
|
-
|
|
5932
|
-
|
|
6051
|
+
// Editing an existing secret with the value left BLANK means "keep the value,
|
|
6052
|
+
// just change the profiles" — the vault is write-only, so we must not clobber
|
|
6053
|
+
// it with "". A new secret, or a value typed, is written (overwrite: true
|
|
6054
|
+
// because a human at the vault is acting deliberately; the flag only stops an
|
|
6055
|
+
// AGENT replacing a credential silently).
|
|
6056
|
+
const keepValue = editingSecret && value === "";
|
|
6057
|
+
if (!keepValue) {
|
|
6058
|
+
await fetch("/secrets", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name, value, overwrite: true }) });
|
|
6059
|
+
}
|
|
6060
|
+
await syncSecretToProfiles(name, wanted); // refreshes profileCache
|
|
5933
6061
|
await loadVault(); renderSecrets();
|
|
6062
|
+
offerReload(name); // propose reloading the live agents that now feed on it
|
|
6063
|
+
resetSecretForm();
|
|
5934
6064
|
$("secretKey").focus();
|
|
5935
6065
|
});
|
|
5936
6066
|
// Parse a pasted .env-style block → [{name, value}]. Handles `export
|