@xfey/tutti 0.1.102 → 0.1.104
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 +9 -4
- package/dist/chat-assistant/index.d.ts +6 -1
- package/dist/chat-assistant/index.js +22 -12
- package/dist/collaboration-ingestion/external-source.d.ts +15 -0
- package/dist/collaboration-ingestion/external-source.js +120 -0
- package/dist/collaboration-ingestion/index.d.ts +5 -0
- package/dist/collaboration-ingestion/index.js +4 -0
- package/dist/collaboration-ingestion/invalidations.d.ts +4 -0
- package/dist/collaboration-ingestion/invalidations.js +15 -0
- package/dist/collaboration-ingestion/main-chat.d.ts +33 -0
- package/dist/collaboration-ingestion/main-chat.js +67 -0
- package/dist/collaboration-ingestion/reference-files.d.ts +29 -0
- package/dist/collaboration-ingestion/reference-files.js +158 -0
- package/dist/collaboration-ingestion/types.d.ts +19 -0
- package/dist/collaboration-ingestion/types.js +2 -0
- package/dist/platform-outbound/coordinator.d.ts +17 -0
- package/dist/platform-outbound/coordinator.js +48 -0
- package/dist/platform-outbound/index.d.ts +5 -0
- package/dist/platform-outbound/index.js +5 -0
- package/dist/platform-outbound/repository.d.ts +30 -0
- package/dist/platform-outbound/repository.js +108 -0
- package/dist/platform-outbound/types.d.ts +38 -0
- package/dist/platform-outbound/types.js +2 -0
- package/dist/platform-outbound/worker.d.ts +36 -0
- package/dist/platform-outbound/worker.js +164 -0
- package/dist/server-shell/cli/cli.js +9 -0
- package/dist/server-shell/cli/errors.d.ts +1 -1
- package/dist/server-shell/cli/host-lifecycle.d.ts +2 -0
- package/dist/server-shell/cli/host-lifecycle.js +34 -1
- package/dist/server-shell/cli/host-relay-connection-manager.js +22 -11
- package/dist/server-shell/cli/host-relay-status.js +1 -0
- package/dist/server-shell/cli/host-runtime-endpoint.js +17 -1
- package/dist/server-shell/cli/host-server-runtime.d.ts +2 -0
- package/dist/server-shell/cli/host-server-runtime.js +88 -14
- package/dist/server-shell/cli/launch-command.d.ts +7 -0
- package/dist/server-shell/cli/launch-command.js +39 -1
- package/dist/server-shell/cli/launch.d.ts +2 -0
- package/dist/server-shell/cli/launch.js +3 -0
- package/dist/server-shell/cli/machine-local.d.ts +20 -0
- package/dist/server-shell/cli/machine-local.js +61 -1
- package/dist/server-shell/cli/managed-host.d.ts +16 -1
- package/dist/server-shell/cli/managed-host.js +191 -23
- package/dist/server-shell/cli/relay-registration.d.ts +4 -1
- package/dist/server-shell/cli/relay-registration.js +36 -7
- package/dist/server-shell/cli/runtime-commands.d.ts +5 -1
- package/dist/server-shell/cli/runtime-commands.js +9 -3
- package/dist/server-shell/http/host-tunnel.d.ts +5 -0
- package/dist/server-shell/http/host-tunnel.js +109 -0
- package/dist/server-shell/http/routes/local-control.d.ts +2 -0
- package/dist/server-shell/http/routes/local-control.js +2 -0
- package/dist/server-shell/http/routes/project-api/invalidations.d.ts +0 -1
- package/dist/server-shell/http/routes/project-api/invalidations.js +0 -3
- package/dist/server-shell/http/routes/project-api/messages-routes.js +32 -63
- package/dist/server-shell/http/routes/project-api/openapi-workspace-routes.d.ts +8 -4
- package/dist/server-shell/http/routes/project-api/openapi.d.ts +8 -4
- package/dist/server-shell/http/routes/project-api/reference-files-routes.js +25 -94
- package/dist/server-shell/http/routes/project-api/reference-files.d.ts +1 -11
- package/dist/server-shell/http/routes/project-api/reference-files.js +2 -87
- package/dist/server-shell/http/routes/project-api/schemas.d.ts +8 -4
- package/dist/server-shell/http/routes/project-api/schemas.js +8 -3
- package/dist/server-shell/http/routes/project-api/session-requirements.js +34 -2
- package/dist/server-shell/http/routes/project-api/types.d.ts +3 -1
- package/dist/server-shell/http/routes/project-api/workspace-projections.js +8 -2
- package/dist/server-shell/local-console/package-update-hosts.js +4 -1
- package/dist/server-shell/local-console/project-service.d.ts +11 -2
- package/dist/server-shell/local-console/project-service.js +36 -8
- package/dist/server-shell/local-console/server.js +6 -1
- package/dist/server-shell/local-console/session.d.ts +4 -0
- package/dist/server-shell/local-console/session.js +14 -0
- package/dist/server-shell/session/relay-session-context.d.ts +12 -1
- package/dist/server-shell/session/relay-session-context.js +19 -1
- package/migrations/0016_external_source_mappings.sql +141 -0
- package/migrations/0017_platform_outbound_intents.sql +31 -0
- package/migrations/README.md +3 -1
- package/node_modules/@tutti/relay-client/dist/host-control.d.ts +26 -1
- package/node_modules/@tutti/relay-client/dist/host-control.js +62 -0
- package/node_modules/@tutti/relay-client/dist/tunnel-frames.js +32 -6
- package/node_modules/@tutti/relay-client/dist/tunnel-types.d.ts +5 -1
- package/node_modules/@tutti/shared/dist/domain/index.d.ts +9 -0
- package/node_modules/@tutti/shared/dist/domain/index.js +14 -0
- package/node_modules/@tutti/shared/dist/ids/index.d.ts +6 -0
- package/node_modules/@tutti/shared/dist/ids/index.js +4 -0
- package/node_modules/@tutti/shared/dist/schemas/api/index.d.ts +4 -0
- package/node_modules/@tutti/shared/dist/schemas/api/index.js +2 -0
- package/node_modules/@tutti/shared/dist/schemas/api/local-console-account-handoffs.d.ts +13 -0
- package/node_modules/@tutti/shared/dist/schemas/api/local-console-account-handoffs.js +16 -0
- package/node_modules/@tutti/shared/dist/schemas/api/platform-sessions.d.ts +43 -0
- package/node_modules/@tutti/shared/dist/schemas/api/platform-sessions.js +44 -0
- package/node_modules/@tutti/shared/dist/schemas/domain/index.d.ts +7 -0
- package/node_modules/@tutti/shared/dist/schemas/domain/index.js +10 -0
- package/node_modules/@tutti/shared/dist/schemas/internal/index.d.ts +29 -0
- package/node_modules/@tutti/shared/dist/schemas/internal/index.js +24 -1
- package/node_modules/@tutti/shared/dist/schemas/internal/platform-integration.d.ts +1346 -0
- package/node_modules/@tutti/shared/dist/schemas/internal/platform-integration.js +516 -0
- package/node_modules/@tutti/shared/dist/utils/feishu-diagnostics/index.d.ts +10 -0
- package/node_modules/@tutti/shared/dist/utils/feishu-diagnostics/index.js +19 -0
- package/node_modules/@tutti/shared/dist/utils/index.d.ts +1 -0
- package/node_modules/@tutti/shared/dist/utils/index.js +1 -0
- package/node_modules/@tutti/shared/dist/utils/redaction/index.d.ts +1 -1
- package/node_modules/@tutti/shared/dist/utils/redaction/index.js +18 -0
- package/package.json +1 -1
- package/web/assets/{homepage-motion-scene-BtT5bd30.js → homepage-motion-scene-DEVrQ9aj.js} +1 -1
- package/web/assets/{index-CxCTqGFO.css → index-6i9Uk7fS.css} +1 -1
- package/web/assets/index-Lxtst2sl.js +69 -0
- package/web/index.html +2 -2
- package/web/assets/index-DtiStc-n.js +0 -69
|
@@ -137,6 +137,21 @@ function isLaunchStatusPayload(value) {
|
|
|
137
137
|
}
|
|
138
138
|
const relay = record.relay;
|
|
139
139
|
const visibility = relay.join_url_visibility;
|
|
140
|
+
const feishuSetup = relay.feishu_setup;
|
|
141
|
+
const validFeishuSetup = feishuSetup === undefined ||
|
|
142
|
+
(typeof feishuSetup === "object" &&
|
|
143
|
+
feishuSetup !== null &&
|
|
144
|
+
!Array.isArray(feishuSetup) &&
|
|
145
|
+
(feishuSetup.state === "account_handoff_required" ||
|
|
146
|
+
feishuSetup.state === "connected" ||
|
|
147
|
+
(feishuSetup.state === "pairing_required" &&
|
|
148
|
+
((feishuSetup.visibility === "visible_once" &&
|
|
149
|
+
typeof feishuSetup.pairing_code === "string" &&
|
|
150
|
+
typeof feishuSetup.command === "string" &&
|
|
151
|
+
typeof feishuSetup.expires_at === "string") ||
|
|
152
|
+
(feishuSetup.visibility === "not_recoverable" &&
|
|
153
|
+
(feishuSetup.expires_at === undefined ||
|
|
154
|
+
typeof feishuSetup.expires_at === "string"))))));
|
|
140
155
|
return (typeof relay.relay_project_ref === "string" &&
|
|
141
156
|
typeof relay.host_connection_ref === "string" &&
|
|
142
157
|
(relay.join_url === undefined || typeof relay.join_url === "string") &&
|
|
@@ -145,7 +160,8 @@ function isLaunchStatusPayload(value) {
|
|
|
145
160
|
typeof relay.join_token_expires_at === "string") &&
|
|
146
161
|
(relay.join_token_reusable === undefined || typeof relay.join_token_reusable === "boolean") &&
|
|
147
162
|
typeof relay.tunnel_url === "string" &&
|
|
148
|
-
typeof relay.connected_at === "string"
|
|
163
|
+
typeof relay.connected_at === "string" &&
|
|
164
|
+
validFeishuSetup);
|
|
149
165
|
}
|
|
150
166
|
export async function readExistingHostLaunchStatus(options) {
|
|
151
167
|
try {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { FastifyInstance } from "fastify";
|
|
2
2
|
import { type ExecutionStatusProjection } from "@tutti/shared/schemas/api";
|
|
3
|
+
import type { IntegrationDeliveryDisposition, TrustedConversationSourceIngestionCommand } from "@tutti/shared/schemas/internal";
|
|
3
4
|
import { type HostProjectStore } from "../../store/index.js";
|
|
4
5
|
import type { createHostServer } from "../http/create-server.js";
|
|
5
6
|
import type { HostLocalLaunchStatus } from "../http/routes/local-control.js";
|
|
@@ -17,6 +18,7 @@ export type HostServerHandle = {
|
|
|
17
18
|
log_file_path: string;
|
|
18
19
|
runtime_endpoint: MachineRuntimeEndpointRecord;
|
|
19
20
|
trusted_relay_metadata_store: TrustedRelaySessionMetadataStore;
|
|
21
|
+
integration_delivery: (command: TrustedConversationSourceIngestionCommand) => IntegrationDeliveryDisposition;
|
|
20
22
|
attachRelayConnectionManager: (manager: HostRelayConnectionManager) => void;
|
|
21
23
|
closed: Promise<void>;
|
|
22
24
|
close: () => Promise<void>;
|
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
import { basename, join } from "node:path";
|
|
2
2
|
import { MAX_PROVIDER_MODEL_NAME_LENGTH, } from "@tutti/shared/schemas/api";
|
|
3
3
|
import { serializeRuntimeLogLine } from "@tutti/shared/utils";
|
|
4
|
-
import { completeRelayUpload, resolveRelayUpload } from "@tutti/relay-client";
|
|
4
|
+
import { completeRelayUpload, deliverRelayPlatformOutbound, RelayHostControlClientError, resolveRelayUpload, } from "@tutti/relay-client";
|
|
5
5
|
import { ApprovalRequestManager } from "../../approvals/index.js";
|
|
6
6
|
import { ArtifactPreviewManager } from "../../artifacts/index.js";
|
|
7
7
|
import { ReadOnlyChatAssistant } from "../../chat-assistant/index.js";
|
|
8
8
|
import { CheckProcessCoordinator } from "../../checks/index.js";
|
|
9
|
+
import { ingestTrustedConversationSource } from "../../collaboration-ingestion/index.js";
|
|
9
10
|
import { ensureInitialProjectWelcomeMessage } from "../../collaboration-state/index.js";
|
|
10
11
|
import { Phase5ControlPlane } from "../../control-plane/index.js";
|
|
11
12
|
import { resolveProjectOpenAiProcedureWorkflowRunner } from "../../control-plane/workflows/index.js";
|
|
12
13
|
import { cleanupStaleCodexAppServerTempRoots } from "../../providers/openai/app-server/stale-temp-cleanup.js";
|
|
13
14
|
import { CodexAppServerInvocationCoordinator } from "../../providers/openai/app-server/invocation-coordinator.js";
|
|
14
15
|
import { resolveProjectOpenAiReadOnlyChatAssistantModel } from "../../providers/openai/chat-assistant.js";
|
|
16
|
+
import { HostPlatformOutboundCoordinator, HostPlatformOutboundWorker, } from "../../platform-outbound/index.js";
|
|
15
17
|
import { configureProjectOpenAiProvider, discoverProjectOpenAiModels, readOpenAiProviderConfigProjection, updateProjectOpenAiDefaultModel, } from "../../providers/openai/index.js";
|
|
16
18
|
import { readProviderUsageProjection, recordProviderUsageEvent, } from "../../provider-usage/index.js";
|
|
17
19
|
import { resolveProjectOpenAiRunPipelineRunner } from "../../run-pipeline/openai.js";
|
|
@@ -94,18 +96,21 @@ function createHostProviderModelService(options) {
|
|
|
94
96
|
};
|
|
95
97
|
}
|
|
96
98
|
function createHostRuntimeLogger(options) {
|
|
99
|
+
const write = (level, fields, message) => {
|
|
100
|
+
const line = serializeRuntimeLogLine({
|
|
101
|
+
level,
|
|
102
|
+
service: "tutti-host",
|
|
103
|
+
component: options.component,
|
|
104
|
+
message,
|
|
105
|
+
fields,
|
|
106
|
+
now: options.now,
|
|
107
|
+
});
|
|
108
|
+
appendHostLogLine(options.logFilePath, line);
|
|
109
|
+
};
|
|
97
110
|
return {
|
|
98
|
-
info(fields, message)
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
service: "tutti-host",
|
|
102
|
-
component: options.component,
|
|
103
|
-
message,
|
|
104
|
-
fields,
|
|
105
|
-
now: options.now,
|
|
106
|
-
});
|
|
107
|
-
appendHostLogLine(options.logFilePath, line);
|
|
108
|
-
},
|
|
111
|
+
info: (fields, message) => write("info", fields, message),
|
|
112
|
+
warn: (fields, message) => write("warn", fields, message),
|
|
113
|
+
error: (fields, message) => write("error", fields, message),
|
|
109
114
|
};
|
|
110
115
|
}
|
|
111
116
|
function createLifecycleIncidentReporter(logger, consumer) {
|
|
@@ -284,6 +289,12 @@ export async function startForegroundHostServer(options) {
|
|
|
284
289
|
logFilePath,
|
|
285
290
|
now,
|
|
286
291
|
});
|
|
292
|
+
const platformOutboundLogger = createHostRuntimeLogger({
|
|
293
|
+
component: "platform-outbound",
|
|
294
|
+
logFilePath,
|
|
295
|
+
now,
|
|
296
|
+
});
|
|
297
|
+
let relayConnectionManager;
|
|
287
298
|
const procedureLifecycleIncident = createLifecycleIncidentReporter(controlPlaneLogger, "procedure");
|
|
288
299
|
const runLifecycleIncident = createLifecycleIncidentReporter(runPipelineLogger, "run");
|
|
289
300
|
const chatLifecycleIncident = createLifecycleIncidentReporter(controlPlaneLogger, "chat_assistant");
|
|
@@ -353,6 +364,43 @@ export async function startForegroundHostServer(options) {
|
|
|
353
364
|
onLifecycleIncident: procedureLifecycleIncident,
|
|
354
365
|
}),
|
|
355
366
|
});
|
|
367
|
+
const platformOutboundWorker = options.preparation.machine_local.binding.surface_profile.kind === "feishu"
|
|
368
|
+
? new HostPlatformOutboundWorker(store, {
|
|
369
|
+
deliver: async (item) => {
|
|
370
|
+
const connectionState = relayConnectionManager?.getState();
|
|
371
|
+
if (connectionState?.status !== "connected") {
|
|
372
|
+
throw new RelayHostControlClientError("relay_unavailable", "Relay is not connected for platform outbound handoff", true);
|
|
373
|
+
}
|
|
374
|
+
const hostSecret = readHostRegistrationSecret(options.preparation.tutti_home, options.preparation.project_id);
|
|
375
|
+
const relayProjectRef = connectionState.connection.relay_project_ref;
|
|
376
|
+
return await deliverRelayPlatformOutbound({
|
|
377
|
+
relayUrl: options.preparation.relay_url,
|
|
378
|
+
projectId: options.preparation.project_id,
|
|
379
|
+
relayProjectRef,
|
|
380
|
+
hostConnectionRef: connectionState.connection.host_connection_ref,
|
|
381
|
+
hostRegistrationSecret: hostSecret.secret,
|
|
382
|
+
intent: {
|
|
383
|
+
protocol_version: 1,
|
|
384
|
+
intent_ref: item.intent.intent_ref,
|
|
385
|
+
relay_project_ref: relayProjectRef,
|
|
386
|
+
installation: {
|
|
387
|
+
platform: item.intent.platform,
|
|
388
|
+
installation_ref: item.intent.installation_ref,
|
|
389
|
+
},
|
|
390
|
+
binding_ref: item.intent.binding_ref,
|
|
391
|
+
canonical_message_ref: item.intent.canonical_message_id,
|
|
392
|
+
trigger_message_ref: item.intent.trigger_message_id,
|
|
393
|
+
content: { kind: "text", text: item.body },
|
|
394
|
+
created_at: item.intent.created_at,
|
|
395
|
+
attempt: item.intent.attempt_count,
|
|
396
|
+
},
|
|
397
|
+
});
|
|
398
|
+
},
|
|
399
|
+
}, platformOutboundLogger, { now })
|
|
400
|
+
: undefined;
|
|
401
|
+
const platformOutboundCoordinator = platformOutboundWorker === undefined
|
|
402
|
+
? undefined
|
|
403
|
+
: new HostPlatformOutboundCoordinator(platformOutboundWorker, platformOutboundLogger, now);
|
|
356
404
|
const chatAssistant = new ReadOnlyChatAssistant({
|
|
357
405
|
project: chatAssistantProject,
|
|
358
406
|
store,
|
|
@@ -377,6 +425,14 @@ export async function startForegroundHostServer(options) {
|
|
|
377
425
|
},
|
|
378
426
|
waitForInspectionAvailability: () => controlPlane.waitForRepoInspectionIdle(),
|
|
379
427
|
onScratchpadSourceChanged: (message) => controlPlane.notifyScratchpadSourceChanged(message),
|
|
428
|
+
...(platformOutboundCoordinator === undefined
|
|
429
|
+
? {}
|
|
430
|
+
: {
|
|
431
|
+
onMainChatAgentMessageCreated: ({ tx, triggerMessage, responseMessage }) => platformOutboundCoordinator.captureAgentResponse(tx, {
|
|
432
|
+
triggerMessage,
|
|
433
|
+
responseMessage,
|
|
434
|
+
}),
|
|
435
|
+
}),
|
|
380
436
|
now,
|
|
381
437
|
});
|
|
382
438
|
const projectRuntimeLifecycle = new HostProjectRuntimeLifecycle({
|
|
@@ -387,7 +443,6 @@ export async function startForegroundHostServer(options) {
|
|
|
387
443
|
approvalManager,
|
|
388
444
|
});
|
|
389
445
|
let close = async () => { };
|
|
390
|
-
let relayConnectionManager;
|
|
391
446
|
const packageUpdateReadiness = () => packageUpdateReadinessFromExecutionStatus(controlPlane.getExecutionStatus());
|
|
392
447
|
const localControl = {
|
|
393
448
|
token,
|
|
@@ -474,11 +529,19 @@ export async function startForegroundHostServer(options) {
|
|
|
474
529
|
},
|
|
475
530
|
logger: { file: logFilePath },
|
|
476
531
|
lifecycle: {
|
|
477
|
-
close: () =>
|
|
532
|
+
close: async () => {
|
|
533
|
+
try {
|
|
534
|
+
await projectRuntimeLifecycle.close();
|
|
535
|
+
}
|
|
536
|
+
finally {
|
|
537
|
+
await platformOutboundCoordinator?.stop();
|
|
538
|
+
}
|
|
539
|
+
},
|
|
478
540
|
},
|
|
479
541
|
localControl,
|
|
480
542
|
now,
|
|
481
543
|
projectApi: {
|
|
544
|
+
surfaceProfile: options.preparation.machine_local.binding.surface_profile,
|
|
482
545
|
project: projectApiProject,
|
|
483
546
|
server: {
|
|
484
547
|
version: HOST_SERVER_VERSION,
|
|
@@ -615,8 +678,19 @@ export async function startForegroundHostServer(options) {
|
|
|
615
678
|
log_file_path: logFilePath,
|
|
616
679
|
runtime_endpoint: runtimeEndpoint,
|
|
617
680
|
trusted_relay_metadata_store: trustedMetadataStore,
|
|
681
|
+
integration_delivery: (command) => ingestTrustedConversationSource({
|
|
682
|
+
store,
|
|
683
|
+
events: workspaceEvents,
|
|
684
|
+
command,
|
|
685
|
+
scratchpadSource: {
|
|
686
|
+
notify: (message) => controlPlane.notifyScratchpadSourceChanged(message),
|
|
687
|
+
},
|
|
688
|
+
chatAssistant,
|
|
689
|
+
now,
|
|
690
|
+
}),
|
|
618
691
|
attachRelayConnectionManager: (manager) => {
|
|
619
692
|
relayConnectionManager = manager;
|
|
693
|
+
platformOutboundCoordinator?.start();
|
|
620
694
|
},
|
|
621
695
|
closed,
|
|
622
696
|
close,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { LaunchConfirmationRequest } from "./git-bootstrap.js";
|
|
2
|
+
import type { FeishuSetupProjection } from "@tutti/shared/schemas/internal";
|
|
2
3
|
export declare function confirmFromTty(request: LaunchConfirmationRequest): Promise<boolean>;
|
|
3
4
|
export declare function confirmTextFromTty(message: string): Promise<boolean>;
|
|
4
5
|
export declare function renderCompletionPage(options: {
|
|
@@ -10,6 +11,12 @@ export declare function renderCompletionPage(options: {
|
|
|
10
11
|
tty?: boolean;
|
|
11
12
|
hyperlinks?: boolean;
|
|
12
13
|
}): string;
|
|
14
|
+
export declare function renderFeishuCompletionPage(options: {
|
|
15
|
+
feishuSetup?: FeishuSetupProjection;
|
|
16
|
+
projectId: string;
|
|
17
|
+
workspaceRoot: string;
|
|
18
|
+
tty?: boolean;
|
|
19
|
+
}): string;
|
|
13
20
|
export declare function runForegroundLaunchCommand(options: {
|
|
14
21
|
target?: string;
|
|
15
22
|
yes: boolean;
|
|
@@ -88,6 +88,32 @@ export function renderCompletionPage(options) {
|
|
|
88
88
|
isTty ? borderedBlock(summaryLines) : summaryLines.join("\n"),
|
|
89
89
|
].join("\n");
|
|
90
90
|
}
|
|
91
|
+
export function renderFeishuCompletionPage(options) {
|
|
92
|
+
const isTty = options.tty ?? process.stdout.isTTY === true;
|
|
93
|
+
const summaryLines = [
|
|
94
|
+
"Tutti is hosting this project.",
|
|
95
|
+
`Project: ${formatProjectLabel(options.workspaceRoot, options.projectId)}`,
|
|
96
|
+
"Collaboration surface: Feishu",
|
|
97
|
+
];
|
|
98
|
+
if (options.feishuSetup?.state === "connected") {
|
|
99
|
+
summaryLines.push("Feishu conversation: connected");
|
|
100
|
+
}
|
|
101
|
+
else if (options.feishuSetup?.state === "pairing_required" &&
|
|
102
|
+
options.feishuSetup.visibility === "visible_once") {
|
|
103
|
+
summaryLines.push(`Connect command: ${options.feishuSetup.command}`);
|
|
104
|
+
summaryLines.push(`Connect code expires at: ${options.feishuSetup.expires_at}`);
|
|
105
|
+
}
|
|
106
|
+
else if (options.feishuSetup?.state === "pairing_required") {
|
|
107
|
+
summaryLines.push("Feishu conversation: pairing code is not recoverable; restart from Local Console to rotate it");
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
summaryLines.push("Feishu Owner: open this project through Local Console to connect an account");
|
|
111
|
+
}
|
|
112
|
+
return [
|
|
113
|
+
...(isTty ? [CLEAR, renderTuttiTerminalLogo({ tty: true }), ""] : []),
|
|
114
|
+
isTty ? borderedBlock(summaryLines) : summaryLines.join("\n"),
|
|
115
|
+
].join("\n");
|
|
116
|
+
}
|
|
91
117
|
async function ensureProviderConfigured(preparation) {
|
|
92
118
|
if (preparation.provider_status === "configured") {
|
|
93
119
|
return;
|
|
@@ -133,14 +159,26 @@ export async function runBackgroundLaunchCommand(options) {
|
|
|
133
159
|
confirm: confirmFromTty,
|
|
134
160
|
});
|
|
135
161
|
await ensureProviderConfigured(preparation);
|
|
136
|
-
spawnDetachedHost({
|
|
162
|
+
const startup = spawnDetachedHost({
|
|
163
|
+
tuttiHome: preparation.tutti_home,
|
|
164
|
+
projectId: preparation.project_id,
|
|
137
165
|
workspaceRoot: preparation.workspace_root,
|
|
138
166
|
});
|
|
139
167
|
const ready = await waitForManagedHostReady({
|
|
140
168
|
tuttiHome: preparation.tutti_home,
|
|
141
169
|
projectId: preparation.project_id,
|
|
142
170
|
workspaceRoot: preparation.workspace_root,
|
|
171
|
+
startupAttemptRef: startup.startup_attempt_ref,
|
|
143
172
|
});
|
|
173
|
+
if (preparation.machine_local.binding.surface_profile.kind === "feishu") {
|
|
174
|
+
process.stdout.write(`${renderFeishuCompletionPage({
|
|
175
|
+
...(ready.feishu_setup === undefined ? {} : { feishuSetup: ready.feishu_setup }),
|
|
176
|
+
projectId: preparation.project_id,
|
|
177
|
+
workspaceRoot: preparation.workspace_root,
|
|
178
|
+
tty: process.stdout.isTTY === true,
|
|
179
|
+
})}\n`);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
144
182
|
if (ready.join_url === undefined) {
|
|
145
183
|
throw new LaunchError("relay_registration_failed", "Host started but Relay did not return a visible join URL", "Run `tutti invite` to rotate a fresh invite link.");
|
|
146
184
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ProjectSurface } from "@tutti/shared/domain";
|
|
1
2
|
import { type ProjectId } from "@tutti/shared/ids";
|
|
2
3
|
import { type OpenAiProviderConfigProjection } from "../../providers/openai/index.js";
|
|
3
4
|
import { type GitBootstrapResult, type LaunchConfirmationHandler } from "./git-bootstrap.js";
|
|
@@ -13,6 +14,7 @@ export type LaunchPreparationOptions = {
|
|
|
13
14
|
allowWorkspaceRootTakeover?: boolean;
|
|
14
15
|
projectDisplayName?: string;
|
|
15
16
|
projectDescription?: string;
|
|
17
|
+
projectSurface?: ProjectSurface;
|
|
16
18
|
};
|
|
17
19
|
export type LaunchProviderStatus = OpenAiProviderConfigProjection["status"];
|
|
18
20
|
export type LaunchPreparationResult = {
|
|
@@ -86,6 +86,9 @@ export async function prepareLaunchProject(options) {
|
|
|
86
86
|
if (options.now !== undefined) {
|
|
87
87
|
bindingOptions.now = options.now;
|
|
88
88
|
}
|
|
89
|
+
if (options.projectSurface !== undefined) {
|
|
90
|
+
bindingOptions.surface = options.projectSurface;
|
|
91
|
+
}
|
|
89
92
|
const machineLocal = ensureMachineProjectBinding(bindingOptions);
|
|
90
93
|
const providerConfig = readOpenAiProviderConfigProjection({
|
|
91
94
|
tuttiHome,
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { type ProjectSurface, type ProjectSurfaceProfile } from "@tutti/shared/domain";
|
|
1
2
|
import { type ProjectId, type RelayProjectRef } from "@tutti/shared/ids";
|
|
2
3
|
export type MachineProjectBindingRecord = {
|
|
3
4
|
project_id: ProjectId;
|
|
5
|
+
surface_profile: ProjectSurfaceProfile;
|
|
4
6
|
workspace_root: string;
|
|
5
7
|
local_store_root: string;
|
|
6
8
|
display_name_snapshot?: string;
|
|
@@ -17,6 +19,13 @@ export type HostRegistrationSecretFile = {
|
|
|
17
19
|
created_at: string;
|
|
18
20
|
updated_at: string;
|
|
19
21
|
};
|
|
22
|
+
export type FeishuOwnerAccountHandoffFile = {
|
|
23
|
+
kind: "feishu_owner_account_handoff_v1";
|
|
24
|
+
project_id: ProjectId;
|
|
25
|
+
token: string;
|
|
26
|
+
created_at: string;
|
|
27
|
+
expires_at: string;
|
|
28
|
+
};
|
|
20
29
|
export type MachineRuntimeEndpointRecord = {
|
|
21
30
|
project_id: ProjectId;
|
|
22
31
|
pid: number;
|
|
@@ -31,6 +40,7 @@ export type EnsureMachineProjectBindingOptions = {
|
|
|
31
40
|
workspaceRoot: string;
|
|
32
41
|
displayNameSnapshot?: string;
|
|
33
42
|
relayUrl?: string;
|
|
43
|
+
surface?: ProjectSurface;
|
|
34
44
|
allowWorkspaceRootTakeover?: boolean;
|
|
35
45
|
now?: () => Date;
|
|
36
46
|
};
|
|
@@ -48,6 +58,16 @@ export declare function ensureHostLogFile(logFilePath: string): void;
|
|
|
48
58
|
export declare function appendHostLogLine(logFilePath: string, line: string): void;
|
|
49
59
|
export declare function createHostRegistrationSecretRef(projectId: ProjectId): string;
|
|
50
60
|
export declare function getHostRegistrationSecretPath(tuttiHome: string, projectId: ProjectId): string;
|
|
61
|
+
export declare function getFeishuOwnerAccountHandoffPath(tuttiHome: string, projectId: ProjectId): string;
|
|
62
|
+
export declare function writeFeishuOwnerAccountHandoff(options: {
|
|
63
|
+
tuttiHome: string;
|
|
64
|
+
projectId: ProjectId;
|
|
65
|
+
token: string;
|
|
66
|
+
expiresAt: string;
|
|
67
|
+
now?: Date;
|
|
68
|
+
}): void;
|
|
69
|
+
export declare function readFeishuOwnerAccountHandoff(tuttiHome: string, projectId: ProjectId, now?: Date): FeishuOwnerAccountHandoffFile | null;
|
|
70
|
+
export declare function deleteFeishuOwnerAccountHandoff(tuttiHome: string, projectId: ProjectId): void;
|
|
51
71
|
export declare function createHostRegistrationSecret(): string;
|
|
52
72
|
export declare function createMachineRuntimeEndpointToken(): string;
|
|
53
73
|
export declare function readMachineProjectBinding(tuttiHome: string, projectId: ProjectId): MachineProjectBindingRecord | null;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
2
|
import { appendFileSync, chmodSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
3
3
|
import { dirname, join, resolve } from "node:path";
|
|
4
|
+
import { createProjectSurfaceProfile, isProjectSurfaceProfile, } from "@tutti/shared/domain";
|
|
4
5
|
import { ID_PREFIXES, isPrefixedId } from "@tutti/shared/ids";
|
|
5
6
|
import { LaunchError } from "./errors.js";
|
|
6
7
|
function isRecord(value) {
|
|
@@ -55,6 +56,58 @@ export function createHostRegistrationSecretRef(projectId) {
|
|
|
55
56
|
export function getHostRegistrationSecretPath(tuttiHome, projectId) {
|
|
56
57
|
return join(tuttiHome, "credentials", "host-registration", `${projectId}.json`);
|
|
57
58
|
}
|
|
59
|
+
export function getFeishuOwnerAccountHandoffPath(tuttiHome, projectId) {
|
|
60
|
+
return join(tuttiHome, "credentials", "feishu-owner-handoffs", `${projectId}.json`);
|
|
61
|
+
}
|
|
62
|
+
export function writeFeishuOwnerAccountHandoff(options) {
|
|
63
|
+
const now = options.now ?? new Date();
|
|
64
|
+
if (!/^[A-Za-z0-9_-]{32,128}$/u.test(options.token) ||
|
|
65
|
+
Number.isNaN(Date.parse(options.expiresAt)) ||
|
|
66
|
+
Date.parse(options.expiresAt) <= now.getTime()) {
|
|
67
|
+
throw new LaunchError("relay_registration_failed", "The private Feishu Owner account handoff is invalid or expired", "Return through the Local Console account gate, then launch the Feishu project again.");
|
|
68
|
+
}
|
|
69
|
+
const value = {
|
|
70
|
+
kind: "feishu_owner_account_handoff_v1",
|
|
71
|
+
project_id: options.projectId,
|
|
72
|
+
token: options.token,
|
|
73
|
+
created_at: now.toISOString(),
|
|
74
|
+
expires_at: options.expiresAt,
|
|
75
|
+
};
|
|
76
|
+
writePrivateJsonFile(getFeishuOwnerAccountHandoffPath(options.tuttiHome, options.projectId), value);
|
|
77
|
+
}
|
|
78
|
+
export function readFeishuOwnerAccountHandoff(tuttiHome, projectId, now = new Date()) {
|
|
79
|
+
const filePath = getFeishuOwnerAccountHandoffPath(tuttiHome, projectId);
|
|
80
|
+
const readResult = readJsonFile(filePath);
|
|
81
|
+
if (readResult.kind === "missing") {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
const raw = readResult.value;
|
|
85
|
+
if (!isRecord(raw) ||
|
|
86
|
+
raw.kind !== "feishu_owner_account_handoff_v1" ||
|
|
87
|
+
raw.project_id !== projectId ||
|
|
88
|
+
typeof raw.token !== "string" ||
|
|
89
|
+
!/^[A-Za-z0-9_-]{32,128}$/u.test(raw.token) ||
|
|
90
|
+
typeof raw.created_at !== "string" ||
|
|
91
|
+
Number.isNaN(Date.parse(raw.created_at)) ||
|
|
92
|
+
typeof raw.expires_at !== "string" ||
|
|
93
|
+
Number.isNaN(Date.parse(raw.expires_at))) {
|
|
94
|
+
throw new LaunchError("relay_registration_failed", "The private Feishu Owner account handoff file is invalid", "Return through the Local Console account gate, then launch the Feishu project again.");
|
|
95
|
+
}
|
|
96
|
+
if (Date.parse(raw.expires_at) <= now.getTime()) {
|
|
97
|
+
unlinkSync(filePath);
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
kind: raw.kind,
|
|
102
|
+
project_id: raw.project_id,
|
|
103
|
+
token: raw.token,
|
|
104
|
+
created_at: raw.created_at,
|
|
105
|
+
expires_at: raw.expires_at,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
export function deleteFeishuOwnerAccountHandoff(tuttiHome, projectId) {
|
|
109
|
+
rmSync(getFeishuOwnerAccountHandoffPath(tuttiHome, projectId), { force: true });
|
|
110
|
+
}
|
|
58
111
|
export function createHostRegistrationSecret() {
|
|
59
112
|
return randomBytes(32).toString("base64url");
|
|
60
113
|
}
|
|
@@ -81,11 +134,13 @@ export function readMachineProjectBinding(tuttiHome, projectId) {
|
|
|
81
134
|
typeof raw.host_registration_secret_ref !== "string") ||
|
|
82
135
|
(raw.relay_url !== undefined &&
|
|
83
136
|
(typeof raw.relay_url !== "string" || raw.relay_url.trim() === "")) ||
|
|
84
|
-
(raw.created_at !== undefined && typeof raw.created_at !== "string")
|
|
137
|
+
(raw.created_at !== undefined && typeof raw.created_at !== "string") ||
|
|
138
|
+
(raw.surface_profile !== undefined && !isProjectSurfaceProfile(raw.surface_profile))) {
|
|
85
139
|
throw new LaunchError("project_identity_conflict", "Machine-local project binding is invalid or conflicts with the Git config project identity", "Inspect or remove the project binding under TUTTI_HOME after confirming it is not needed.");
|
|
86
140
|
}
|
|
87
141
|
const binding = {
|
|
88
142
|
project_id: raw.project_id,
|
|
143
|
+
surface_profile: raw.surface_profile === undefined ? createProjectSurfaceProfile() : raw.surface_profile,
|
|
89
144
|
workspace_root: raw.workspace_root,
|
|
90
145
|
local_store_root: raw.local_store_root,
|
|
91
146
|
updated_at: raw.updated_at,
|
|
@@ -259,6 +314,10 @@ export function ensureMachineProjectBinding(options) {
|
|
|
259
314
|
const bindingPath = getMachineProjectBindingPath(options.tuttiHome, options.projectId);
|
|
260
315
|
const secret = ensureHostRegistrationSecret(options.tuttiHome, options.projectId, now);
|
|
261
316
|
const existing = readMachineProjectBinding(options.tuttiHome, options.projectId);
|
|
317
|
+
const requestedSurface = options.surface ?? existing?.surface_profile.kind ?? "standalone";
|
|
318
|
+
if (existing !== null && existing.surface_profile.kind !== requestedSurface) {
|
|
319
|
+
throw new LaunchError("project_surface_conflict", `This project is already configured for the ${existing.surface_profile.kind} collaboration surface`, "Use the existing project surface. Surface migration requires a separate explicit migration flow.");
|
|
320
|
+
}
|
|
262
321
|
if (existing !== null &&
|
|
263
322
|
resolve(existing.workspace_root) !== workspaceRoot &&
|
|
264
323
|
options.allowWorkspaceRootTakeover !== true) {
|
|
@@ -266,6 +325,7 @@ export function ensureMachineProjectBinding(options) {
|
|
|
266
325
|
}
|
|
267
326
|
const binding = {
|
|
268
327
|
project_id: options.projectId,
|
|
328
|
+
surface_profile: createProjectSurfaceProfile(requestedSurface),
|
|
269
329
|
workspace_root: workspaceRoot,
|
|
270
330
|
local_store_root: localStoreRoot,
|
|
271
331
|
host_registration_secret_ref: existing?.host_registration_secret_ref ?? secret.ref,
|
|
@@ -1,16 +1,30 @@
|
|
|
1
|
+
import { type ProjectId } from "@tutti/shared/ids";
|
|
1
2
|
import { type FetchLike } from "./host-runtime-endpoint.js";
|
|
2
3
|
import { type MachineRuntimeEndpointRecord } from "./machine-local.js";
|
|
3
|
-
import type {
|
|
4
|
+
import type { FeishuSetupProjection } from "@tutti/shared/schemas/internal";
|
|
4
5
|
export type ManagedHostReadyResult = {
|
|
5
6
|
endpoint: MachineRuntimeEndpointRecord;
|
|
6
7
|
join_url?: string;
|
|
7
8
|
join_token_expires_at?: string;
|
|
8
9
|
join_token_reusable?: boolean;
|
|
10
|
+
feishu_setup?: FeishuSetupProjection;
|
|
9
11
|
};
|
|
12
|
+
export type DetachedHostStartup = {
|
|
13
|
+
startup_attempt_ref: string;
|
|
14
|
+
pid?: number;
|
|
15
|
+
};
|
|
16
|
+
export declare function normalizeDetachedHostExecArgv(execArgv: readonly string[], resolveImport?: (specifier: string) => string): string[];
|
|
10
17
|
export declare function spawnDetachedHost(options: {
|
|
18
|
+
tuttiHome: string;
|
|
19
|
+
projectId: ProjectId;
|
|
11
20
|
workspaceRoot: string;
|
|
12
21
|
env?: NodeJS.ProcessEnv;
|
|
13
22
|
inheritProcessEnv?: boolean;
|
|
23
|
+
}): DetachedHostStartup;
|
|
24
|
+
export declare function recordInternalHostStartupFailure(options: {
|
|
25
|
+
tuttiHome: string;
|
|
26
|
+
error: unknown;
|
|
27
|
+
env?: NodeJS.ProcessEnv;
|
|
14
28
|
}): void;
|
|
15
29
|
export declare function waitForManagedHostReady(options: {
|
|
16
30
|
tuttiHome: string;
|
|
@@ -19,5 +33,6 @@ export declare function waitForManagedHostReady(options: {
|
|
|
19
33
|
fetchImpl?: FetchLike;
|
|
20
34
|
inviteMode?: "required" | "preserve";
|
|
21
35
|
timeoutMs?: number;
|
|
36
|
+
startupAttemptRef?: string;
|
|
22
37
|
}): Promise<ManagedHostReadyResult>;
|
|
23
38
|
//# sourceMappingURL=managed-host.d.ts.map
|