@m6d/cortex-cli 0.19.0 → 0.20.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.
package/README.md CHANGED
@@ -38,7 +38,7 @@ Scaffolds `./<name>`: a backend for Cortex agents, and Cortex itself to run them
38
38
  - `bun run dev` starts Cortex (`docker compose up`), the backend and `bun cortex setup --wait --yes` in
39
39
  an mprocs dashboard. `bun run dev:agent` runs the same through `concurrently`, one labelled log for
40
40
  coding agents and CI.
41
- - eslint, prettier, cspell, fallow and tsconfig are inlined, so `bun run verify` works standalone.
41
+ - oxlint, prettier, cspell, fallow and tsconfig are inlined, so `bun run verify` works standalone.
42
42
 
43
43
  It then runs `bun install`, and `git init` unless the target is already inside a work tree. An
44
44
  existing non-empty directory is an error. So is a name like `cortex` or `cortex.`, since the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@m6d/cortex-cli",
3
- "version": "0.19.0",
3
+ "version": "0.20.0",
4
4
  "description": "Scaffold Cortex projects and manage a Cortex deployment",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -29,8 +29,8 @@
29
29
  },
30
30
  "devDependencies": {
31
31
  "@cortex/contracts": "workspace:*",
32
- "@types/bun": "1.3.14",
33
- "typescript": "~6.0.3"
32
+ "@types/bun": "1.4.4",
33
+ "typescript": "npm:@typescript/typescript6@^6.0.2"
34
34
  },
