@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.
Files changed (106) hide show
  1. package/README.md +9 -4
  2. package/dist/chat-assistant/index.d.ts +6 -1
  3. package/dist/chat-assistant/index.js +22 -12
  4. package/dist/collaboration-ingestion/external-source.d.ts +15 -0
  5. package/dist/collaboration-ingestion/external-source.js +120 -0
  6. package/dist/collaboration-ingestion/index.d.ts +5 -0
  7. package/dist/collaboration-ingestion/index.js +4 -0
  8. package/dist/collaboration-ingestion/invalidations.d.ts +4 -0
  9. package/dist/collaboration-ingestion/invalidations.js +15 -0
  10. package/dist/collaboration-ingestion/main-chat.d.ts +33 -0
  11. package/dist/collaboration-ingestion/main-chat.js +67 -0
  12. package/dist/collaboration-ingestion/reference-files.d.ts +29 -0
  13. package/dist/collaboration-ingestion/reference-files.js +158 -0
  14. package/dist/collaboration-ingestion/types.d.ts +19 -0
  15. package/dist/collaboration-ingestion/types.js +2 -0
  16. package/dist/platform-outbound/coordinator.d.ts +17 -0
  17. package/dist/platform-outbound/coordinator.js +48 -0
  18. package/dist/platform-outbound/index.d.ts +5 -0
  19. package/dist/platform-outbound/index.js +5 -0
  20. package/dist/platform-outbound/repository.d.ts +30 -0
  21. package/dist/platform-outbound/repository.js +108 -0
  22. package/dist/platform-outbound/types.d.ts +38 -0
  23. package/dist/platform-outbound/types.js +2 -0
  24. package/dist/platform-outbound/worker.d.ts +36 -0
  25. package/dist/platform-outbound/worker.js +164 -0
  26. package/dist/server-shell/cli/cli.js +9 -0
  27. package/dist/server-shell/cli/errors.d.ts +1 -1
  28. package/dist/server-shell/cli/host-lifecycle.d.ts +2 -0
  29. package/dist/server-shell/cli/host-lifecycle.js +34 -1
  30. package/dist/server-shell/cli/host-relay-connection-manager.js +22 -11
  31. package/dist/server-shell/cli/host-relay-status.js +1 -0
  32. package/dist/server-shell/cli/host-runtime-endpoint.js +17 -1
  33. package/dist/server-shell/cli/host-server-runtime.d.ts +2 -0
  34. package/dist/server-shell/cli/host-server-runtime.js +88 -14
  35. package/dist/server-shell/cli/launch-command.d.ts +7 -0
  36. package/dist/server-shell/cli/launch-command.js +39 -1
  37. package/dist/server-shell/cli/launch.d.ts +2 -0
  38. package/dist/server-shell/cli/launch.js +3 -0
  39. package/dist/server-shell/cli/machine-local.d.ts +20 -0
  40. package/dist/server-shell/cli/machine-local.js +61 -1
  41. package/dist/server-shell/cli/managed-host.d.ts +16 -1
  42. package/dist/server-shell/cli/managed-host.js +191 -23
  43. package/dist/server-shell/cli/relay-registration.d.ts +4 -1
  44. package/dist/server-shell/cli/relay-registration.js +36 -7
  45. package/dist/server-shell/cli/runtime-commands.d.ts +5 -1
  46. package/dist/server-shell/cli/runtime-commands.js +9 -3
  47. package/dist/server-shell/http/host-tunnel.d.ts +5 -0
  48. package/dist/server-shell/http/host-tunnel.js +109 -0
  49. package/dist/server-shell/http/routes/local-control.d.ts +2 -0
  50. package/dist/server-shell/http/routes/local-control.js +2 -0
  51. package/dist/server-shell/http/routes/project-api/invalidations.d.ts +0 -1
  52. package/dist/server-shell/http/routes/project-api/invalidations.js +0 -3
  53. package/dist/server-shell/http/routes/project-api/messages-routes.js +32 -63
  54. package/dist/server-shell/http/routes/project-api/openapi-workspace-routes.d.ts +8 -4
  55. package/dist/server-shell/http/routes/project-api/openapi.d.ts +8 -4
  56. package/dist/server-shell/http/routes/project-api/reference-files-routes.js +25 -94
  57. package/dist/server-shell/http/routes/project-api/reference-files.d.ts +1 -11
  58. package/dist/server-shell/http/routes/project-api/reference-files.js +2 -87
  59. package/dist/server-shell/http/routes/project-api/schemas.d.ts +8 -4
  60. package/dist/server-shell/http/routes/project-api/schemas.js +8 -3
  61. package/dist/server-shell/http/routes/project-api/session-requirements.js +34 -2
  62. package/dist/server-shell/http/routes/project-api/types.d.ts +3 -1
  63. package/dist/server-shell/http/routes/project-api/workspace-projections.js +8 -2
  64. package/dist/server-shell/local-console/package-update-hosts.js +4 -1
  65. package/dist/server-shell/local-console/project-service.d.ts +11 -2
  66. package/dist/server-shell/local-console/project-service.js +36 -8
  67. package/dist/server-shell/local-console/server.js +6 -1
  68. package/dist/server-shell/local-console/session.d.ts +4 -0
  69. package/dist/server-shell/local-console/session.js +14 -0
  70. package/dist/server-shell/session/relay-session-context.d.ts +12 -1
  71. package/dist/server-shell/session/relay-session-context.js +19 -1
  72. package/migrations/0016_external_source_mappings.sql +141 -0
  73. package/migrations/0017_platform_outbound_intents.sql +31 -0
  74. package/migrations/README.md +3 -1
  75. package/node_modules/@tutti/relay-client/dist/host-control.d.ts +26 -1
  76. package/node_modules/@tutti/relay-client/dist/host-control.js +62 -0
  77. package/node_modules/@tutti/relay-client/dist/tunnel-frames.js +32 -6
  78. package/node_modules/@tutti/relay-client/dist/tunnel-types.d.ts +5 -1
  79. package/node_modules/@tutti/shared/dist/domain/index.d.ts +9 -0
  80. package/node_modules/@tutti/shared/dist/domain/index.js +14 -0
  81. package/node_modules/@tutti/shared/dist/ids/index.d.ts +6 -0
  82. package/node_modules/@tutti/shared/dist/ids/index.js +4 -0
  83. package/node_modules/@tutti/shared/dist/schemas/api/index.d.ts +4 -0
  84. package/node_modules/@tutti/shared/dist/schemas/api/index.js +2 -0
  85. package/node_modules/@tutti/shared/dist/schemas/api/local-console-account-handoffs.d.ts +13 -0
  86. package/node_modules/@tutti/shared/dist/schemas/api/local-console-account-handoffs.js +16 -0
  87. package/node_modules/@tutti/shared/dist/schemas/api/platform-sessions.d.ts +43 -0
  88. package/node_modules/@tutti/shared/dist/schemas/api/platform-sessions.js +44 -0
  89. package/node_modules/@tutti/shared/dist/schemas/domain/index.d.ts +7 -0
  90. package/node_modules/@tutti/shared/dist/schemas/domain/index.js +10 -0
  91. package/node_modules/@tutti/shared/dist/schemas/internal/index.d.ts +29 -0
  92. package/node_modules/@tutti/shared/dist/schemas/internal/index.js +24 -1
  93. package/node_modules/@tutti/shared/dist/schemas/internal/platform-integration.d.ts +1346 -0
  94. package/node_modules/@tutti/shared/dist/schemas/internal/platform-integration.js +516 -0
  95. package/node_modules/@tutti/shared/dist/utils/feishu-diagnostics/index.d.ts +10 -0
  96. package/node_modules/@tutti/shared/dist/utils/feishu-diagnostics/index.js +19 -0
  97. package/node_modules/@tutti/shared/dist/utils/index.d.ts +1 -0
  98. package/node_modules/@tutti/shared/dist/utils/index.js +1 -0
  99. package/node_modules/@tutti/shared/dist/utils/redaction/index.d.ts +1 -1
  100. package/node_modules/@tutti/shared/dist/utils/redaction/index.js +18 -0
  101. package/package.json +1 -1
  102. package/web/assets/{homepage-motion-scene-BtT5bd30.js → homepage-motion-scene-DEVrQ9aj.js} +1 -1
  103. package/web/assets/{index-CxCTqGFO.css → index-6i9Uk7fS.css} +1 -1
  104. package/web/assets/index-Lxtst2sl.js +69 -0
  105. package/web/index.html +2 -2
  106. 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
- const line = serializeRuntimeLogLine({
100
- level: "info",
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: () => projectRuntimeLifecycle.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 { ProjectId } from "@tutti/shared/ids";
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