@ouro.bot/cli 0.1.0-alpha.6 → 0.1.0-alpha.61
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/AdoptionSpecialist.ouro/agent.json +70 -9
- package/AdoptionSpecialist.ouro/psyche/SOUL.md +5 -2
- package/AdoptionSpecialist.ouro/psyche/identities/monty.md +2 -2
- package/README.md +147 -205
- package/assets/ouroboros.png +0 -0
- package/changelog.json +334 -0
- package/dist/heart/active-work.js +178 -0
- package/dist/heart/bridges/manager.js +358 -0
- package/dist/heart/bridges/state-machine.js +135 -0
- package/dist/heart/bridges/store.js +123 -0
- package/dist/heart/config.js +57 -23
- package/dist/heart/core.js +236 -90
- package/dist/heart/cross-chat-delivery.js +146 -0
- package/dist/heart/daemon/agent-discovery.js +81 -0
- package/dist/heart/daemon/auth-flow.js +351 -0
- package/dist/heart/daemon/daemon-cli.js +1175 -232
- package/dist/heart/daemon/daemon-entry.js +55 -6
- package/dist/heart/daemon/daemon-runtime-sync.js +212 -0
- package/dist/heart/daemon/daemon.js +189 -10
- package/dist/heart/daemon/hatch-animation.js +10 -3
- package/dist/heart/daemon/hatch-flow.js +4 -82
- package/dist/heart/daemon/hooks/bundle-meta.js +92 -0
- package/dist/heart/daemon/launchd.js +159 -0
- package/dist/heart/daemon/log-tailer.js +4 -3
- package/dist/heart/daemon/message-router.js +17 -8
- package/dist/heart/daemon/ouro-bot-entry.js +0 -0
- package/dist/heart/daemon/ouro-bot-global-installer.js +128 -0
- package/dist/heart/daemon/ouro-entry.js +0 -0
- package/dist/heart/daemon/ouro-path-installer.js +178 -0
- package/dist/heart/daemon/ouro-uti.js +11 -2
- package/dist/heart/daemon/process-manager.js +14 -1
- package/dist/heart/daemon/run-hooks.js +37 -0
- package/dist/heart/daemon/runtime-logging.js +58 -15
- package/dist/heart/daemon/runtime-metadata.js +219 -0
- package/dist/heart/daemon/runtime-mode.js +67 -0
- package/dist/heart/daemon/sense-manager.js +307 -0
- package/dist/heart/daemon/skill-management-installer.js +73 -0
- package/dist/heart/daemon/socket-client.js +202 -0
- package/dist/heart/daemon/specialist-orchestrator.js +53 -84
- package/dist/heart/daemon/specialist-prompt.js +64 -5
- package/dist/heart/daemon/specialist-tools.js +213 -58
- package/dist/heart/daemon/staged-restart.js +114 -0
- package/dist/heart/daemon/thoughts.js +379 -0
- package/dist/heart/daemon/update-checker.js +111 -0
- package/dist/heart/daemon/update-hooks.js +138 -0
- package/dist/heart/daemon/wrapper-publish-guard.js +86 -0
- package/dist/heart/delegation.js +62 -0
- package/dist/heart/identity.js +122 -19
- package/dist/heart/kicks.js +1 -19
- package/dist/heart/model-capabilities.js +40 -0
- package/dist/heart/progress-story.js +42 -0
- package/dist/heart/providers/anthropic.js +74 -9
- package/dist/heart/providers/azure.js +86 -7
- package/dist/heart/providers/minimax.js +4 -0
- package/dist/heart/providers/openai-codex.js +12 -3
- package/dist/heart/safe-workspace.js +228 -0
- package/dist/heart/sense-truth.js +61 -0
- package/dist/heart/session-activity.js +169 -0
- package/dist/heart/session-recall.js +116 -0
- package/dist/heart/streaming.js +100 -22
- package/dist/heart/target-resolution.js +123 -0
- package/dist/heart/turn-coordinator.js +28 -0
- package/dist/mind/associative-recall.js +14 -2
- package/dist/mind/bundle-manifest.js +70 -0
- package/dist/mind/context.js +27 -11
- package/dist/mind/first-impressions.js +16 -2
- package/dist/mind/friends/channel.js +35 -0
- package/dist/mind/friends/group-context.js +144 -0
- package/dist/mind/friends/store-file.js +19 -0
- package/dist/mind/friends/trust-explanation.js +74 -0
- package/dist/mind/friends/types.js +8 -0
- package/dist/mind/memory.js +27 -26
- package/dist/mind/pending.js +72 -9
- package/dist/mind/phrases.js +1 -0
- package/dist/mind/prompt.js +299 -77
- package/dist/mind/token-estimate.js +8 -12
- package/dist/nerves/cli-logging.js +15 -2
- package/dist/nerves/coverage/run-artifacts.js +1 -1
- package/dist/repertoire/ado-client.js +4 -2
- package/dist/repertoire/coding/feedback.js +134 -0
- package/dist/repertoire/coding/index.js +4 -1
- package/dist/repertoire/coding/manager.js +62 -4
- package/dist/repertoire/coding/spawner.js +3 -3
- package/dist/repertoire/coding/tools.js +41 -2
- package/dist/repertoire/data/ado-endpoints.json +188 -0
- package/dist/repertoire/skills.js +3 -26
- package/dist/repertoire/tasks/board.js +12 -0
- package/dist/repertoire/tasks/index.js +23 -9
- package/dist/repertoire/tasks/transitions.js +1 -2
- package/dist/repertoire/tools-base.js +629 -251
- package/dist/repertoire/tools-bluebubbles.js +93 -0
- package/dist/repertoire/tools-teams.js +58 -25
- package/dist/repertoire/tools.js +92 -48
- package/dist/senses/bluebubbles-client.js +210 -5
- package/dist/senses/bluebubbles-entry.js +2 -0
- package/dist/senses/bluebubbles-inbound-log.js +109 -0
- package/dist/senses/bluebubbles-media.js +339 -0
- package/dist/senses/bluebubbles-model.js +12 -4
- package/dist/senses/bluebubbles-mutation-log.js +45 -5
- package/dist/senses/bluebubbles-runtime-state.js +109 -0
- package/dist/senses/bluebubbles-session-cleanup.js +72 -0
- package/dist/senses/bluebubbles.js +890 -45
- package/dist/senses/cli-layout.js +87 -0
- package/dist/senses/cli.js +345 -144
- package/dist/senses/continuity.js +94 -0
- package/dist/senses/debug-activity.js +148 -0
- package/dist/senses/inner-dialog-worker.js +47 -18
- package/dist/senses/inner-dialog.js +330 -84
- package/dist/senses/pipeline.js +278 -0
- package/dist/senses/teams.js +570 -129
- package/dist/senses/trust-gate.js +112 -2
- package/package.json +14 -3
- package/subagents/README.md +4 -70
- package/dist/heart/daemon/specialist-session.js +0 -142
- package/dist/heart/daemon/subagent-installer.js +0 -125
- package/dist/inner-worker-entry.js +0 -4
- package/subagents/work-doer.md +0 -233
- package/subagents/work-merger.md +0 -624
- package/subagents/work-planner.md +0 -373
|
@@ -34,6 +34,11 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.CANONICAL_BUNDLE_MANIFEST = void 0;
|
|
37
|
+
exports.getChangelogPath = getChangelogPath;
|
|
38
|
+
exports.getPackageVersion = getPackageVersion;
|
|
39
|
+
exports.createBundleMeta = createBundleMeta;
|
|
40
|
+
exports.backfillBundleMeta = backfillBundleMeta;
|
|
41
|
+
exports.resetBackfillTracking = resetBackfillTracking;
|
|
37
42
|
exports.isCanonicalBundlePath = isCanonicalBundlePath;
|
|
38
43
|
exports.findNonCanonicalBundlePaths = findNonCanonicalBundlePaths;
|
|
39
44
|
const fs = __importStar(require("fs"));
|
|
@@ -41,6 +46,7 @@ const path = __importStar(require("path"));
|
|
|
41
46
|
const runtime_1 = require("../nerves/runtime");
|
|
42
47
|
exports.CANONICAL_BUNDLE_MANIFEST = [
|
|
43
48
|
{ path: "agent.json", kind: "file" },
|
|
49
|
+
{ path: "bundle-meta.json", kind: "file" },
|
|
44
50
|
{ path: "psyche/SOUL.md", kind: "file" },
|
|
45
51
|
{ path: "psyche/IDENTITY.md", kind: "file" },
|
|
46
52
|
{ path: "psyche/LORE.md", kind: "file" },
|
|
@@ -48,11 +54,75 @@ exports.CANONICAL_BUNDLE_MANIFEST = [
|
|
|
48
54
|
{ path: "psyche/ASPIRATIONS.md", kind: "file" },
|
|
49
55
|
{ path: "psyche/memory", kind: "dir" },
|
|
50
56
|
{ path: "friends", kind: "dir" },
|
|
57
|
+
{ path: "state", kind: "dir" },
|
|
51
58
|
{ path: "tasks", kind: "dir" },
|
|
52
59
|
{ path: "skills", kind: "dir" },
|
|
53
60
|
{ path: "senses", kind: "dir" },
|
|
54
61
|
{ path: "senses/teams", kind: "dir" },
|
|
55
62
|
];
|
|
63
|
+
function getChangelogPath() {
|
|
64
|
+
const changelogPath = path.resolve(__dirname, "../../changelog.json");
|
|
65
|
+
(0, runtime_1.emitNervesEvent)({
|
|
66
|
+
component: "mind",
|
|
67
|
+
event: "mind.changelog_path_resolved",
|
|
68
|
+
message: "resolved changelog path",
|
|
69
|
+
meta: { path: changelogPath },
|
|
70
|
+
});
|
|
71
|
+
return changelogPath;
|
|
72
|
+
}
|
|
73
|
+
function getPackageVersion() {
|
|
74
|
+
const packageJsonPath = path.resolve(__dirname, "../../package.json");
|
|
75
|
+
const raw = fs.readFileSync(packageJsonPath, "utf-8");
|
|
76
|
+
const parsed = JSON.parse(raw);
|
|
77
|
+
(0, runtime_1.emitNervesEvent)({
|
|
78
|
+
component: "mind",
|
|
79
|
+
event: "mind.package_version_read",
|
|
80
|
+
message: "read package version",
|
|
81
|
+
meta: { version: parsed.version },
|
|
82
|
+
});
|
|
83
|
+
return parsed.version;
|
|
84
|
+
}
|
|
85
|
+
function createBundleMeta() {
|
|
86
|
+
return {
|
|
87
|
+
runtimeVersion: getPackageVersion(),
|
|
88
|
+
bundleSchemaVersion: 1,
|
|
89
|
+
lastUpdated: new Date().toISOString(),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
const _backfilledRoots = new Set();
|
|
93
|
+
/**
|
|
94
|
+
* If bundle-meta.json is missing from the agent root, create it with current runtime version.
|
|
95
|
+
* This backfills existing agent bundles that were created before bundle-meta.json was introduced.
|
|
96
|
+
* Only attempts once per bundleRoot per process.
|
|
97
|
+
*/
|
|
98
|
+
function backfillBundleMeta(bundleRoot) {
|
|
99
|
+
if (_backfilledRoots.has(bundleRoot))
|
|
100
|
+
return;
|
|
101
|
+
_backfilledRoots.add(bundleRoot);
|
|
102
|
+
const metaPath = path.join(bundleRoot, "bundle-meta.json");
|
|
103
|
+
try {
|
|
104
|
+
if (fs.existsSync(metaPath)) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const meta = createBundleMeta();
|
|
108
|
+
fs.writeFileSync(metaPath, JSON.stringify(meta, null, 2) + "\n", "utf-8");
|
|
109
|
+
(0, runtime_1.emitNervesEvent)({
|
|
110
|
+
component: "mind",
|
|
111
|
+
event: "mind.bundle_meta_backfill",
|
|
112
|
+
message: "backfilled missing bundle-meta.json",
|
|
113
|
+
meta: { bundleRoot },
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
// Non-blocking: if we can't write, that's okay
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Reset the backfill tracking set. Used in tests.
|
|
122
|
+
*/
|
|
123
|
+
function resetBackfillTracking() {
|
|
124
|
+
_backfilledRoots.clear();
|
|
125
|
+
}
|
|
56
126
|
const CANONICAL_FILE_PATHS = new Set(exports.CANONICAL_BUNDLE_MANIFEST
|
|
57
127
|
.filter((entry) => entry.kind === "file")
|
|
58
128
|
.map((entry) => entry.path));
|
package/dist/mind/context.js
CHANGED
|
@@ -50,17 +50,17 @@ function buildTrimmableBlocks(messages) {
|
|
|
50
50
|
let i = 0;
|
|
51
51
|
while (i < messages.length) {
|
|
52
52
|
const msg = messages[i];
|
|
53
|
-
if (msg
|
|
53
|
+
if (msg.role === "system") {
|
|
54
54
|
i++;
|
|
55
55
|
continue;
|
|
56
56
|
}
|
|
57
57
|
// Tool coherence block: assistant message with tool_calls + immediately following tool results
|
|
58
|
-
if (msg
|
|
58
|
+
if (msg.role === "assistant" && Array.isArray(msg.tool_calls) && msg.tool_calls.length > 0) {
|
|
59
59
|
const indices = [i];
|
|
60
60
|
i++;
|
|
61
61
|
while (i < messages.length) {
|
|
62
62
|
const next = messages[i];
|
|
63
|
-
if (next
|
|
63
|
+
if (next.role !== "tool")
|
|
64
64
|
break;
|
|
65
65
|
indices.push(i);
|
|
66
66
|
i++;
|
|
@@ -78,13 +78,13 @@ function buildTrimmableBlocks(messages) {
|
|
|
78
78
|
function getSystemMessageIndices(messages) {
|
|
79
79
|
const indices = [];
|
|
80
80
|
for (let i = 0; i < messages.length; i++) {
|
|
81
|
-
if (messages[i]
|
|
81
|
+
if (messages[i].role === "system")
|
|
82
82
|
indices.push(i);
|
|
83
83
|
}
|
|
84
84
|
return indices;
|
|
85
85
|
}
|
|
86
86
|
function buildTrimmedMessages(messages, kept) {
|
|
87
|
-
return messages.filter((m, idx) => m
|
|
87
|
+
return messages.filter((m, idx) => m.role === "system" || kept.has(idx));
|
|
88
88
|
}
|
|
89
89
|
function trimMessages(messages, maxTokens, contextMargin, actualTokenCount) {
|
|
90
90
|
const targetTokens = Math.floor(maxTokens * (1 - contextMargin / 100));
|
|
@@ -132,7 +132,7 @@ function trimMessages(messages, maxTokens, contextMargin, actualTokenCount) {
|
|
|
132
132
|
let remaining = actualTokenCount;
|
|
133
133
|
const kept = new Set();
|
|
134
134
|
for (let i = 0; i < messages.length; i++) {
|
|
135
|
-
if (messages[i]
|
|
135
|
+
if (messages[i].role !== "system")
|
|
136
136
|
kept.add(i);
|
|
137
137
|
}
|
|
138
138
|
// Drop oldest blocks until we fall under target.
|
|
@@ -146,7 +146,7 @@ function trimMessages(messages, maxTokens, contextMargin, actualTokenCount) {
|
|
|
146
146
|
let trimmed = buildTrimmedMessages(messages, kept);
|
|
147
147
|
// If we're still above budget after dropping everything trimmable, preserve system only.
|
|
148
148
|
if (remaining > targetTokens) {
|
|
149
|
-
trimmed = messages.filter((m) => m
|
|
149
|
+
trimmed = messages.filter((m) => m.role === "system");
|
|
150
150
|
}
|
|
151
151
|
const estimatedAfter = (0, token_estimate_1.estimateTokensForMessages)(trimmed);
|
|
152
152
|
(0, runtime_1.emitNervesEvent)({
|
|
@@ -227,7 +227,7 @@ function repairSessionMessages(messages) {
|
|
|
227
227
|
});
|
|
228
228
|
return result;
|
|
229
229
|
}
|
|
230
|
-
function saveSession(filePath, messages, lastUsage) {
|
|
230
|
+
function saveSession(filePath, messages, lastUsage, state) {
|
|
231
231
|
const violations = validateSessionMessages(messages);
|
|
232
232
|
if (violations.length > 0) {
|
|
233
233
|
(0, runtime_1.emitNervesEvent)({
|
|
@@ -243,6 +243,12 @@ function saveSession(filePath, messages, lastUsage) {
|
|
|
243
243
|
const envelope = { version: 1, messages };
|
|
244
244
|
if (lastUsage)
|
|
245
245
|
envelope.lastUsage = lastUsage;
|
|
246
|
+
if (state?.mustResolveBeforeHandoff === true || typeof state?.lastFriendActivityAt === "string") {
|
|
247
|
+
envelope.state = {
|
|
248
|
+
...(state?.mustResolveBeforeHandoff === true ? { mustResolveBeforeHandoff: true } : {}),
|
|
249
|
+
...(typeof state?.lastFriendActivityAt === "string" ? { lastFriendActivityAt: state.lastFriendActivityAt } : {}),
|
|
250
|
+
};
|
|
251
|
+
}
|
|
246
252
|
fs.writeFileSync(filePath, JSON.stringify(envelope, null, 2));
|
|
247
253
|
}
|
|
248
254
|
function loadSession(filePath) {
|
|
@@ -263,13 +269,23 @@ function loadSession(filePath) {
|
|
|
263
269
|
});
|
|
264
270
|
messages = repairSessionMessages(messages);
|
|
265
271
|
}
|
|
266
|
-
|
|
272
|
+
const rawState = data?.state && typeof data.state === "object" && data.state !== null
|
|
273
|
+
? data.state
|
|
274
|
+
: undefined;
|
|
275
|
+
const state = rawState && (rawState.mustResolveBeforeHandoff === true
|
|
276
|
+
|| typeof rawState.lastFriendActivityAt === "string")
|
|
277
|
+
? {
|
|
278
|
+
...(rawState.mustResolveBeforeHandoff === true ? { mustResolveBeforeHandoff: true } : {}),
|
|
279
|
+
...(typeof rawState.lastFriendActivityAt === "string" ? { lastFriendActivityAt: rawState.lastFriendActivityAt } : {}),
|
|
280
|
+
}
|
|
281
|
+
: undefined;
|
|
282
|
+
return { messages, lastUsage: data.lastUsage, state };
|
|
267
283
|
}
|
|
268
284
|
catch {
|
|
269
285
|
return null;
|
|
270
286
|
}
|
|
271
287
|
}
|
|
272
|
-
function postTurn(messages, sessPath, usage, hooks) {
|
|
288
|
+
function postTurn(messages, sessPath, usage, hooks, state) {
|
|
273
289
|
if (hooks?.beforeTrim) {
|
|
274
290
|
try {
|
|
275
291
|
hooks.beforeTrim([...messages]);
|
|
@@ -289,7 +305,7 @@ function postTurn(messages, sessPath, usage, hooks) {
|
|
|
289
305
|
const { maxTokens, contextMargin } = (0, config_1.getContextConfig)();
|
|
290
306
|
const trimmed = trimMessages(messages, maxTokens, contextMargin, usage?.input_tokens);
|
|
291
307
|
messages.splice(0, messages.length, ...trimmed);
|
|
292
|
-
saveSession(sessPath, messages, usage);
|
|
308
|
+
saveSession(sessPath, messages, usage, state);
|
|
293
309
|
}
|
|
294
310
|
function deleteSession(filePath) {
|
|
295
311
|
try {
|
|
@@ -11,9 +11,22 @@ exports.ONBOARDING_TOKEN_THRESHOLD = 100_000;
|
|
|
11
11
|
function isOnboarding(friend) {
|
|
12
12
|
return (friend.totalTokens ?? 0) < exports.ONBOARDING_TOKEN_THRESHOLD;
|
|
13
13
|
}
|
|
14
|
-
function
|
|
14
|
+
function hasLiveContinuityPressure(state) {
|
|
15
|
+
if (!state)
|
|
16
|
+
return false;
|
|
17
|
+
if (typeof state.currentObligation === "string" && state.currentObligation.trim().length > 0)
|
|
18
|
+
return true;
|
|
19
|
+
if (state.mustResolveBeforeHandoff === true)
|
|
20
|
+
return true;
|
|
21
|
+
if (state.hasQueuedFollowUp === true)
|
|
22
|
+
return true;
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
function getFirstImpressions(friend, state) {
|
|
15
26
|
if (!isOnboarding(friend))
|
|
16
27
|
return "";
|
|
28
|
+
if (hasLiveContinuityPressure(state))
|
|
29
|
+
return "";
|
|
17
30
|
(0, runtime_1.emitNervesEvent)({
|
|
18
31
|
component: "mind",
|
|
19
32
|
event: "mind.first_impressions",
|
|
@@ -37,7 +50,8 @@ function getFirstImpressions(friend) {
|
|
|
37
50
|
lines.push("- what do they do outside of work that they care about?");
|
|
38
51
|
lines.push("i don't ask all of these at once -- i weave them into conversation naturally, one or two at a time, and i genuinely follow up on what they share.");
|
|
39
52
|
lines.push("i introduce what i can do -- i have tools, integrations, and skills that can help them. i mention these naturally as they become relevant.");
|
|
40
|
-
lines.push("if
|
|
53
|
+
lines.push("if we're already in motion on a task, thread, or follow-up, i do not reset with a generic opener like 'hiya' or 'what do ya need help with?'. i continue directly or ask the specific next question.");
|
|
54
|
+
lines.push("only when the conversation is genuinely fresh and idle, with no active ask or thread in flight, a light opener is okay.");
|
|
41
55
|
lines.push("i save everything i learn immediately with save_friend_note -- names, roles, preferences, projects, anything. the bar is low: if i learned it, i save it.");
|
|
42
56
|
return lines.join("\n");
|
|
43
57
|
}
|
|
@@ -3,10 +3,13 @@
|
|
|
3
3
|
// Pure lookup, no I/O, cannot fail. Unknown channel gets minimal defaults.
|
|
4
4
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
5
|
exports.getChannelCapabilities = getChannelCapabilities;
|
|
6
|
+
exports.isRemoteChannel = isRemoteChannel;
|
|
7
|
+
exports.getAlwaysOnSenseNames = getAlwaysOnSenseNames;
|
|
6
8
|
const runtime_1 = require("../../nerves/runtime");
|
|
7
9
|
const CHANNEL_CAPABILITIES = {
|
|
8
10
|
cli: {
|
|
9
11
|
channel: "cli",
|
|
12
|
+
senseType: "local",
|
|
10
13
|
availableIntegrations: [],
|
|
11
14
|
supportsMarkdown: false,
|
|
12
15
|
supportsStreaming: true,
|
|
@@ -15,6 +18,7 @@ const CHANNEL_CAPABILITIES = {
|
|
|
15
18
|
},
|
|
16
19
|
teams: {
|
|
17
20
|
channel: "teams",
|
|
21
|
+
senseType: "closed",
|
|
18
22
|
availableIntegrations: ["ado", "graph", "github"],
|
|
19
23
|
supportsMarkdown: true,
|
|
20
24
|
supportsStreaming: true,
|
|
@@ -23,15 +27,26 @@ const CHANNEL_CAPABILITIES = {
|
|
|
23
27
|
},
|
|
24
28
|
bluebubbles: {
|
|
25
29
|
channel: "bluebubbles",
|
|
30
|
+
senseType: "open",
|
|
26
31
|
availableIntegrations: [],
|
|
27
32
|
supportsMarkdown: false,
|
|
28
33
|
supportsStreaming: false,
|
|
29
34
|
supportsRichCards: false,
|
|
30
35
|
maxMessageLength: Infinity,
|
|
31
36
|
},
|
|
37
|
+
inner: {
|
|
38
|
+
channel: "inner",
|
|
39
|
+
senseType: "internal",
|
|
40
|
+
availableIntegrations: [],
|
|
41
|
+
supportsMarkdown: false,
|
|
42
|
+
supportsStreaming: true,
|
|
43
|
+
supportsRichCards: false,
|
|
44
|
+
maxMessageLength: Infinity,
|
|
45
|
+
},
|
|
32
46
|
};
|
|
33
47
|
const DEFAULT_CAPABILITIES = {
|
|
34
48
|
channel: "cli",
|
|
49
|
+
senseType: "local",
|
|
35
50
|
availableIntegrations: [],
|
|
36
51
|
supportsMarkdown: false,
|
|
37
52
|
supportsStreaming: false,
|
|
@@ -47,3 +62,23 @@ function getChannelCapabilities(channel) {
|
|
|
47
62
|
});
|
|
48
63
|
return CHANNEL_CAPABILITIES[channel] ?? DEFAULT_CAPABILITIES;
|
|
49
64
|
}
|
|
65
|
+
/** Whether the channel is remote (open or closed) vs local/internal. */
|
|
66
|
+
function isRemoteChannel(capabilities) {
|
|
67
|
+
const senseType = capabilities?.senseType;
|
|
68
|
+
return senseType !== undefined && senseType !== "local" && senseType !== "internal";
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Returns channel names whose senseType is "open" or "closed" -- i.e. channels
|
|
72
|
+
* that are always-on (daemon-managed) rather than interactive or internal.
|
|
73
|
+
*/
|
|
74
|
+
function getAlwaysOnSenseNames() {
|
|
75
|
+
(0, runtime_1.emitNervesEvent)({
|
|
76
|
+
component: "channels",
|
|
77
|
+
event: "channel.always_on_lookup",
|
|
78
|
+
message: "always-on sense names lookup",
|
|
79
|
+
meta: {},
|
|
80
|
+
});
|
|
81
|
+
return Object.entries(CHANNEL_CAPABILITIES)
|
|
82
|
+
.filter(([, cap]) => cap.senseType === "open" || cap.senseType === "closed")
|
|
83
|
+
.map(([channel]) => channel);
|
|
84
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.upsertGroupContextParticipants = upsertGroupContextParticipants;
|
|
4
|
+
const node_crypto_1 = require("node:crypto");
|
|
5
|
+
const runtime_1 = require("../../nerves/runtime");
|
|
6
|
+
const CURRENT_SCHEMA_VERSION = 1;
|
|
7
|
+
function normalizeDisplayName(externalId, displayName) {
|
|
8
|
+
const trimmed = displayName?.trim();
|
|
9
|
+
return trimmed && trimmed.length > 0 ? trimmed : externalId;
|
|
10
|
+
}
|
|
11
|
+
function buildNameNotes(name, now) {
|
|
12
|
+
return name !== "Unknown"
|
|
13
|
+
? { name: { value: name, savedAt: now } }
|
|
14
|
+
: {};
|
|
15
|
+
}
|
|
16
|
+
function dedupeParticipants(participants) {
|
|
17
|
+
const deduped = new Map();
|
|
18
|
+
for (const participant of participants) {
|
|
19
|
+
const externalId = participant.externalId.trim();
|
|
20
|
+
if (!externalId)
|
|
21
|
+
continue;
|
|
22
|
+
const key = `${participant.provider}:${externalId}`;
|
|
23
|
+
if (!deduped.has(key)) {
|
|
24
|
+
deduped.set(key, {
|
|
25
|
+
...participant,
|
|
26
|
+
externalId,
|
|
27
|
+
displayName: participant.displayName?.trim() || undefined,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return Array.from(deduped.values());
|
|
32
|
+
}
|
|
33
|
+
function createGroupExternalId(provider, groupExternalId, linkedAt) {
|
|
34
|
+
return {
|
|
35
|
+
provider,
|
|
36
|
+
externalId: groupExternalId,
|
|
37
|
+
linkedAt,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function shouldPromoteToAcquaintance(friend) {
|
|
41
|
+
return (friend.trustLevel ?? "stranger") === "stranger";
|
|
42
|
+
}
|
|
43
|
+
function createAcquaintanceRecord(participant, groupExternalId, linkedAt) {
|
|
44
|
+
const name = normalizeDisplayName(participant.externalId, participant.displayName);
|
|
45
|
+
return {
|
|
46
|
+
id: (0, node_crypto_1.randomUUID)(),
|
|
47
|
+
name,
|
|
48
|
+
role: "acquaintance",
|
|
49
|
+
trustLevel: "acquaintance",
|
|
50
|
+
connections: [],
|
|
51
|
+
externalIds: [
|
|
52
|
+
{
|
|
53
|
+
provider: participant.provider,
|
|
54
|
+
externalId: participant.externalId,
|
|
55
|
+
linkedAt,
|
|
56
|
+
},
|
|
57
|
+
createGroupExternalId(participant.provider, groupExternalId, linkedAt),
|
|
58
|
+
],
|
|
59
|
+
tenantMemberships: [],
|
|
60
|
+
toolPreferences: {},
|
|
61
|
+
notes: buildNameNotes(name, linkedAt),
|
|
62
|
+
totalTokens: 0,
|
|
63
|
+
createdAt: linkedAt,
|
|
64
|
+
updatedAt: linkedAt,
|
|
65
|
+
schemaVersion: CURRENT_SCHEMA_VERSION,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
async function upsertGroupContextParticipants(input) {
|
|
69
|
+
(0, runtime_1.emitNervesEvent)({
|
|
70
|
+
component: "friends",
|
|
71
|
+
event: "friends.group_context_upsert_start",
|
|
72
|
+
message: "upserting shared-group participant context",
|
|
73
|
+
meta: {
|
|
74
|
+
participantCount: input.participants.length,
|
|
75
|
+
hasGroupExternalId: input.groupExternalId.trim().length > 0,
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
const groupExternalId = input.groupExternalId.trim();
|
|
79
|
+
if (!groupExternalId) {
|
|
80
|
+
return [];
|
|
81
|
+
}
|
|
82
|
+
const now = input.now ?? (() => new Date().toISOString());
|
|
83
|
+
const participants = dedupeParticipants(input.participants);
|
|
84
|
+
const results = [];
|
|
85
|
+
for (const participant of participants) {
|
|
86
|
+
const linkedAt = now();
|
|
87
|
+
const existing = await input.store.findByExternalId(participant.provider, participant.externalId);
|
|
88
|
+
if (!existing) {
|
|
89
|
+
const created = createAcquaintanceRecord(participant, groupExternalId, linkedAt);
|
|
90
|
+
await input.store.put(created.id, created);
|
|
91
|
+
results.push({
|
|
92
|
+
friendId: created.id,
|
|
93
|
+
name: created.name,
|
|
94
|
+
trustLevel: "acquaintance",
|
|
95
|
+
created: true,
|
|
96
|
+
updated: false,
|
|
97
|
+
addedGroupExternalId: true,
|
|
98
|
+
});
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const hasGroupExternalId = existing.externalIds.some((externalId) => externalId.externalId === groupExternalId);
|
|
102
|
+
const promoteToAcquaintance = shouldPromoteToAcquaintance(existing);
|
|
103
|
+
const trustLevel = promoteToAcquaintance
|
|
104
|
+
? "acquaintance"
|
|
105
|
+
: existing.trustLevel;
|
|
106
|
+
const role = promoteToAcquaintance
|
|
107
|
+
? "acquaintance"
|
|
108
|
+
: existing.role;
|
|
109
|
+
const updatedExternalIds = hasGroupExternalId
|
|
110
|
+
? existing.externalIds
|
|
111
|
+
: [...existing.externalIds, createGroupExternalId(participant.provider, groupExternalId, linkedAt)];
|
|
112
|
+
const updated = promoteToAcquaintance || !hasGroupExternalId;
|
|
113
|
+
const record = updated
|
|
114
|
+
? {
|
|
115
|
+
...existing,
|
|
116
|
+
role,
|
|
117
|
+
trustLevel,
|
|
118
|
+
externalIds: updatedExternalIds,
|
|
119
|
+
updatedAt: linkedAt,
|
|
120
|
+
}
|
|
121
|
+
: existing;
|
|
122
|
+
if (updated) {
|
|
123
|
+
await input.store.put(record.id, record);
|
|
124
|
+
}
|
|
125
|
+
results.push({
|
|
126
|
+
friendId: record.id,
|
|
127
|
+
name: record.name,
|
|
128
|
+
trustLevel,
|
|
129
|
+
created: false,
|
|
130
|
+
updated,
|
|
131
|
+
addedGroupExternalId: !hasGroupExternalId,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
(0, runtime_1.emitNervesEvent)({
|
|
135
|
+
component: "friends",
|
|
136
|
+
event: "friends.group_context_upsert_end",
|
|
137
|
+
message: "upserted shared-group participant context",
|
|
138
|
+
meta: {
|
|
139
|
+
participantCount: participants.length,
|
|
140
|
+
updatedCount: results.filter((result) => result.created || result.updated).length,
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
return results;
|
|
144
|
+
}
|
|
@@ -100,6 +100,25 @@ class FileFriendStore {
|
|
|
100
100
|
}
|
|
101
101
|
return entries.some((entry) => entry.endsWith(".json"));
|
|
102
102
|
}
|
|
103
|
+
async listAll() {
|
|
104
|
+
let entries;
|
|
105
|
+
try {
|
|
106
|
+
entries = await fsPromises.readdir(this.friendsPath);
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return [];
|
|
110
|
+
}
|
|
111
|
+
const records = [];
|
|
112
|
+
for (const entry of entries) {
|
|
113
|
+
if (!entry.endsWith(".json"))
|
|
114
|
+
continue;
|
|
115
|
+
const raw = await this.readJson(path.join(this.friendsPath, entry));
|
|
116
|
+
if (!raw)
|
|
117
|
+
continue;
|
|
118
|
+
records.push(this.normalize(raw));
|
|
119
|
+
}
|
|
120
|
+
return records;
|
|
121
|
+
}
|
|
103
122
|
normalize(raw) {
|
|
104
123
|
const trustLevel = raw.trustLevel;
|
|
105
124
|
const normalizedTrustLevel = trustLevel === "family" ||
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.describeTrustContext = describeTrustContext;
|
|
4
|
+
const runtime_1 = require("../../nerves/runtime");
|
|
5
|
+
function findRelatedGroupId(friend) {
|
|
6
|
+
return friend.externalIds.find((externalId) => externalId.externalId.startsWith("group:"))?.externalId;
|
|
7
|
+
}
|
|
8
|
+
function resolveLevel(friend) {
|
|
9
|
+
return friend.trustLevel ?? "stranger";
|
|
10
|
+
}
|
|
11
|
+
function describeTrustContext(input) {
|
|
12
|
+
const level = resolveLevel(input.friend);
|
|
13
|
+
const relatedGroupId = findRelatedGroupId(input.friend);
|
|
14
|
+
const explanation = level === "family" || level === "friend"
|
|
15
|
+
? {
|
|
16
|
+
level,
|
|
17
|
+
basis: "direct",
|
|
18
|
+
summary: level === "family"
|
|
19
|
+
? "direct family trust"
|
|
20
|
+
: "direct trusted relationship",
|
|
21
|
+
why: "this relationship is directly trusted rather than inferred through a shared group or cold first contact.",
|
|
22
|
+
permits: [
|
|
23
|
+
"local operations when appropriate",
|
|
24
|
+
"proactive follow-through",
|
|
25
|
+
"full collaborative problem solving",
|
|
26
|
+
],
|
|
27
|
+
constraints: [],
|
|
28
|
+
}
|
|
29
|
+
: level === "acquaintance"
|
|
30
|
+
? {
|
|
31
|
+
level,
|
|
32
|
+
basis: "shared_group",
|
|
33
|
+
summary: relatedGroupId
|
|
34
|
+
? "known through the shared project group"
|
|
35
|
+
: "known through a shared group context",
|
|
36
|
+
why: relatedGroupId
|
|
37
|
+
? `this trust comes from the shared group context ${relatedGroupId}, not from direct endorsement.`
|
|
38
|
+
: "this trust comes from shared group context rather than direct endorsement.",
|
|
39
|
+
permits: [
|
|
40
|
+
"group-safe coordination",
|
|
41
|
+
"normal conversation inside the shared context",
|
|
42
|
+
],
|
|
43
|
+
constraints: [
|
|
44
|
+
"guarded local actions",
|
|
45
|
+
"do not assume broad private authority",
|
|
46
|
+
],
|
|
47
|
+
relatedGroupId,
|
|
48
|
+
}
|
|
49
|
+
: {
|
|
50
|
+
level,
|
|
51
|
+
basis: "unknown",
|
|
52
|
+
summary: "truly unknown first-contact context",
|
|
53
|
+
why: "this person is not known through direct trust or a shared group context.",
|
|
54
|
+
permits: [
|
|
55
|
+
"safe first-contact orientation only",
|
|
56
|
+
],
|
|
57
|
+
constraints: [
|
|
58
|
+
"first contact does not reach the full model on open channels",
|
|
59
|
+
"no local or privileged actions",
|
|
60
|
+
],
|
|
61
|
+
};
|
|
62
|
+
(0, runtime_1.emitNervesEvent)({
|
|
63
|
+
component: "friends",
|
|
64
|
+
event: "friends.trust_explained",
|
|
65
|
+
message: "built explicit trust explanation",
|
|
66
|
+
meta: {
|
|
67
|
+
channel: input.channel,
|
|
68
|
+
level: explanation.level,
|
|
69
|
+
basis: explanation.basis,
|
|
70
|
+
hasRelatedGroup: Boolean(explanation.relatedGroupId),
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
return explanation;
|
|
74
|
+
}
|
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
// Context kernel type definitions.
|
|
3
3
|
// FriendRecord (merged identity + memory), channel capabilities, and resolved context.
|
|
4
4
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
|
+
exports.TRUSTED_LEVELS = void 0;
|
|
5
6
|
exports.isIdentityProvider = isIdentityProvider;
|
|
6
7
|
exports.isIntegration = isIntegration;
|
|
8
|
+
exports.isTrustedLevel = isTrustedLevel;
|
|
7
9
|
const runtime_1 = require("../../nerves/runtime");
|
|
8
10
|
const IDENTITY_PROVIDERS = new Set(["aad", "local", "teams-conversation", "imessage-handle"]);
|
|
9
11
|
function isIdentityProvider(value) {
|
|
@@ -19,3 +21,9 @@ const INTEGRATIONS = new Set(["ado", "github", "graph"]);
|
|
|
19
21
|
function isIntegration(value) {
|
|
20
22
|
return typeof value === "string" && INTEGRATIONS.has(value);
|
|
21
23
|
}
|
|
24
|
+
/** Trust levels that grant full tool access and proactive send capability. */
|
|
25
|
+
exports.TRUSTED_LEVELS = new Set(["family", "friend"]);
|
|
26
|
+
/** Whether a trust level grants full access (family or friend). Defaults to "friend" for legacy records. */
|
|
27
|
+
function isTrustedLevel(trustLevel) {
|
|
28
|
+
return exports.TRUSTED_LEVELS.has(trustLevel ?? "friend");
|
|
29
|
+
}
|