35
35
  "publishConfig": {
36
36
  "access": "public"
@@ -1030,7 +1030,7 @@ class Parser {
1030
1030
  }
1031
1031
 
1032
1032
  /** `a + b * (c - 1)`: `*` and `/` bind before `+` and `-`; a date plus or minus an INTERVAL folds to the date it names. */
1033
- // eslint-disable-next-line m6d/no-explicit-return-type -- mutually recursive with factor and unit
1033
+ // oxlint-disable-next-line m6d/no-explicit-return-type -- mutually recursive with factor and unit
1034
1034
  private term(message: string): Term {
1035
1035
  let left = this.factor(message);
1036
1036
  while (this.peekPunct("+") || this.peekPunct("-")) {
@@ -1,6 +1,7 @@
1
1
  import { z } from "zod";
2
2
 
3
3
  import { isRecord } from "../json/index";
4
+ import { TIMEZONE_HEADER } from "../wire/index";
4
5
  import { managementProcedureKinds } from "./procedures.generated";
5
6
  import type { ManagementProcedures } from "./procedures.generated";
6
7
 
@@ -75,10 +76,19 @@ export class ManagementClientError extends Error {
75
76
  }
76
77
  }
77
78
 
78
- /** Where management calls go: a console's origin and a management key (`cortex_mgmt_…`); a test hands in its own `fetch`. */
79
+ /**
80
+ * Where management calls go: a console's origin and a management key
81
+ * (`cortex_mgmt_…`); a test hands in its own `fetch`. `locale` (`ar`) and
82
+ * `timeZone` (an IANA zone, `Asia/Dubai`) are the caller's, sent as
83
+ * `Accept-Language` and `X-Cortex-Timezone`: Cortex words its refusals in
84
+ * the one and reads a day range in the other, else English and UTC days. A
85
+ * host acting for many users builds a client per request.
86
+ */
79
87
  export type ManagementTarget = {
80
88
  url: string;
81
89
  key: string;
90
+ locale?: string;
91
+ timeZone?: string;
82
92
  fetch?: (url: string, init: RequestInit) => Promise<Response>;
83
93
  };
84
94
 
@@ -100,6 +110,8 @@ export async function callManagement(
100
110
  headers: {
101
111
  Authorization: `Bearer ${target.key}`,
102
112
  ...(method === "post" ? { "Content-Type": "application/json" } : {}),
113
+ ...(target.locale ? { "Accept-Language": target.locale } : {}),
114
+ ...(target.timeZone ? { [TIMEZONE_HEADER]: target.timeZone } : {}),
103
115
  },
104
116
  body: method === "post" && input !== undefined ? JSON.stringify(input) : undefined,
105
117
  signal,
@@ -206,10 +206,13 @@ export type StoredSchemaFieldRow = {
206
206
 
207
207
  /** Whether each procedure is a query (GET) or a mutation (POST). */
208
208
  export const managementProcedureKinds = {
209
+ "agents.addAlias": "mutation",
209
210
  "agents.create": "mutation",
210
211
  "agents.delete": "mutation",
212
+ "agents.deleteAlias": "mutation",
211
213
  "agents.get": "query",
212
214
  "agents.list": "query",
215
+ "agents.listAliases": "query",
213
216
  "agents.options": "query",
214
217
  "agents.publishPrompt": "mutation",
215
218
  "agents.rollbackPrompt": "mutation",
@@ -298,12 +301,9 @@ export const managementProcedureKinds = {
298
301
  "secrets.set": "mutation",
299
302
  "servers.create": "mutation",
300
303
  "servers.delete": "mutation",
301
- "servers.deleteAlias": "mutation",
302
304
  "servers.get": "query",
303
305
  "servers.list": "query",
304
- "servers.listAliases": "query",
305
306
  "servers.options": "query",
306
- "servers.setAlias": "mutation",
307
307
  "servers.sync": "mutation",
308
308
  "servers.update": "mutation",
309
309
  "services.create": "mutation",
@@ -379,12 +379,25 @@ export const managementProcedureKinds = {
379
379
 
380
380
  /** Each procedure's input, as it is sent, and output, as JSON carries it. */
381
381
  export type ManagementProcedures = {
382
- /** Create an agent on a registered server. The draft has no prompt yet; save one with agents.savePrompt before publishing. */
382
+ /** Give an agent an alias: a second slug it answers to wherever a slug is resolved. Refused (slug_taken) when an agent's slug or another alias holds the name; to move an alias, delete it and add it to the other agent. */
383
+ "agents.addAlias": {
384
+ input: {
385
+ alias: string;
386
+ agentId: string;
387
+ };
388
+ output: {
389
+ alias: string;
390
+ agentId: string;
391
+ createdAt: string;
392
+ updatedAt: string;
393
+ };
394
+ };
395
+ /** Create an agent on a registered server, or on none (`serverId` null), fixed for good either way. An agent with no server has no native tools, no server variables and no server auth, and publishes only on a chat model of its own. The draft has no prompt yet; save one with agents.savePrompt before publishing. */
383
396
  "agents.create": {
384
397
  input: {
385
398
  slug: string;
386
399
  name: string;
387
- serverId: string;
400
+ serverId: string | null;
388
401
  };
389
402
  output: {
390
403
  id: string;
@@ -393,7 +406,7 @@ export type ManagementProcedures = {
393
406
  overridable: Array<string>;
394
407
  applied: string | number | boolean | null | Record<string, unknown> | Array<unknown> | null;
395
408
  slug: string;
396
- serverId: string;
409
+ serverId: string | null;
397
410
  name: string;
398
411
  promptDraft: string;
399
412
  ragEnabled: boolean;
@@ -438,7 +451,7 @@ export type ManagementProcedures = {
438
451
  overridable: Array<string>;
439
452
  applied: string | number | boolean | null | Record<string, unknown> | Array<unknown> | null;
440
453
  slug: string;
441
- serverId: string;
454
+ serverId: string | null;
442
455
  name: string;
443
456
  promptDraft: string;
444
457
  ragEnabled: boolean;
@@ -470,6 +483,18 @@ export type ManagementProcedures = {
470
483
  updatedAt: string;
471
484
  };
472
485
  };
486
+ /** Remove an alias. */
487
+ "agents.deleteAlias": {
488
+ input: {
489
+ alias: string;
490
+ };
491
+ output: {
492
+ alias: string;
493
+ agentId: string;
494
+ createdAt: string;
495
+ updatedAt: string;
496
+ };
497
+ };
473
498
  /** One agent by id, including its draft prompt and settings. */
474
499
  "agents.get": {
475
500
  input: {
@@ -482,7 +507,7 @@ export type ManagementProcedures = {
482
507
  overridable: Array<string>;
483
508
  applied: string | number | boolean | null | Record<string, unknown> | Array<unknown> | null;
484
509
  slug: string;
485
- serverId: string;
510
+ serverId: string | null;
486
511
  name: string;
487
512
  promptDraft: string;
488
513
  ragEnabled: boolean;
@@ -510,7 +535,7 @@ export type ManagementProcedures = {
510
535
  summarizationModel: string | null;
511
536
  context: string | number | boolean | null | Record<string, unknown> | Array<unknown>;
512
537
  limits: string | number | boolean | null | Record<string, unknown> | Array<unknown>;
513
- serverName: string;
538
+ serverName: string | null;
514
539
  serverVerifies: boolean;
515
540
  variables: Array<{
516
541
  name: string;
@@ -547,7 +572,7 @@ export type ManagementProcedures = {
547
572
  source: "direct" | "manifest" | "server";
548
573
  sourceId: string | null;
549
574
  overridable: Array<string>;
550
- serverName: string;
575
+ serverName: string | null;
551
576
  published: boolean;
552
577
  toolCount: number;
553
578
  serviceCount: number;
@@ -560,6 +585,27 @@ export type ManagementProcedures = {
560
585
  filteredCount: number;
561
586
  };
562
587
  };
588
+ /** List an agent's aliases (second slugs, unique across the deployment beside every agent's own), with pagination. */
589
+ "agents.listAliases": {
590
+ input: {
591
+ pageIndex?: number;
592
+ pageSize?: number;
593
+ q?: string;
594
+ agentId: string;
595
+ };
596
+ output: {
597
+ items: Array<{
598
+ alias: string;
599
+ agentId: string;
600
+ createdAt: string;
601
+ updatedAt: string;
602
+ }>;
603
+ /** Every row the caller may see, ignoring the search and filters. */
604
+ count: number;
605
+ /** The rows matching the search and filters, over all pages: the total to report. */
606
+ filteredCount: number;
607
+ };
608
+ };
563
609
  /** Agent id, slug, and name pairs for selectors. */
564
610
  "agents.options": {
565
611
  input: undefined;
@@ -567,7 +613,7 @@ export type ManagementProcedures = {
567
613
  id: string;
568
614
  slug: string;
569
615
  name: string;
570
- serverId: string;
616
+ serverId: string | null;
571
617
  }>;
572
618
  };
573
619
  /** Publish the agent's current draft prompt and settings so Cortex serves them. Refused as `auth_required` while the agent's end-user authentication is unchosen, and as `chat_model_required` when neither the agent nor its server picks a chat model. `warnings` says what the publish went ahead despite: published tools the prompt names without an @-mention (neither pinned nor mentioned by a skill it mentions), which the agent usually cannot see. */
@@ -621,7 +667,7 @@ export type ManagementProcedures = {
621
667
  overridable: Array<string>;
622
668
  applied: string | number | boolean | null | Record<string, unknown> | Array<unknown> | null;
623
669
  slug: string;
624
- serverId: string;
670
+ serverId: string | null;
625
671
  name: string;
626
672
  promptDraft: string;
627
673
  ragEnabled: boolean;
@@ -731,7 +777,7 @@ export type ManagementProcedures = {
731
777
  overridable: Array<string>;
732
778
  applied: string | number | boolean | null | Record<string, unknown> | Array<unknown> | null;
733
779
  slug: string;
734
- serverId: string;
780
+ serverId: string | null;
735
781
  name: string;
736
782
  promptDraft: string;
737
783
  ragEnabled: boolean;
@@ -776,7 +822,7 @@ export type ManagementProcedures = {
776
822
  overridable: Array<string>;
777
823
  applied: string | number | boolean | null | Record<string, unknown> | Array<unknown> | null;
778
824
  slug: string;
779
- serverId: string;
825
+ serverId: string | null;
780
826
  name: string;
781
827
  promptDraft: string;
782
828
  ragEnabled: boolean;
@@ -1633,7 +1679,7 @@ export type ManagementProcedures = {
1633
1679
  agents?: Array<{
1634
1680
  slug: string;
1635
1681
  name: string;
1636
- server: string;
1682
+ server?: string;
1637
1683
  prompt: string;
1638
1684
  auth: {
1639
1685
  kind: "jwks";
@@ -2158,7 +2204,7 @@ export type ManagementProcedures = {
2158
2204
  agents?: Array<{
2159
2205
  slug: string;
2160
2206
  name: string;
2161
- server: string;
2207
+ server?: string;
2162
2208
  prompt: string;
2163
2209
  auth: {
2164
2210
  kind: "jwks";
@@ -3117,7 +3163,7 @@ export type ManagementProcedures = {
3117
3163
  outcome: "started" | "awaiting_approval" | "completed" | "failed" | "rejected" | "dropped";
3118
3164
  reason: "busy" | "approval_pending" | "budget" | "rate_limited" | "invalid_payload" | "missing_thread_key" | "no_turn" | "no_output" | "invalid_output" | null;
3119
3165
  output: unknown;
3120
- serverName: string;
3166
+ serverName: string | null;
3121
3167
  agentSlug: string;
3122
3168
  agentName: string;
3123
3169
  threadTitle: string | null;
@@ -3153,8 +3199,8 @@ export type ManagementProcedures = {
3153
3199
  activeUsers: number;
3154
3200
  };
3155
3201
  perUser: Array<{
3156
- serverId: string;
3157
- serverName: string;
3202
+ scopeId: string;
3203
+ scopeName: string;
3158
3204
  userId: string;
3159
3205
  triggerName: string | null;
3160
3206
  threads: number;
@@ -3166,8 +3212,8 @@ export type ManagementProcedures = {
3166
3212
  messages: number;
3167
3213
  }>;
3168
3214
  perServer: Array<{
3169
- serverId: string;
3170
- serverName: string;
3215
+ serverId: string | null;
3216
+ serverName: string | null;
3171
3217
  totalTokens: number;
3172
3218
  }>;
3173
3219
  perAgent: Array<{
@@ -3205,7 +3251,7 @@ export type ManagementProcedures = {
3205
3251
  detail: string | null;
3206
3252
  threadId: string;
3207
3253
  createdAt: string;
3208
- serverName: string;
3254
+ serverName: string | null;
3209
3255
  agentSlug: string;
3210
3256
  agentName: string;
3211
3257
  threadTitle: string | null;
@@ -3387,7 +3433,7 @@ export type ManagementProcedures = {
3387
3433
  outcome: "started" | "awaiting_approval" | "completed" | "failed" | "rejected" | "dropped";
3388
3434
  reason: "busy" | "approval_pending" | "budget" | "rate_limited" | "invalid_payload" | "missing_thread_key" | "no_turn" | "no_output" | "invalid_output" | null;
3389
3435
  output: unknown;
3390
- serverName: string;
3436
+ serverName: string | null;
3391
3437
  agentSlug: string;
3392
3438
  agentName: string;
3393
3439
  threadTitle: string | null;
@@ -3702,18 +3748,6 @@ export type ManagementProcedures = {
3702
3748
  updatedAt: string;
3703
3749
  };
3704
3750
  };
3705
- /** Remove an alias. */
3706
- "servers.deleteAlias": {
3707
- input: {
3708
- alias: string;
3709
- };
3710
- output: {
3711
- alias: string;
3712
- agentId: string;
3713
- createdAt: string;
3714
- updatedAt: string;
3715
- };
3716
- };
3717
3751
  /** One server by id with its declared variables and the components whose audience is the server. */
3718
3752
  "servers.get": {
3719
3753
  input: {
@@ -3776,27 +3810,6 @@ export type ManagementProcedures = {
3776
3810
  filteredCount: number;
3777
3811
  };
3778
3812
  };
3779
- /** List the aliases (second slugs, unique across the deployment beside every agent's own) of a server's agents, with pagination. */
3780
- "servers.listAliases": {
3781
- input: {
3782
- pageIndex?: number;
3783
- pageSize?: number;
3784
- q?: string;
3785
- serverId: string;
3786
- };
3787
- output: {
3788
- items: Array<{
3789
- alias: string;
3790
- agentId: string;
3791
- agentName: string;
3792
- agentSlug: string;
3793
- }>;
3794
- /** Every row the caller may see, ignoring the search and filters. */
3795
- count: number;
3796
- /** The rows matching the search and filters, over all pages: the total to report. */
3797
- filteredCount: number;
3798
- };
3799
- };
3800
3813
  /** Server id and name pairs for selectors. Cortex's own system server is left out unless `includeSystem` asks for it, as a filter over its threads does. */
3801
3814
  "servers.options": {
3802
3815
  input: {
@@ -3807,17 +3820,6 @@ export type ManagementProcedures = {
3807
3820
  name: string;
3808
3821
  }>;
3809
3822
  };
3810
- /** Point an alias at an agent, creating it or moving it among the agents of one server. Refused (slug_taken) when an agent goes by the name or it is an alias of another server's agent. */
3811
- "servers.setAlias": {
3812
- input: {
3813
- alias: string;
3814
- agentId: string;
3815
- };
3816
- output: {
3817
- alias: string;
3818
- agentId: string;
3819
- };
3820
- };
3821
3823
  /** Read the server's manifest now and apply its variables and native tools; a refused manifest leaves the previous one in place. */
3822
3824
  "servers.sync": {
3823
3825
  input: {
@@ -4361,8 +4363,8 @@ export type ManagementProcedures = {
4361
4363
  triggerName: string | null;
4362
4364
  createdAt: string;
4363
4365
  updatedAt: string;
4364
- serverId: string;
4365
- serverName: string;
4366
+ serverId: string | null;
4367
+ serverName: string | null;
4366
4368
  agentSlug: string;
4367
4369
  agentId: string;
4368
4370
  agentName: string;
@@ -4400,8 +4402,8 @@ export type ManagementProcedures = {
4400
4402
  triggerName: string | null;
4401
4403
  createdAt: string;
4402
4404
  updatedAt: string;
4403
- serverId: string;
4404
- serverName: string;
4405
+ serverId: string | null;
4406
+ serverName: string | null;
4405
4407
  agentSlug: string;
4406
4408
  }>;
4407
4409
  /** Every row the caller may see, ignoring the search and filters. */
@@ -1500,55 +1500,77 @@ export const agentSettingsSchema = z.strictObject({
1500
1500
  /** An agent's name in characters. The console caps its editor at the same. */
1501
1501
  export const AGENT_LIMITS = { nameLength: 120 } as const;
1502
1502
 
1503
- const manifestAgentSchema = z.strictObject({
1504
- slug: agentSlugSchema,
1505
- name: z.string().trim().min(1).max(AGENT_LIMITS.nameLength),
1506
- /** The server it belongs to, by name; a manifest never creates one. */
1507
- server: nameSchema,
1508
- /** Rich text: it may mention tools and skills, and read `{{variables}}`. */
1509
- prompt: z.string().trim().min(1),
1510
- auth: agentAuthSchema,
1511
- /**
1512
- * `chat` left out runs on the server's pick; `vision` and
1513
- * `summarization` left out use none of their own.
1514
- */
1515
- models: z
1516
- .strictObject({
1517
- chat: modelSchema.optional(),
1518
- vision: modelSchema.omit({ effort: true }).optional(),
1519
- summarization: modelSchema.omit({ effort: true }).optional(),
1520
- })
1521
- .default({}),
1522
- settings: agentSettingsSchema.prefault({}),
1523
- /** Messages a minute and model tokens a UTC day, for all users together and for each; null, no limit. */
1524
- rateLimits: z
1525
- .strictObject({
1526
- allUsers: z
1527
- .strictObject({
1528
- messagesPerMinute: rateLimit(agentRateLimits.messagesPerMinute, "allUsers"),
1529
- tokensPerDay: rateLimit(agentRateLimits.tokensPerDay, "allUsers"),
1530
- })
1531
- .prefault({}),
1532
- perUser: z
1533
- .strictObject({
1534
- messagesPerMinute: rateLimit(agentRateLimits.messagesPerMinute, "perUser"),
1535
- tokensPerDay: rateLimit(agentRateLimits.tokensPerDay, "perUser"),
1536
- })
1537
- .prefault({}),
1538
- })
1539
- .prefault({}),
1540
- suggestions: suggestionsSchema.default(() => byLocale(() => [])),
1541
- greeting: greetingSchema.default(() => byLocale(() => "")),
1542
- overridable: overridable([
1543
- "name",
1544
- "prompt",
1545
- "models",
1546
- "settings",
1547
- "rateLimits",
1548
- "suggestions",
1549
- "greeting",
1550
- ]),
1551
- });
1503
+ const manifestAgentSchema = z
1504
+ .strictObject({
1505
+ slug: agentSlugSchema,
1506
+ name: z.string().trim().min(1).max(AGENT_LIMITS.nameLength),
1507
+ /**
1508
+ * The server it belongs to, by name; a manifest never creates one. Left
1509
+ * out, the agent has no server: no native tools, no server variables,
1510
+ * no `server` auth, and no model to inherit, so it names its own `chat`.
1511
+ */
1512
+ server: nameSchema.optional(),
1513
+ /** Rich text: it may mention tools and skills, and read `{{variables}}`. */
1514
+ prompt: z.string().trim().min(1),
1515
+ auth: agentAuthSchema,
1516
+ /**
1517
+ * `chat` left out runs on the server's pick; `vision` and
1518
+ * `summarization` left out use none of their own.
1519
+ */
1520
+ models: z
1521
+ .strictObject({
1522
+ chat: modelSchema.optional(),
1523
+ vision: modelSchema.omit({ effort: true }).optional(),
1524
+ summarization: modelSchema.omit({ effort: true }).optional(),
1525
+ })
1526
+ .default({}),
1527
+ settings: agentSettingsSchema.prefault({}),
1528
+ /** Messages a minute and model tokens a UTC day, for all users together and for each; null, no limit. */
1529
+ rateLimits: z
1530
+ .strictObject({
1531
+ allUsers: z
1532
+ .strictObject({
1533
+ messagesPerMinute: rateLimit(agentRateLimits.messagesPerMinute, "allUsers"),
1534
+ tokensPerDay: rateLimit(agentRateLimits.tokensPerDay, "allUsers"),
1535
+ })
1536
+ .prefault({}),
1537
+ perUser: z
1538
+ .strictObject({
1539
+ messagesPerMinute: rateLimit(agentRateLimits.messagesPerMinute, "perUser"),
1540
+ tokensPerDay: rateLimit(agentRateLimits.tokensPerDay, "perUser"),
1541
+ })
1542
+ .prefault({}),
1543
+ })
1544
+ .prefault({}),
1545
+ suggestions: suggestionsSchema.default(() => byLocale(() => [])),
1546
+ greeting: greetingSchema.default(() => byLocale(() => "")),
1547
+ overridable: overridable([
1548
+ "name",
1549
+ "prompt",
1550
+ "models",
1551
+ "settings",
1552
+ "rateLimits",
1553
+ "suggestions",
1554
+ "greeting",
1555
+ ]),
1556
+ })
1557
+ .superRefine((agent, context) => {
1558
+ if (agent.server !== undefined) return;
1559
+ if (agent.auth.kind === "server") {
1560
+ context.addIssue({
1561
+ code: "custom",
1562
+ path: ["auth"],
1563
+ message: "server auth needs a server",
1564
+ });
1565
+ }
1566
+ if (agent.models.chat === undefined) {
1567
+ context.addIssue({
1568
+ code: "custom",
1569
+ path: ["models", "chat"],
1570
+ message: "an agent with no server names its own chat model",
1571
+ });
1572
+ }
1573
+ });
1552
1574
  export type ManifestAgent = z.input<typeof manifestAgentSchema>;
1553
1575
 
1554
1576
  /**
@@ -166,6 +166,8 @@ export function inputSchemaOf(operation: OpenApiOperation) {
166
166
  export function managementTarget(profile: { url: string; key: string }) {
167
167
  return {
168
168
  ...profile,
169
+ // Every day range a command takes is this machine's days.
170
+ timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
169
171
  fetch: (url: string, init: RequestInit) =>
170
172
  fetch(url, init).catch((error: unknown) => {
171
173
  throw new UnreachableError(error);
@@ -2,9 +2,10 @@
2
2
  * Every file `cortex new` writes, as one pure function. Files with nothing to
3
3
  * fill in are verbatim text under templates/; the ones that vary with the
4
4
  * project are assembled here. The tooling configs are inlined rather than
5
- * pulled from `@m6d/eslint-config`, `@m6d/prettier-config` and `@m6d/tsconfig`:
6
- * those are `private: true` at `0.0.0` and resolvable only through this
7
- * workspace, and a standalone scaffold has no workspace.
5
+ * pulled from `@m6d/prettier-config`, `@m6d/tsconfig` and the root
6
+ * `.oxlintrc.json`: those are `private: true` at `0.0.0` (the lint config
7
+ * loads `@m6d/oxlint-plugin`) and resolvable only through this workspace, and
8
+ * a standalone scaffold has no workspace.
8
9
  */
9
10
 
10
11
  import { NAME_MAX_LENGTH } from "@cortex/contracts/manifest";
@@ -16,11 +17,11 @@ import datasetsTs from "@/scaffold/templates/datasets.ts.txt";
16
17
  import tsconfigJson from "@/scaffold/templates/tsconfig.json.txt";
17
18
  import gitignoreText from "@/scaffold/templates/gitignore.txt";
18
19
  import dockerignoreText from "@/scaffold/templates/dockerignore.txt";
19
- import eslintConfig from "@/scaffold/templates/eslint.config.mjs.txt";
20
+ import oxlintrc from "@/scaffold/templates/oxlintrc.json.txt";
20
21
  import prettierConfig from "@/scaffold/templates/prettier.config.mjs.txt";
21
22
  import fallowrc from "@/scaffold/templates/fallowrc.jsonc.txt";
22
- // Project jargon is the project's to add. Only `mprocs`, which its own
23
- // package.json names, is seeded: a stranger's vocabulary is noise in a repo.
23
+ // Project jargon is the project's to add. Only the tool names its own
24
+ // package.json carries are seeded: a stranger's vocabulary is noise in a repo.
24
25
  import cspellJson from "@/scaffold/templates/cspell.json.txt";
25
26
  import { applyPorts, type Ports } from "@/scaffold/ports";
26
27
 
@@ -29,7 +30,7 @@ import { applyPorts, type Ports } from "@/scaffold/ports";
29
30
  * `@m6d/cortex` range below are stamped by scripts/sync-package-versions.ts
30
31
  * before the CLI publishes, so a scaffold pins what was current then.
31
32
  */
32
- const CORTEX_VERSION = "0.18.0";
33
+ const CORTEX_VERSION = "0.19.0";
33
34
 
34
35
  /** This machine as Cortex's containers name it: the one private host the stack's egress policy admits. */
35
36
  export const DOCKER_HOST = "host.docker.internal";
@@ -54,18 +55,16 @@ export const EMBEDDING = { model: "text-embedding-3-small", dimension: 768 } as
54
55
  const DEV_DEPENDENCIES = {
55
56
  // `cortex apply`, `cortex setup` and the management commands, at the release that wrote the project.
56
57
  "@m6d/cortex-cli": `~${pkg.version}`,
57
- "@types/bun": "1.3.14",
58
+ "@types/bun": "1.4.4",
58
59
  // The two runners of `bun run dev` and `bun run dev:agent`.
59
60
  concurrently: "^10.0.6",
60
61
  cspell: "^9.6.4",
61
- eslint: "^9",
62
- "eslint-config-prettier": "^10.1.8",
63
- "eslint-plugin-prettier": "^5.5.5",
64
62
  fallow: "^3.9.1",
65
63
  mprocs: "^0.9.6",
64
+ oxlint: "^1.90.0",
65
+ "oxlint-tsgolint": "^7.0.2005",
66
66
  prettier: "^3.8.1",
67
- typescript: "~6.0.3",
68
- "typescript-eslint": "^8.65.0",
67
+ typescript: "^7.0.2",
69
68
  };
70
69
 
71
70
  /**
@@ -145,12 +144,12 @@ function packageJson({ name, datasets, setup }: Project) {
145
144
  postinstall: "bun run --if-present setup.ts",
146
145
  format: "prettier --write .",
147
146
  "format:check": "prettier --check .",
148
- lint: "eslint",
147
+ lint: "oxlint",
149
148
  spellcheck: 'cspell "**/*.{ts,mjs,json,md}"',
150
149
  verify: "bun run format:check && bun run lint && bun run spellcheck && bun run check && bun run audit",
151
150
  },
152
151
  dependencies: {
153
- "@m6d/cortex": "~0.6.0",
152
+ "@m6d/cortex": "~0.7.0",
154
153
  // drizzle 1.0 is in beta; this is the build `@m6d/cortex/drizzle` is tested against.
155
154
  ...(datasets ? { "drizzle-orm": "1.0.0-beta.15-859cf75" } : {}),
156
155
  hono: "^4.12.3",
@@ -249,9 +248,11 @@ COMPOSE_PROJECT_NAME=
249
248
  DEFAULT_ADMIN_EMAIL=admin@example.com
250
249
  DEFAULT_ADMIN_PASSWORD=
251
250
  BOOTSTRAP_MANAGEMENT_KEY=
252
- # Session signing, and the key the secrets store is encrypted with.
251
+ # Session signing, the key the secrets store is encrypted with, and object
252
+ # storage's password (storage is published on this machine for uploads).
253
253
  BETTER_AUTH_SECRET=
254
254
  SECRETS_MASTER_KEY=
255
+ S3_SECRET_ACCESS_KEY=
255
256
  # Any OpenAI-compatible embeddings endpoint and model. Tool search and
256
257
  # knowledge collections embed with it, and stay off while the key is blank.
257
258
  EMBEDDING_BASE_URL=https://api.openai.com/v1
@@ -288,17 +289,14 @@ MODEL_PROVIDER_MODEL=
288
289
  function dockerCompose() {
289
290
  return `# Cortex, for this project's agents to run on: the console at
290
291
  # http://localhost:{{port.cortex}}, the engine widgets and trigger callers reach at
291
- # http://localhost:{{port.engine}}, and the stores they keep state in. \`bun run dev\`
292
- # starts it; .env holds its secrets.
292
+ # http://localhost:{{port.engine}}, storage the console uploads to at
293
+ # http://localhost:{{port.storage}}, and the stores they keep state in.
294
+ # \`bun run dev\` starts it; .env holds its secrets.
293
295
  #
294
296
  # The host ports were picked at scaffold time from what was free on this
295
297
  # machine. Move one and the URLs that name it here, in .env and in README.md
296
298
  # follow.
297
299
  # CORTEX_VERSION, set in the shell, runs another release of the image.
298
- #
299
- # The console's browser uploads (knowledge documents) go straight to
300
- # S3_ENDPOINT, which only this stack resolves. Point it at storage the
301
- # browser reaches too before relying on them.
302
300
 
303
301
  # The stack runs as .env's COMPOSE_PROJECT_NAME, drawn for each checkout by
304
302
  # setup.ts, so no two checkouts share containers or volumes, whatever their
@@ -316,9 +314,13 @@ x-cortex: &cortex
316
314
  DATABASE_URL: postgres://postgres:postgres@postgres:5432/cortex
317
315
  REDIS_URL: redis://redis:6379
318
316
  S3_ENDPOINT: http://minio:9000
317
+ # The console's uploads go straight to storage, from the browser.
318
+ S3_PUBLIC_ENDPOINT: http://localhost:{{port.storage}}
319
319
  S3_REGION: us-east-1
320
320
  S3_ACCESS_KEY_ID: minio
321
- S3_SECRET_ACCESS_KEY: minio-secret-key
321
+ # Interpolated like storage's own, so both read the one value even when the
322
+ # shell exports another.
323
+ S3_SECRET_ACCESS_KEY: \${S3_SECRET_ACCESS_KEY:?run bun install to generate .env}
322
324
  S3_BUCKET: cortex
323
325
  BETTER_AUTH_ALLOWED_HOSTS: localhost:{{port.cortex}},127.0.0.1:{{port.cortex}}
324
326
  # The dimension the image's migrations give the vector columns.
@@ -366,9 +368,13 @@ services:
366
368
  command: server /data
367
369
  environment:
368
370
  MINIO_ROOT_USER: minio
369
- MINIO_ROOT_PASSWORD: minio-secret-key
371
+ MINIO_ROOT_PASSWORD: \${S3_SECRET_ACCESS_KEY:?run bun install to generate .env}
372
+ # The console's uploads come from its origin alone.
373
+ MINIO_API_CORS_ALLOW_ORIGIN: http://localhost:{{port.cortex}},http://127.0.0.1:{{port.cortex}}
370
374
  volumes:
371
375
  - minio-data:/data
376
+ ports:
377
+ - "127.0.0.1:{{port.storage}}:9000"
372
378
  healthcheck:
373
379
  test: ["CMD", "bash", "-c", "</dev/tcp/127.0.0.1/9000"]
374
380
  interval: 5s
@@ -381,7 +387,7 @@ services:
381
387
  minio:
382
388
  condition: service_healthy
383
389
  environment:
384
- MC_HOST_local: http://minio:minio-secret-key@minio:9000
390
+ MC_HOST_local: http://minio:\${S3_SECRET_ACCESS_KEY:?run bun install to generate .env}@minio:9000
385
391
  command: ["mb", "--ignore-existing", "local/cortex"]
386
392
 
387
393
  # One-shot: migrate the database, then create the first admin and its key.
@@ -547,7 +553,7 @@ export function scaffoldFiles(input: Input) {
547
553
  ".gitignore": gitignoreText,
548
554
  ".dockerignore": dockerignoreText,
549
555
  "README.md": readme(project),
550
- "eslint.config.mjs": eslintConfig,
556
+ ".oxlintrc.json": oxlintrc,
551
557
  "prettier.config.mjs": prettierConfig,
552
558
  "cspell.json": cspellJson,
553
559
  ".fallowrc.jsonc": fallowrc,
@@ -2,9 +2,10 @@
2
2
  * Host ports for the Cortex stack a generated project runs, chosen once at
3
3
  * scaffold time.
4
4
  *
5
- * Only the app and the engine are published: the browser and this
6
- * project's callers reach them, and nothing outside the stack reaches its
7
- * databases. The backend listens on the third. Fixed ports would collide with
5
+ * The app, the engine and storage are published: the browser and this
6
+ * project's callers reach them (storage through the upload links Cortex
7
+ * signs), and nothing outside the stack reaches its databases. The backend
8
+ * listens on the fourth. Fixed ports would collide with
8
9
  * the next project scaffolded on the same machine, so each project draws its
9
10
  * own. Container ports stay canonical.
10
11
  *
@@ -60,7 +61,7 @@ export function allocatePorts() {
60
61
  return port;
61
62
  }
62
63
 
63
- return { cortex: next(), engine: next(), server: next() };
64
+ return { cortex: next(), engine: next(), storage: next(), server: next() };
64
65
  }
65
66
 
66
67
  export type Ports = ReturnType<typeof allocatePorts>;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": "0.2",
3
3
  "language": "en",
4
- "words": ["mprocs"],
4
+ "words": ["mprocs", "oxlint", "tsgolint"],
5
5
  "ignorePaths": [
6
6
  "node_modules/**",
7
7
  "out/**",
@@ -0,0 +1,67 @@
1
+ {
2
+ "$schema": "./node_modules/oxlint/configuration_schema.json",
3
+ // typescript-eslint's recommended type-checked set; nothing else runs.
4
+ "categories": {
5
+ "correctness": "off"
6
+ },
7
+ "options": {
8
+ "typeAware": true,
9
+ "reportUnusedDisableDirectives": "warn"
10
+ },
11
+ "ignorePatterns": ["**/node_modules/**", "**/dist/**", "**/.fallow/**"],
12
+ "overrides": [
13
+ {
14
+ "files": ["**/*.ts"],
15
+ "plugins": ["typescript"],
16
+ "rules": {
17
+ "no-var": "error",
18
+ "prefer-const": "error",
19
+ "prefer-rest-params": "error",
20
+ "prefer-spread": "error",
21
+ "no-array-constructor": "error",
22
+ "no-unused-expressions": "error",
23
+ "no-unused-vars": "error",
24
+ "typescript/await-thenable": "error",
25
+ "typescript/ban-ts-comment": "error",
26
+ "typescript/no-array-delete": "error",
27
+ "typescript/no-base-to-string": "error",
28
+ "typescript/no-duplicate-enum-values": "error",
29
+ "typescript/no-duplicate-type-constituents": "error",
30
+ "typescript/no-empty-object-type": "error",
31
+ "typescript/no-explicit-any": "error",
32
+ "typescript/no-extra-non-null-assertion": "error",
33
+ "typescript/no-floating-promises": "error",
34
+ "typescript/no-for-in-array": "error",
35
+ "typescript/no-implied-eval": "error",
36
+ "typescript/no-misused-new": "error",
37
+ "typescript/no-misused-promises": "error",
38
+ "typescript/no-namespace": "error",
39
+ "typescript/no-non-null-asserted-optional-chain": "error",
40
+ "typescript/no-redundant-type-constituents": "error",
41
+ "typescript/no-require-imports": "error",
42
+ "typescript/no-this-alias": "error",
43
+ "typescript/no-unnecessary-type-assertion": "error",
44
+ "typescript/no-unnecessary-type-constraint": "error",
45
+ "typescript/no-unsafe-argument": "error",
46
+ "typescript/no-unsafe-assignment": "error",
47
+ "typescript/no-unsafe-call": "error",
48
+ "typescript/no-unsafe-declaration-merging": "error",
49
+ "typescript/no-unsafe-enum-comparison": "error",
50
+ "typescript/no-unsafe-function-type": "error",
51
+ "typescript/no-unsafe-member-access": "error",
52
+ "typescript/no-unsafe-return": "error",
53
+ "typescript/no-unsafe-unary-minus": "error",
54
+ "typescript/no-wrapper-object-types": "error",
55
+ "typescript/only-throw-error": "error",
56
+ "typescript/prefer-as-const": "error",
57
+ "typescript/prefer-namespace-keyword": "error",
58
+ "typescript/prefer-promise-reject-errors": "error",
59
+ "typescript/require-await": "error",
60
+ "typescript/restrict-plus-operands": "error",
61
+ "typescript/restrict-template-expressions": "error",
62
+ "typescript/triple-slash-reference": "error",
63
+ "typescript/unbound-method": "error"
64
+ }
65
+ }
66
+ ]
67
+ }
@@ -30,6 +30,7 @@ const GENERATED = [
30
30
  ["BOOTSTRAP_MANAGEMENT_KEY", () => `cortex_mgmt_${token()}`],
31
31
  ["BETTER_AUTH_SECRET", key],
32
32
  ["SECRETS_MASTER_KEY", key],
33
+ ["S3_SECRET_ACCESS_KEY", token],
33
34
  ] as const;
34
35
 
35
36
  const source = existsSync(".env") ? ".env" : ".env.example";
@@ -1,20 +0,0 @@
1
- import { defineConfig, globalIgnores } from "eslint/config";
2
- import prettierConfig from "eslint-config-prettier";
3
- import prettierPlugin from "eslint-plugin-prettier";
4
- import tseslint from "typescript-eslint";
5
-
6
- export default defineConfig([
7
- globalIgnores(["**/node_modules/**", "**/dist/**", "**/.fallow/**"]),
8
- prettierConfig,
9
- {
10
- plugins: { prettier: prettierPlugin },
11
- rules: { "prettier/prettier": "error" },
12
- },
13
- {
14
- files: ["**/*.ts"],
15
- extends: [tseslint.configs.recommendedTypeChecked],
16
- languageOptions: {
17
- parserOptions: { projectService: true, tsconfigRootDir: import.meta.dirname },
18
- },
19
- },
20
- ]);