ocean-crew 0.0.0-stage → 0.4.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.
Files changed (92) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +156 -2
  3. package/THIRD_PARTY_NOTICES.md +159 -0
  4. package/client.js +31389 -0
  5. package/cordis.patch.yml +6 -0
  6. package/dist/host/chat/btw.js +436 -0
  7. package/dist/host/chat/channel.js +1241 -0
  8. package/dist/host/chat/chat-index.js +344 -0
  9. package/dist/host/chat/delivery.js +231 -0
  10. package/dist/host/chat/feed.js +165 -0
  11. package/dist/host/chat/handoffs.js +625 -0
  12. package/dist/host/chat/parts.js +2033 -0
  13. package/dist/host/chat/requests.js +155 -0
  14. package/dist/host/chat/resume.js +364 -0
  15. package/dist/host/chat/side.js +457 -0
  16. package/dist/host/chat/steer.js +346 -0
  17. package/dist/host/chat/turns.js +1457 -0
  18. package/dist/host/connectors/catalog.js +331 -0
  19. package/dist/host/connectors/gateway.js +339 -0
  20. package/dist/host/connectors/index.js +1235 -0
  21. package/dist/host/connectors/mail/index.js +912 -0
  22. package/dist/host/connectors/mail/presets.js +53 -0
  23. package/dist/host/connectors/mail/transport.js +519 -0
  24. package/dist/host/connectors/oauth.js +337 -0
  25. package/dist/host/core/constants.js +123 -0
  26. package/dist/host/core/diagnostics.js +310 -0
  27. package/dist/host/core/dsh-update.js +212 -0
  28. package/dist/host/core/events.js +16 -0
  29. package/dist/host/core/files.js +37 -0
  30. package/dist/host/core/folders.js +283 -0
  31. package/dist/host/core/home-lock.js +137 -0
  32. package/dist/host/core/links.js +52 -0
  33. package/dist/host/core/runtime.js +5 -0
  34. package/dist/host/core/store.js +662 -0
  35. package/dist/host/core/text.js +59 -0
  36. package/dist/host/core/thinking.js +60 -0
  37. package/dist/host/core/time-context.js +69 -0
  38. package/dist/host/core/update.js +466 -0
  39. package/dist/host/groups/calls.js +684 -0
  40. package/dist/host/groups/index.js +79 -0
  41. package/dist/host/groups/log.js +334 -0
  42. package/dist/host/groups/prompt.js +124 -0
  43. package/dist/host/groups/relay.js +489 -0
  44. package/dist/host/groups/round.js +992 -0
  45. package/dist/host/groups/tasks.js +217 -0
  46. package/dist/host/groups/tools.js +478 -0
  47. package/dist/host/groups/types.js +8 -0
  48. package/dist/host/im/format.js +228 -0
  49. package/dist/host/im/index.js +2461 -0
  50. package/dist/host/im/platforms/feishu.js +1134 -0
  51. package/dist/host/im/platforms/index.js +6 -0
  52. package/dist/host/im/platforms/media-type.js +70 -0
  53. package/dist/host/im/platforms/weixin.js +1117 -0
  54. package/dist/host/im/reaction.js +51 -0
  55. package/dist/host/im/supervisor.js +56 -0
  56. package/dist/host/team/agents.js +161 -0
  57. package/dist/host/team/models.js +427 -0
  58. package/dist/host/team/permissions.js +263 -0
  59. package/dist/host/team/privacy.js +108 -0
  60. package/dist/host/team/prompt.js +86 -0
  61. package/dist/host/team/providers.js +461 -0
  62. package/dist/host/team/roster.js +1011 -0
  63. package/dist/host/team/tools.js +1638 -0
  64. package/dist/host/work/approvals.js +290 -0
  65. package/dist/host/work/avatar-library.js +155 -0
  66. package/dist/host/work/avatar.js +132 -0
  67. package/dist/host/work/capabilities.js +862 -0
  68. package/dist/host/work/companion.js +306 -0
  69. package/dist/host/work/goal-skills.js +91 -0
  70. package/dist/host/work/goals.js +1337 -0
  71. package/dist/host/work/images.js +283 -0
  72. package/dist/host/work/learning.js +1021 -0
  73. package/dist/host/work/memory-pending.js +108 -0
  74. package/dist/host/work/memory.js +3237 -0
  75. package/dist/host/work/notify.js +146 -0
  76. package/dist/host/work/reset.js +43 -0
  77. package/dist/host/work/schedules.js +309 -0
  78. package/dist/host/work/secrets.js +702 -0
  79. package/dist/host/work/steward.js +166 -0
  80. package/dist/host/work/tts.js +192 -0
  81. package/dist/host/work/usage.js +84 -0
  82. package/dist/index.js +137 -0
  83. package/dist/shared/api.js +32 -0
  84. package/dist/shared/companion.js +110 -0
  85. package/dist/shared/config.js +3 -0
  86. package/dist/shared/coordination.js +6 -0
  87. package/dist/shared/group-replies.js +95 -0
  88. package/dist/shared/im-platforms.js +35 -0
  89. package/dist/shared/im.js +3 -0
  90. package/dist/shared/mentions.js +85 -0
  91. package/dist/shared/state.js +25 -0
  92. package/package.json +81 -5
