@creezio/tasks 0.22.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 (106) hide show
  1. package/README.md +279 -0
  2. package/dist/.creezio-src-hash.json +4 -0
  3. package/dist/ai-task-agent.d.ts +47 -0
  4. package/dist/ai-task-agent.d.ts.map +1 -0
  5. package/dist/ai-task-agent.js +558 -0
  6. package/dist/ai-task-agent.js.map +1 -0
  7. package/dist/ai-task-runner.d.ts +78 -0
  8. package/dist/ai-task-runner.d.ts.map +1 -0
  9. package/dist/ai-task-runner.js +645 -0
  10. package/dist/ai-task-runner.js.map +1 -0
  11. package/dist/api-mount.d.ts +4 -0
  12. package/dist/api-mount.d.ts.map +1 -0
  13. package/dist/api-mount.js +91 -0
  14. package/dist/api-mount.js.map +1 -0
  15. package/dist/assistant-adapter.d.ts +7 -0
  16. package/dist/assistant-adapter.d.ts.map +1 -0
  17. package/dist/assistant-adapter.js +96 -0
  18. package/dist/assistant-adapter.js.map +1 -0
  19. package/dist/brand/config.d.ts +165 -0
  20. package/dist/brand/config.d.ts.map +1 -0
  21. package/dist/brand/config.js +39 -0
  22. package/dist/brand/config.js.map +1 -0
  23. package/dist/env-bridge.d.ts +12 -0
  24. package/dist/env-bridge.d.ts.map +1 -0
  25. package/dist/env-bridge.js +33 -0
  26. package/dist/env-bridge.js.map +1 -0
  27. package/dist/hono-routes.d.ts +8 -0
  28. package/dist/hono-routes.d.ts.map +1 -0
  29. package/dist/hono-routes.js +563 -0
  30. package/dist/hono-routes.js.map +1 -0
  31. package/dist/index.d.ts +29 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +21 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/kanban-service.d.ts +136 -0
  36. package/dist/kanban-service.d.ts.map +1 -0
  37. package/dist/kanban-service.js +580 -0
  38. package/dist/kanban-service.js.map +1 -0
  39. package/dist/mcp-host-tools.d.ts +59 -0
  40. package/dist/mcp-host-tools.d.ts.map +1 -0
  41. package/dist/mcp-host-tools.js +326 -0
  42. package/dist/mcp-host-tools.js.map +1 -0
  43. package/dist/mcp-workspace-tools.d.ts +15 -0
  44. package/dist/mcp-workspace-tools.d.ts.map +1 -0
  45. package/dist/mcp-workspace-tools.js +338 -0
  46. package/dist/mcp-workspace-tools.js.map +1 -0
  47. package/dist/memory-store.d.ts +3 -0
  48. package/dist/memory-store.d.ts.map +1 -0
  49. package/dist/memory-store.js +75 -0
  50. package/dist/memory-store.js.map +1 -0
  51. package/dist/sqlite-driver.d.ts +13 -0
  52. package/dist/sqlite-driver.d.ts.map +1 -0
  53. package/dist/sqlite-driver.js +13 -0
  54. package/dist/sqlite-driver.js.map +1 -0
  55. package/dist/sqlite-store.d.ts +15 -0
  56. package/dist/sqlite-store.d.ts.map +1 -0
  57. package/dist/sqlite-store.js +113 -0
  58. package/dist/sqlite-store.js.map +1 -0
  59. package/dist/task-runs.d.ts +108 -0
  60. package/dist/task-runs.d.ts.map +1 -0
  61. package/dist/task-runs.js +427 -0
  62. package/dist/task-runs.js.map +1 -0
  63. package/dist/types.d.ts +35 -0
  64. package/dist/types.d.ts.map +1 -0
  65. package/dist/types.js +15 -0
  66. package/dist/types.js.map +1 -0
  67. package/dist-cjs/ai-task-agent.js +569 -0
  68. package/dist-cjs/ai-task-agent.js.map +1 -0
  69. package/dist-cjs/ai-task-runner.js +660 -0
  70. package/dist-cjs/ai-task-runner.js.map +1 -0
  71. package/dist-cjs/api-mount.js +94 -0
  72. package/dist-cjs/api-mount.js.map +1 -0
  73. package/dist-cjs/assistant-adapter.js +99 -0
  74. package/dist-cjs/assistant-adapter.js.map +1 -0
  75. package/dist-cjs/brand/config.js +47 -0
  76. package/dist-cjs/brand/config.js.map +1 -0
  77. package/dist-cjs/env-bridge.js +36 -0
  78. package/dist-cjs/env-bridge.js.map +1 -0
  79. package/dist-cjs/hono-routes.js +566 -0
  80. package/dist-cjs/hono-routes.js.map +1 -0
  81. package/dist-cjs/index.js +117 -0
  82. package/dist-cjs/index.js.map +1 -0
  83. package/dist-cjs/kanban-service.js +601 -0
  84. package/dist-cjs/kanban-service.js.map +1 -0
  85. package/dist-cjs/mcp-host-tools.js +333 -0
  86. package/dist-cjs/mcp-host-tools.js.map +1 -0
  87. package/dist-cjs/mcp-workspace-tools.js +342 -0
  88. package/dist-cjs/mcp-workspace-tools.js.map +1 -0
  89. package/dist-cjs/memory-store.js +81 -0
  90. package/dist-cjs/memory-store.js.map +1 -0
  91. package/dist-cjs/package.json +3 -0
  92. package/dist-cjs/sqlite-driver.js +16 -0
  93. package/dist-cjs/sqlite-driver.js.map +1 -0
  94. package/dist-cjs/sqlite-store.js +119 -0
  95. package/dist-cjs/sqlite-store.js.map +1 -0
  96. package/dist-cjs/task-runs.js +457 -0
  97. package/dist-cjs/task-runs.js.map +1 -0
  98. package/dist-cjs/types.js +18 -0
  99. package/dist-cjs/types.js.map +1 -0
  100. package/package.json +79 -0
  101. package/scripts/port-from-tempoflow.mjs +405 -0
  102. package/ui/ai-activity-panel.tsx +559 -0
  103. package/ui/index.ts +7 -0
  104. package/ui/task-detail-sheet.tsx +668 -0
  105. package/ui/tasks-kanban-client.tsx +647 -0
  106. package/ui/tasks-types.ts +120 -0