@@ -0,0 +1,51 @@
1
+ // Status reactions on the message that asked for the work: a processing mark
2
+ // while the Bot runs, then the outcome. The lifecycle follows dsh-im's
3
+ // status-reaction module (MIT; dsh-im is listed in NOTICE): transitions are
4
+ // serialized per message and every provider failure is swallowed, so a
5
+ // reaction never holds up a reply.
6
+ export const STATUS_REACTIONS = Object.freeze({
7
+ processing: "OnIt",
8
+ success: "DONE",
9
+ error: "ERROR",
10
+ });
11
+ export function startStatusReaction({ add, remove, emojis = STATUS_REACTIONS, warn, } = {}) {
12
+ // A platform without both operations gets no reaction at all: the adapter says so
13
+ // by leaving one of them out.
14
+ if (typeof add !== "function" || typeof remove !== "function")
15
+ return undefined;
16
+ let current = null;
17
+ let closed = false;
18
+ let tail = Promise.resolve();
19
+ const step = (operation) => {
20
+ tail = tail.then(operation).catch((error) => {
21
+ warn?.(error);
22
+ });
23
+ return tail;
24
+ };
25
+ const transition = (emoji) => step(async () => {
26
+ if (current !== null) {
27
+ const previous = current;
28
+ current = null;
29
+ await remove(previous);
30
+ }
31
+ if (typeof emoji !== "string" || emoji === "")
32
+ return;
33
+ const id = await add(emoji);
34
+ if (typeof id === "string" && id !== "")
35
+ current = id;
36
+ });
37
+ transition(emojis.processing);
38
+ const finish = (emoji) => {
39
+ if (!closed) {
40
+ closed = true;
41
+ transition(emoji);
42
+ }
43
+ return tail;
44
+ };
45
+ return Object.freeze({
46
+ success: () => finish(emojis.success),
47
+ error: () => finish(emojis.error),
48
+ clear: () => finish(""),
49
+ settled: () => tail,
50
+ });
51
+ }
@@ -0,0 +1,56 @@
1
+ // One pure decision per connection per health check. The IM host layer owns the
2
+ // runners, queues and timers; this module only says whether to start, stop,
3
+ // restart or leave a connection alone, and when the next attempt is due.
4
+ // The backoff ladder and the three-way failure classification follow dsh-im's
5
+ // connection supervisor (MIT; dsh-im is listed in NOTICE); its controller layer
6
+ // is not needed because im/index.ts already keeps the runner slots and a single
7
+ // serialized sync chain.
8
+ export const RETRY_DELAYS_MS = Object.freeze([250, 1_000, 3_000, 5_000, 10_000, 30_000]);
9
+ // Pause before attempt number `attempts` (1-based); the last delay repeats.
10
+ export function retryDelay(attempts) {
11
+ const tries = Number.isFinite(attempts) && attempts > 0 ? Math.floor(attempts) : 1;
12
+ return RETRY_DELAYS_MS[Math.min(tries - 1, RETRY_DELAYS_MS.length - 1)];
13
+ }
14
+ /**
15
+ * Decide what one health check does with one connection.
16
+ * - desired: the connection should be connected (enabled, has secrets, writable host)
17
+ * - running: a runner object currently holds its slot
18
+ * - state: last reported state, 'off' | 'connecting' | 'ready' | 'error'
19
+ * - messageError: the error came from one refused message, not from the link
20
+ * - attempts: attempts since the last success
21
+ * - nextRetryAt: earliest time for the next attempt, ms epoch (0 = now)
22
+ */
23
+ export function nextAction(now, entry = {}) {
24
+ const desired = entry.desired === true;
25
+ const running = entry.running === true;
26
+ const state = typeof entry.state === "string" ? entry.state : "off";
27
+ const attempts = Number.isFinite(entry.attempts) && entry.attempts > 0 ? Math.floor(entry.attempts) : 0;
28
+ const due = Number.isFinite(entry.nextRetryAt) && entry.nextRetryAt > 0 ? entry.nextRetryAt : 0;
29
+ if (!desired)
30
+ return { action: running ? "stop" : "none", attempts: 0, nextRetryAt: 0 };
31
+ if (state === "ready" && running)
32
+ return { action: "none", attempts: 0, nextRetryAt: 0 };
33
+ if (state === "error") {
34
+ // A refused message is not a broken link: reconnect only after the link
35
+ // itself failed, otherwise one bad chat would restart the connection forever.
36
+ if (running && entry.messageError === true)
37
+ return { action: "none", attempts, nextRetryAt: 0 };
38
+ if (now < due)
39
+ return { action: "none", attempts, nextRetryAt: due };
40
+ const tries = attempts + 1;
41
+ // Without a runner a failed connect left nothing to restart: a start is the retry.
42
+ return { action: running ? "restart" : "start", attempts: tries, nextRetryAt: now + retryDelay(tries) };
43
+ }
44
+ if (!running)
45
+ return { action: "start", attempts, nextRetryAt: 0 };
46
+ return { action: "none", attempts, nextRetryAt: due };
47
+ }
48
+ export function failClass(error) {
49
+ const failure = error;
50
+ if (failure?.permanent === true ||
51
+ (typeof failure?.status === "number" && failure.status >= 400 && failure.status < 500 && failure.status !== 429))
52
+ return "permanent";
53
+ if (failure?.unknown === true || failure?.stopped === true)
54
+ return "unknown";
55
+ return "retry";
56
+ }
@@ -0,0 +1,161 @@
1
+ // The DSH Agent: a team's one plain dsh entry, listed beside the Bots but not one of
2
+ // them. Its Sessions are ordinary root Sessions in the Ocean Crew workspace, so they get
3
+ // the whole dsh conversation surface and none of the team tools.
4
+ import { randomUUID } from "node:crypto";
5
+ import { AGENT_NAME, AGENT_SESSION_TITLE_MAX } from "../core/constants.js";
6
+ export function install(rt) {
7
+ const { ctx } = rt;
8
+ const state = rt.state;
9
+ const table = state;
10
+ const agents = () => {
11
+ table.agents ??= {};
12
+ return table.agents;
13
+ };
14
+ const registry = () => {
15
+ const service = ctx.workspaceRegistry;
16
+ if (service === undefined)
17
+ throw new Error("This DSH has no workspace registry, so it has no DSH Agent");
18
+ return service;
19
+ };
20
+ const controller = () => ctx.sessionController;
21
+ const titles = () => ctx.sessionTitle;
22
+ /** Session id -> DSH Agent record id. */
23
+ const sessionIndex = new Map();
24
+ // store.js calls this from indexSessions after every adopt and save.
25
+ rt.indexAgents = () => {
26
+ sessionIndex.clear();
27
+ for (const agent of Object.values(agents())) {
28
+ for (const sessionId of agent.sessions ?? [])
29
+ sessionIndex.set(sessionId, agent.id);
30
+ }
31
+ };
32
+ // The DSH Agent a Session belongs to; Bot and group Sessions get undefined.
33
+ const agentOf = (sessionId) => agents()[sessionIndex.get(sessionId) ?? ""];
34
+ const writeCheck = () => {
35
+ const readOnly = rt.readOnly();
36
+ if (readOnly !== undefined)
37
+ throw new Error(readOnly);
38
+ };
39
+ const record = (agentId) => {
40
+ const agent = rt.own(agents(), agentId);
41
+ if (agent === undefined)
42
+ throw new Error("Unknown DSH Agent");
43
+ return agent;
44
+ };
45
+ const sessionsOf = (agent) => {
46
+ if (agent.sessions === undefined)
47
+ agent.sessions = [];
48
+ return agent.sessions;
49
+ };
50
+ async function createAgentSession(agentId, workspaceId) {
51
+ writeCheck();
52
+ const agent = record(agentId);
53
+ // A known workspace id wins (the Agent's latest Session may live outside the
54
+ // team workspace after a workspace switch); anything else gets the team's own.
55
+ const target = typeof workspaceId === "string" && registry().get?.(workspaceId)
56
+ ? workspaceId
57
+ : await rt.ensureWorkspace();
58
+ const { sessionId } = await controller().create({ workspaceId: target });
59
+ sessionsOf(agent).push(sessionId);
60
+ await rt.save();
61
+ return sessionId;
62
+ }
63
+ // A root Session the user opened from a DSH Agent page (the hero's workspace picker
64
+ // or a fork) is adopted into the same Agent: it keeps dsh's surface and the team
65
+ // tools stay hidden.
66
+ async function adoptAgentSession(agentId, sessionId) {
67
+ writeCheck();
68
+ const agent = record(agentId);
69
+ if (sessionsOf(agent).includes(sessionId))
70
+ return sessionId;
71
+ await rt.resolveAgent(sessionId);
72
+ const owned = rt.selfOf(sessionId) !== undefined ||
73
+ rt.botOf(sessionId) !== undefined ||
74
+ rt.roomOf(sessionId) !== undefined ||
75
+ agentOf(sessionId) !== undefined;
76
+ if (owned)
77
+ throw new Error("Session already belongs to the team");
78
+ sessionsOf(agent).push(sessionId);
79
+ await rt.save();
80
+ return sessionId;
81
+ }
82
+ async function addAgent() {
83
+ writeCheck();
84
+ if (Object.keys(agents()).length > 0)
85
+ throw new Error("The team already has a DSH Agent");
86
+ const agent = { id: randomUUID(), name: AGENT_NAME, sessions: [], createdAt: Date.now() };
87
+ agents()[agent.id] = agent;
88
+ try {
89
+ await createAgentSession(agent.id);
90
+ }
91
+ catch (error) {
92
+ // The first Session may already be created and saved: archive it and persist
93
+ // the rollback, or the Agent returns on restart with its team tools never denied.
94
+ const createdSessions = [...sessionsOf(agent)];
95
+ delete agents()[agent.id];
96
+ for (const sessionId of createdSessions)
97
+ await registry()
98
+ .archiveSession(sessionId)
99
+ .catch(() => { });
100
+ await rt.save().catch(() => { });
101
+ throw error;
102
+ }
103
+ return agent;
104
+ }
105
+ async function archiveAgentSession(agentId, sessionId) {
106
+ writeCheck();
107
+ const agent = record(agentId);
108
+ if (!sessionsOf(agent).includes(sessionId))
109
+ throw new Error("Session does not belong to the DSH Agent");
110
+ // Archive first: the Session leaves the Agent only once it is actually gone,
111
+ // so a failed archive stays retryable and keeps its tool restriction.
112
+ await registry().archiveSession(sessionId);
113
+ agent.sessions = sessionsOf(agent).filter((id) => id !== sessionId);
114
+ await rt.save();
115
+ }
116
+ async function renameAgentSession(agentId, sessionId, title) {
117
+ writeCheck();
118
+ const agent = record(agentId);
119
+ if (!sessionsOf(agent).includes(sessionId))
120
+ throw new Error("Session does not belong to the DSH Agent");
121
+ const clean = String(title ?? "").trim();
122
+ if (clean === "")
123
+ throw new Error("A session needs a title");
124
+ const resolved = (await rt.resolveAgent(sessionId));
125
+ await titles().rename(resolved.session, clean.slice(0, AGENT_SESSION_TITLE_MAX));
126
+ }
127
+ async function removeAgent(agentId) {
128
+ writeCheck();
129
+ const agent = record(agentId);
130
+ const sessions = [...sessionsOf(agent)];
131
+ const kept = [];
132
+ let failure;
133
+ for (const sessionId of sessions) {
134
+ try {
135
+ await registry().archiveSession(sessionId);
136
+ }
137
+ catch (error) {
138
+ failure ??= error;
139
+ kept.push(sessionId);
140
+ }
141
+ }
142
+ // What could not be archived stays with the Agent, so it keeps its tool
143
+ // restriction and the removal can be retried from the same place.
144
+ if (kept.length > 0) {
145
+ agent.sessions = kept;
146
+ await rt.save();
147
+ throw failure;
148
+ }
149
+ delete agents()[agentId];
150
+ await rt.save();
151
+ }
152
+ Object.assign(rt, {
153
+ agentOf,
154
+ addAgent,
155
+ createAgentSession,
156
+ adoptAgentSession,
157
+ archiveAgentSession,
158
+ renameAgentSession,
159
+ removeAgent,
160
+ });
161
+ }