@@ -0,0 +1,405 @@
1
+ /**
2
+ * One-shot port TempoFlow gold → @creezio/tasks (platform-generic).
3
+ * Run from packages/tasks: node scripts/port-from-tempoflow.mjs
4
+ */
5
+ import fs from "node:fs";
6
+ import path from "node:path";
7
+ import { fileURLToPath } from "node:url";
8
+
9
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
10
+ const PKG = path.resolve(__dirname, "..");
11
+ const SRC = path.join(PKG, "src");
12
+ const TF = path.resolve(PKG, "../../../tempoflow2/crm/src");
13
+
14
+ function readTf(rel) {
15
+ return fs.readFileSync(path.join(TF, rel), "utf8");
16
+ }
17
+
18
+ function writeSrc(rel, content) {
19
+ const dest = path.join(SRC, rel);
20
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
21
+ fs.writeFileSync(dest, content);
22
+ console.log("wrote", rel, content.split("\n").length, "lines");
23
+ }
24
+
25
+ const BRAND_CONFIG = `/**
26
+ * Configuration marque pour le runtime tasks plateforme.
27
+ * Les marques appellent \`configureTasksBrand\` au boot (server / instrumentation).
28
+ */
29
+ import type { Context } from "hono";
30
+
31
+ export type TasksUserKind = "human" | "ai";
32
+
33
+ export type TasksUser = {
34
+ id: string;
35
+ username: string;
36
+ role: "owner" | "collaborator";
37
+ kind: TasksUserKind;
38
+ active: boolean;
39
+ permissions: string[];
40
+ };
41
+
42
+ export type TasksSession = {
43
+ sub: string;
44
+ email: string;
45
+ role: "owner" | "collaborator";
46
+ permissions?: string[];
47
+ actorSub?: string;
48
+ actorRole?: "owner";
49
+ };
50
+
51
+ export type TasksSqliteStatement = {
52
+ run: (...args: unknown[]) => { changes: number };
53
+ get: (...args: unknown[]) => unknown;
54
+ all: (...args: unknown[]) => unknown[];
55
+ };
56
+
57
+ export type TasksSqliteDb = {
58
+ prepare: (sql: string) => TasksSqliteStatement;
59
+ };
60
+
61
+ export type TasksDbAdapter = {
62
+ getWriteDb: () => TasksSqliteDb;
63
+ queryAll: <T = Record<string, unknown>>(sql: string, params?: unknown[]) => T[];
64
+ queryOne: <T = Record<string, unknown>>(
65
+ sql: string,
66
+ params?: unknown[],
67
+ ) => T | null | undefined;
68
+ tableExists: (name: string) => boolean;
69
+ };
70
+
71
+ export type TasksUsersAdapter = {
72
+ getById: (id: string) => TasksUser | null;
73
+ list: () => TasksUser[];
74
+ getOwner: () => TasksUser | null;
75
+ ready: () => boolean;
76
+ };
77
+
78
+ export type TasksPresenceAdapter = {
79
+ isDesktopOnline: (userId: string) => boolean;
80
+ listOnlineBridges: () => Array<{
81
+ userId: string;
82
+ deviceId: string;
83
+ deviceLabel?: string | null;
84
+ bridgeConnected: boolean;
85
+ online: boolean;
86
+ }>;
87
+ };
88
+
89
+ export type TasksWorkspaceAdapter = {
90
+ ensureOnHost: (opts: {
91
+ aiUserId: string;
92
+ hostUserId: string;
93
+ }) => Promise<unknown>;
94
+ navigate: (opts: {
95
+ aiUserId: string;
96
+ hostUserId: string;
97
+ path: string;
98
+ }) => Promise<Record<string, unknown>>;
99
+ openTab: (opts: {
100
+ aiUserId: string;
101
+ hostUserId: string;
102
+ url: string;
103
+ title?: string;
104
+ params?: Record<string, unknown>;
105
+ }) => Promise<Record<string, unknown>>;
106
+ listTabs: (opts: {
107
+ aiUserId: string;
108
+ hostUserId: string;
109
+ }) => Promise<Record<string, unknown>>;
110
+ webAction: (opts: {
111
+ aiUserId: string;
112
+ hostUserId: string;
113
+ webType: string;
114
+ params: Record<string, unknown>;
115
+ tabId?: string;
116
+ }) => Promise<Record<string, unknown>>;
117
+ startScreencast: (
118
+ aiUserId: string,
119
+ hostUserId: string,
120
+ ) => Promise<unknown>;
121
+ stopScreencast: (
122
+ aiUserId: string,
123
+ hostUserId: string,
124
+ ) => Promise<unknown>;
125
+ };
126
+
127
+ export type TasksNavAdapter = {
128
+ permissionForPath: (pathname: string) => string | null;
129
+ hasPermission: (
130
+ permissions: readonly string[] | undefined,
131
+ required: string | null,
132
+ ) => boolean;
133
+ };
134
+
135
+ export type ResolvedExternalTab = {
136
+ url: string;
137
+ title?: string;
138
+ params?: Record<string, unknown>;
139
+ };
140
+
141
+ export type TasksExternalTabsAdapter = {
142
+ resolve: (input: {
143
+ url?: string;
144
+ fournisseurId?: string | number;
145
+ title?: string;
146
+ }) =>
147
+ | { ok: true; value: ResolvedExternalTab }
148
+ | { ok: false; error: string; code?: string };
149
+ toWorkspaceParams: (resolved: ResolvedExternalTab) => Record<string, unknown>;
150
+ };
151
+
152
+ export type TasksScreencastAdapter = {
153
+ viewerCount: (aiUserId: string) => number;
154
+ subscribe: (
155
+ aiUserId: string,
156
+ listener: (frame: { jpegBase64: string; ts?: number }) => void,
157
+ ) => () => void;
158
+ };
159
+
160
+ export type TasksAuthAdapter = {
161
+ getSessionFromContext: (c: Context) => Promise<TasksSession | null>;
162
+ sessionActorIsOwner: (session: TasksSession | null) => boolean;
163
+ sessionIsImpersonating: (session: TasksSession | null) => boolean;
164
+ };
165
+
166
+ export type TasksBrandConfig = {
167
+ /** Nom produit (prompt agent, labels Hermes). */
168
+ productName: string;
169
+ /** Description courte domaine métier pour le prompt agent. */
170
+ productDomain: string;
171
+ /** Label footer Hermes, ex. "TempoFlow CRM". */
172
+ hermesSourceLabel: string;
173
+ /** Skill Hermes à charger, ex. "tempoflow2-crm". */
174
+ hermesSkill: string;
175
+ /** Préfixe env AI, ex. "TF2_AI" → TF2_AI_MODEL, etc. */
176
+ envPrefix: string;
177
+ /** Préfixe idempotency clés CRM, ex. "crm". */
178
+ idempotencyPrefix: string;
179
+ /** Préfixe idempotency assistant, ex. "asst". */
180
+ assistantIdempotencyPrefix: string;
181
+ /** Chemin UI kanban, défaut "/taches". */
182
+ taskHref: string;
183
+ /** Exemples de routes dans le prompt agent. */
184
+ examplePaths: string[];
185
+
186
+ db: TasksDbAdapter;
187
+ users: TasksUsersAdapter;
188
+ presence: TasksPresenceAdapter;
189
+ workspace: TasksWorkspaceAdapter;
190
+ navigation: TasksNavAdapter;
191
+ externalTabs: TasksExternalTabsAdapter;
192
+ screencast: TasksScreencastAdapter;
193
+ auth: TasksAuthAdapter;
194
+ };
195
+
196
+ let brandConfig: TasksBrandConfig | null = null;
197
+
198
+ export function configureTasksBrand(config: TasksBrandConfig): void {
199
+ brandConfig = config;
200
+ }
201
+
202
+ export function getTasksBrandConfig(): TasksBrandConfig | null {
203
+ return brandConfig;
204
+ }
205
+
206
+ export function requireTasksBrand(): TasksBrandConfig {
207
+ if (!brandConfig) {
208
+ throw new Error(
209
+ "@creezio/tasks: configureTasksBrand() requis avant d'utiliser le runtime kanban",
210
+ );
211
+ }
212
+ return brandConfig;
213
+ }
214
+
215
+ export function resetTasksBrandForTests(): void {
216
+ brandConfig = null;
217
+ }
218
+
219
+ /** Lit process.env[\`\${envPrefix}_\${suffix}\`]. */
220
+ export function tasksEnv(suffix: string, fallback = ""): string {
221
+ const cfg = brandConfig;
222
+ const prefix = cfg?.envPrefix || "CREEZIO_AI";
223
+ return (process.env[\`\${prefix}_\${suffix}\`] || fallback).trim();
224
+ }
225
+
226
+ export function tasksEnvNumber(suffix: string, fallback: number): number {
227
+ const raw = Number(tasksEnv(suffix, String(fallback)));
228
+ return Number.isFinite(raw) ? raw : fallback;
229
+ }
230
+ `;
231
+
232
+ writeSrc("brand/config.ts", BRAND_CONFIG);
233
+
234
+ // --- kanban-service ---
235
+ {
236
+ let s = readTf("lib/tasks.ts");
237
+ s = s.replace(/^import "@\/lib\/assistant\/configure-brand";\n/, "");
238
+ s = s.replace(
239
+ `import { randomUUID } from "crypto";\nimport {\n hermesCronCreate,\n hermesKanbanConfigured,\n hermesKanbanCreateTask,\n hermesKanbanDispatch,\n hermesKanbanGetTask,\n hermesKanbanListTasks,\n hermesKanbanPatchTask,\n type HermesKanbanStatus,\n type HermesKanbanTask,\n type HermesKanbanTaskDetail,\n} from "@creezio/assistant";\nimport { getWriteDb, queryAll, queryOne, tableExists } from "@/lib/db";\nimport { upsertKitPlatformTask } from "@creezio/tasks";\nimport { getUserById } from "@/lib/users";\n`,
240
+ `import { randomUUID } from "node:crypto";\nimport {\n hermesCronCreate,\n hermesKanbanConfigured,\n hermesKanbanCreateTask,\n hermesKanbanDispatch,\n hermesKanbanGetTask,\n hermesKanbanListTasks,\n hermesKanbanPatchTask,\n type HermesKanbanStatus,\n type HermesKanbanTask,\n type HermesKanbanTaskDetail,\n} from "@creezio/assistant";\nimport { requireTasksBrand, type TasksUser } from "./brand/config.js";\nimport { upsertKitPlatformTask } from "./env-bridge.js";\n\nfunction getWriteDb() {\n return requireTasksBrand().db.getWriteDb();\n}\nfunction queryAll<T>(sql: string, params: unknown[] = []): T[] {\n return requireTasksBrand().db.queryAll<T>(sql, params);\n}\nfunction queryOne<T>(sql: string, params: unknown[] = []): T | null | undefined {\n return requireTasksBrand().db.queryOne<T>(sql, params);\n}\nfunction tableExists(name: string): boolean {\n return requireTasksBrand().db.tableExists(name);\n}\nfunction getUserById(id: string): TasksUser | null {\n return requireTasksBrand().users.getById(id);\n}\n`,
241
+ );
242
+ s = s.replace(
243
+ `export type Task = TaskRow & {\n assignee: ReturnType<typeof getUserById>;\n};`,
244
+ `export type Task = TaskRow & {\n assignee: TasksUser | null;\n};`,
245
+ );
246
+ s = s.replace(
247
+ `"Source: TempoFlow CRM"`,
248
+ `\`Source: \${requireTasksBrand().hermesSourceLabel}\``,
249
+ );
250
+ s = s.replace(
251
+ `idempotencyKey: task.idempotency_key || \`crm:\${task.id}\`,`,
252
+ `idempotencyKey: task.idempotency_key || \`\${requireTasksBrand().idempotencyPrefix}:\${task.id}\`,`,
253
+ );
254
+ s = s.replace(
255
+ `\`Tâche récurrente TempoFlow CRM « \${task.title} ».\,\``,
256
+ `\`Tâche récurrente \${requireTasksBrand().hermesSourceLabel} « \${task.title} ».\,\``,
257
+ );
258
+ // Fix the recurring prompt line carefully
259
+ s = s.replace(
260
+ /Tâche récurrente TempoFlow CRM « \$\{task\.title\} »\./g,
261
+ "Tâche récurrente ${requireTasksBrand().hermesSourceLabel} « ${task.title} ».",
262
+ );
263
+ s = s.replace(
264
+ `"Charge le skill tempoflow2-crm.",`,
265
+ `\`Charge le skill \${requireTasksBrand().hermesSkill}.\`,`,
266
+ );
267
+ s = s.replace(
268
+ `idempotency_key: input.idempotencyKey?.trim() || \`crm:\${id}\`,`,
269
+ `idempotency_key: input.idempotencyKey?.trim() || \`\${requireTasksBrand().idempotencyPrefix}:\${id}\`,`,
270
+ );
271
+ s = s.replace(
272
+ `throw new Error("Table tasks absente (migration 029 requise)");`,
273
+ `throw new Error("Table tasks absente (schéma tasks plateforme requis)");`,
274
+ );
275
+ s = s.replace(
276
+ /migration 029/gi,
277
+ "schéma tasks plateforme",
278
+ );
279
+ writeSrc("kanban-service.ts", s);
280
+ }
281
+
282
+ // --- task-runs ---
283
+ {
284
+ let s = readTf("lib/task-runs.ts");
285
+ s = s.replace(
286
+ `import { randomUUID } from "crypto";\nimport { getWriteDb, queryAll, queryOne, tableExists } from "@/lib/db";\n`,
287
+ `import { randomUUID } from "node:crypto";\nimport { requireTasksBrand, tasksEnv } from "./brand/config.js";\n\nfunction getWriteDb() {\n return requireTasksBrand().db.getWriteDb();\n}\nfunction queryAll<T>(sql: string, params: unknown[] = []): T[] {\n return requireTasksBrand().db.queryAll<T>(sql, params);\n}\nfunction queryOne<T>(sql: string, params: unknown[] = []): T | null | undefined {\n return requireTasksBrand().db.queryOne<T>(sql, params);\n}\nfunction tableExists(name: string): boolean {\n return requireTasksBrand().db.tableExists(name);\n}\n`,
288
+ );
289
+ s = s.replace(/__tf2AgentLogListeners/g, "__creezioTasksAgentLogListeners");
290
+ s = s.replace(/__tf2AgentRunListeners/g, "__creezioTasksAgentRunListeners");
291
+ s = s.replace(
292
+ `const raw = Number(process.env.TF2_AI_MAX_CONCURRENT || "2");`,
293
+ `const raw = Number(tasksEnv("MAX_CONCURRENT", "2") || "2");`,
294
+ );
295
+ s = s.replace(/TF2_AI_MAX_RUNS_PER_DAY/g, "MAX_RUNS_PER_DAY (envPrefix)");
296
+ s = s.replace(/TF2_AI_MAX_TOKENS_PER_DAY/g, "MAX_TOKENS_PER_DAY (envPrefix)");
297
+ s = s.replace(/TF2_AI_MAX_CONCURRENT/g, "MAX_CONCURRENT (envPrefix)");
298
+ s = s.replace(
299
+ /Migration 018 requise/g,
300
+ "schéma task_runs plateforme requis",
301
+ );
302
+ writeSrc("task-runs.ts", s);
303
+ }
304
+
305
+ // --- ai-task-agent ---
306
+ {
307
+ let s = readTf("lib/ai-task-agent.ts");
308
+ s = s.replace(/^import "@\/lib\/assistant\/configure-brand";\n/, "");
309
+ s = s.replace(
310
+ `import {\n listTabsInAiWorkspace,\n navigateAiWorkspace,\n openTabInAiWorkspace,\n webActionInAiWorkspace,\n} from "@/lib/ai-workspace";\nimport { hasPermission, permissionForPath } from "@/lib/nav-config";\nimport {\n resolveOpenTabRequest,\n toSupplierOpenTabParams,\n} from "@/lib/open-external-tab";\nimport { createTask, type Task } from "@/lib/tasks";\nimport type { PublicUser } from "@/lib/users";\n`,
311
+ `import {\n requireTasksBrand,\n tasksEnv,\n tasksEnvNumber,\n type TasksUser,\n} from "./brand/config.js";\nimport { createTask, type Task } from "./kanban-service.js";\n\ntype PublicUser = TasksUser;\n\nfunction navigateAiWorkspace(opts: {\n aiUserId: string;\n hostUserId: string;\n path: string;\n}) {\n return requireTasksBrand().workspace.navigate(opts);\n}\nfunction openTabInAiWorkspace(opts: {\n aiUserId: string;\n hostUserId: string;\n url: string;\n title?: string;\n params?: Record<string, unknown>;\n}) {\n return requireTasksBrand().workspace.openTab(opts);\n}\nfunction listTabsInAiWorkspace(opts: {\n aiUserId: string;\n hostUserId: string;\n}) {\n return requireTasksBrand().workspace.listTabs(opts);\n}\nfunction webActionInAiWorkspace(opts: {\n aiUserId: string;\n hostUserId: string;\n webType: string;\n params: Record<string, unknown>;\n tabId?: string;\n}) {\n return requireTasksBrand().workspace.webAction(opts);\n}\nfunction hasPermission(\n permissions: readonly string[] | undefined,\n required: string | null,\n) {\n return requireTasksBrand().navigation.hasPermission(permissions, required);\n}\nfunction permissionForPath(pathname: string) {\n return requireTasksBrand().navigation.permissionForPath(pathname);\n}\nfunction resolveOpenTabRequest(input: {\n url?: string;\n fournisseur_id?: string | number;\n fournisseurId?: string | number;\n title?: string;\n}) {\n return requireTasksBrand().externalTabs.resolve({\n url: input.url,\n fournisseurId: input.fournisseurId ?? input.fournisseur_id,\n title: input.title,\n });\n}\nfunction toSupplierOpenTabParams(resolved: {\n url: string;\n title?: string;\n params?: Record<string, unknown>;\n}) {\n return requireTasksBrand().externalTabs.toWorkspaceParams(resolved);\n}\n`,
312
+ );
313
+ s = s.replace(
314
+ `return (process.env.TF2_AI_MODEL || "").trim() || defaultModel();`,
315
+ `return tasksEnv("MODEL") || defaultModel();`,
316
+ );
317
+ s = s.replace(
318
+ `const raw = Number(process.env.TF2_AI_MAX_STEPS || "20");`,
319
+ `const raw = tasksEnvNumber("MAX_STEPS", 20);`,
320
+ );
321
+ s = s.replace(
322
+ `const raw = Number(process.env.TF2_AI_RUN_TIMEOUT_MS || String(10 * 60 * 1000));`,
323
+ `const raw = tasksEnvNumber("RUN_TIMEOUT_MS", 10 * 60 * 1000);`,
324
+ );
325
+ s = s.replace(
326
+ `const raw = Number(process.env.TF2_AI_MAX_TOKENS || "150000");`,
327
+ `const raw = tasksEnvNumber("MAX_TOKENS", 150_000);`,
328
+ );
329
+ s = s.replace(
330
+ `const raw = (process.env.TF2_AI_WEB_ALLOWED_HOSTS || "").trim();`,
331
+ `const raw = tasksEnv("WEB_ALLOWED_HOSTS");`,
332
+ );
333
+ s = s.replace(
334
+ /TF2_AI_WEB_ALLOWED_HOSTS/g,
335
+ "${requireTasksBrand().envPrefix}_WEB_ALLOWED_HOSTS",
336
+ );
337
+ // Fix the template string that now has wrong quotes after replace
338
+ s = s.replace(
339
+ `error: \`Hôte « \${host} » hors de l'allowlist \${requireTasksBrand().envPrefix}_WEB_ALLOWED_HOSTS — demande à l'humain si nécessaire\`,`,
340
+ `error: \`Hôte « \${host} » hors de l'allowlist \${requireTasksBrand().envPrefix}_WEB_ALLOWED_HOSTS — demande à l'humain si nécessaire\`,`,
341
+ );
342
+ s = s.replace(
343
+ `return \`Tu es « \${ctx.assignee.username} », collaborateur IA de TempoFlow (CRM achats restauration).`,
344
+ `const brand = requireTasksBrand();\n return \`Tu es « \${ctx.assignee.username} », collaborateur IA de \${brand.productName} (\${brand.productDomain}).`,
345
+ );
346
+ s = s.replace(
347
+ `- Kanban des tâches : /taches`,
348
+ `- Kanban des tâches : \${brand.taskHref}`,
349
+ );
350
+ // Fix example paths if present later
351
+ s = s.replace(
352
+ /\/produits|\/marketplaces|\/panier/g,
353
+ (m) => m, // leave for now; brand prompt uses examplePaths below if we inject
354
+ );
355
+ writeSrc("ai-task-agent.ts", s);
356
+ }
357
+
358
+ // --- ai-task-runner ---
359
+ {
360
+ let s = readTf("lib/ai-task-runner.ts");
361
+ s = s.replace(
362
+ `import {\n hasAiAgentModel,\n runAiTaskAgent,\n} from "@/lib/ai-task-agent";\nimport { ensureAiWorkspaceOnHost } from "@/lib/ai-workspace";\nimport {\n isDesktopOnline,\n listOnlineBridges,\n} from "@/lib/desktop-presence";\nimport {\n getTask,\n listRecurringAiTasks,\n setTaskNextRun,\n updateTaskLocal,\n type Task,\n} from "@/lib/tasks";\nimport {\n appendAgentLog,\n claimNextQueuedRun,\n clearHitlResponse,\n countRunsCreatedToday,\n enqueueTaskRun,\n finishTaskRun,\n getActiveRunForAssignee,\n getTaskRun,\n isHitlPaused,\n listRunningRuns,\n maxConcurrentAiRuns,\n setHitlPrompt,\n sumUsageTokensToday,\n type TaskRunRow,\n} from "@/lib/task-runs";\nimport { getOwner, getUserById } from "@/lib/users";\n`,
363
+ `import {\n hasAiAgentModel,\n runAiTaskAgent,\n} from "./ai-task-agent.js";\nimport { requireTasksBrand, tasksEnv } from "./brand/config.js";\nimport {\n getTask,\n listRecurringAiTasks,\n setTaskNextRun,\n updateTaskLocal,\n type Task,\n} from "./kanban-service.js";\nimport {\n appendAgentLog,\n claimNextQueuedRun,\n clearHitlResponse,\n countRunsCreatedToday,\n enqueueTaskRun,\n finishTaskRun,\n getActiveRunForAssignee,\n getTaskRun,\n isHitlPaused,\n listRunningRuns,\n maxConcurrentAiRuns,\n setHitlPrompt,\n sumUsageTokensToday,\n type TaskRunRow,\n} from "./task-runs.js";\n\nfunction ensureAiWorkspaceOnHost(opts: {\n aiUserId: string;\n hostUserId: string;\n}) {\n return requireTasksBrand().workspace.ensureOnHost(opts);\n}\nfunction isDesktopOnline(userId: string) {\n return requireTasksBrand().presence.isDesktopOnline(userId);\n}\nfunction listOnlineBridges() {\n return requireTasksBrand().presence.listOnlineBridges();\n}\nfunction getOwner() {\n return requireTasksBrand().users.getOwner();\n}\nfunction getUserById(id: string) {\n return requireTasksBrand().users.getById(id);\n}\n`,
364
+ );
365
+ s = s.replace(/__tf2AiRunnerActive/g, "__creezioTasksAiRunnerActive");
366
+ s = s.replace(/__tf2AiRunnerTimer/g, "__creezioTasksAiRunnerTimer");
367
+ s = s.replace(/__tf2AiRecurrenceTimer/g, "__creezioTasksAiRecurrenceTimer");
368
+ s = s.replace(/__tf2AiRunnerInflight/g, "__creezioTasksAiRunnerInflight");
369
+ s = s.replace(
370
+ `const v = (process.env.TF2_AI_RUNNER_ENABLED || "1").toLowerCase();`,
371
+ `const v = (tasksEnv("RUNNER_ENABLED", "1") || "1").toLowerCase();`,
372
+ );
373
+ s = s.replace(
374
+ `function envQuota(name: string): number | null {\n const raw = Number((process.env[name] || "").trim());\n return Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : null;\n}`,
375
+ `function envQuota(suffix: string): number | null {\n const raw = Number(tasksEnv(suffix));\n return Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : null;\n}`,
376
+ );
377
+ s = s.replace(
378
+ `const maxRuns = envQuota("TF2_AI_MAX_RUNS_PER_DAY");`,
379
+ `const maxRuns = envQuota("MAX_RUNS_PER_DAY");`,
380
+ );
381
+ s = s.replace(
382
+ /TF2_AI_MAX_RUNS_PER_DAY/g,
383
+ "${requireTasksBrand().envPrefix}_MAX_RUNS_PER_DAY",
384
+ );
385
+ s = s.replace(
386
+ `const maxTokens = envQuota("TF2_AI_MAX_TOKENS_PER_DAY");`,
387
+ `const maxTokens = envQuota("MAX_TOKENS_PER_DAY");`,
388
+ );
389
+ s = s.replace(
390
+ /TF2_AI_MAX_TOKENS_PER_DAY/g,
391
+ "${requireTasksBrand().envPrefix}_MAX_TOKENS_PER_DAY",
392
+ );
393
+ s = s.replace(
394
+ `const env = (process.env.TF2_AI_HITL || "").toLowerCase();`,
395
+ `const env = tasksEnv("HITL").toLowerCase();`,
396
+ );
397
+ s = s.replace(/TF2_AI_HITL/g, "HITL (envPrefix)");
398
+ s = s.replace(
399
+ /Configuration → Clés IA/g,
400
+ "Configuration → Clés IA",
401
+ );
402
+ writeSrc("ai-task-runner.ts", s);
403
+ }
404
+
405
+ console.log("core service files ported; routes/adapter handled separately");