takomi 2.1.2 → 2.1.3

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 (49) hide show
  1. package/.pi/README.md +124 -124
  2. package/.pi/agents/architect.md +15 -15
  3. package/.pi/agents/coder.md +14 -14
  4. package/.pi/agents/designer.md +17 -17
  5. package/.pi/agents/orchestrator.md +22 -22
  6. package/.pi/agents/reviewer.md +16 -16
  7. package/.pi/extensions/oauth-router/README.md +125 -125
  8. package/.pi/extensions/oauth-router/commands.ts +380 -380
  9. package/.pi/extensions/oauth-router/config.ts +200 -200
  10. package/.pi/extensions/oauth-router/index.ts +41 -41
  11. package/.pi/extensions/oauth-router/oauth-flow.ts +154 -154
  12. package/.pi/extensions/oauth-router/oauth-store.ts +121 -121
  13. package/.pi/extensions/oauth-router/package.json +14 -14
  14. package/.pi/extensions/oauth-router/policies.ts +27 -27
  15. package/.pi/extensions/oauth-router/provider.ts +492 -492
  16. package/.pi/extensions/oauth-router/scripts/vibe-verify.py +98 -98
  17. package/.pi/extensions/oauth-router/state.ts +174 -174
  18. package/.pi/extensions/oauth-router/types.ts +153 -153
  19. package/.pi/extensions/takomi-runtime/command-text.ts +130 -130
  20. package/.pi/extensions/takomi-runtime/commands.ts +179 -179
  21. package/.pi/extensions/takomi-runtime/context-panel.ts +282 -282
  22. package/.pi/extensions/takomi-runtime/index.ts +1288 -1288
  23. package/.pi/extensions/takomi-runtime/profile.ts +114 -114
  24. package/.pi/extensions/takomi-runtime/routing-policy.ts +105 -105
  25. package/.pi/extensions/takomi-runtime/shared.ts +492 -492
  26. package/.pi/extensions/takomi-runtime/subagent-controller.ts +364 -364
  27. package/.pi/extensions/takomi-runtime/subagent-render.ts +501 -501
  28. package/.pi/extensions/takomi-runtime/subagent-types.ts +83 -83
  29. package/.pi/extensions/takomi-runtime/ui.ts +133 -133
  30. package/.pi/extensions/takomi-subagents/agent-aliases.ts +18 -18
  31. package/.pi/extensions/takomi-subagents/agents.ts +113 -113
  32. package/.pi/extensions/takomi-subagents/delegation-plan.ts +95 -95
  33. package/.pi/extensions/takomi-subagents/dispatch-helpers.ts +26 -26
  34. package/.pi/extensions/takomi-subagents/dispatch.ts +215 -215
  35. package/.pi/extensions/takomi-subagents/index.ts +75 -75
  36. package/.pi/extensions/takomi-subagents/live-updates.ts +83 -83
  37. package/.pi/extensions/takomi-subagents/native-render.ts +174 -174
  38. package/.pi/extensions/takomi-subagents/tool-runner.ts +209 -209
  39. package/.pi/themes/takomi-noir.json +81 -81
  40. package/package.json +59 -59
  41. package/src/doctor.js +87 -84
  42. package/src/pi-harness.js +355 -351
  43. package/src/pi-installer.js +193 -171
  44. package/src/pi-takomi-core/index.ts +4 -4
  45. package/src/pi-takomi-core/orchestration.ts +402 -402
  46. package/src/pi-takomi-core/routing.ts +93 -93
  47. package/src/pi-takomi-core/types.ts +173 -173
  48. package/src/pi-takomi-core/workflows.ts +299 -299
  49. package/src/skills-installer.js +101 -101
@@ -1,154 +1,154 @@
1
- import { spawn } from "node:child_process";
2
- import { randomUUID } from "node:crypto";
3
- import type { OAuthCredentials } from "@mariozechner/pi-ai";
4
- import { getOAuthProvider } from "@mariozechner/pi-ai/oauth";
5
- import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
6
- import type { RouterUpstreamConfig, StoredRouterAccount } from "./types.ts";
7
-
8
- function now() {
9
- return Date.now();
10
- }
11
-
12
- function normalizeCredentials(credentials: OAuthCredentials) {
13
- const { access, refresh, expires, ...meta } = credentials;
14
- return {
15
- access,
16
- refresh,
17
- expires,
18
- meta,
19
- };
20
- }
21
-
22
- export function openUrlInBrowser(url: string) {
23
- const platform = process.platform;
24
-
25
- try {
26
- if (platform === "win32") {
27
- const child = spawn("rundll32.exe", ["url.dll,FileProtocolHandler", url], {
28
- detached: true,
29
- stdio: "ignore",
30
- });
31
- child.unref();
32
- return;
33
- }
34
-
35
- if (platform === "darwin") {
36
- const child = spawn("open", [url], { detached: true, stdio: "ignore" });
37
- child.unref();
38
- return;
39
- }
40
-
41
- const child = spawn("xdg-open", [url], { detached: true, stdio: "ignore" });
42
- child.unref();
43
- } catch {
44
- // Best effort only.
45
- }
46
- }
47
-
48
- async function promptRequired(ctx: ExtensionContext, message: string, placeholder?: string): Promise<string> {
49
- const response = await ctx.ui.input(message, placeholder);
50
- if (response === undefined) throw new Error("Cancelled by user");
51
- return response;
52
- }
53
-
54
- export async function createAccountFromUpstream(
55
- upstream: RouterUpstreamConfig,
56
- label: string,
57
- ctx: ExtensionContext,
58
- ): Promise<StoredRouterAccount> {
59
- const createdAt = now();
60
-
61
- if (upstream.authMode === "api-key") {
62
- const token = await promptRequired(ctx, `Enter API key or bearer token for ${upstream.label}:`);
63
- return {
64
- id: `acct_${randomUUID().slice(0, 8)}`,
65
- label,
66
- provider: "api-key",
67
- upstreamId: upstream.id,
68
- access: token.trim(),
69
- refresh: "",
70
- expires: Number.MAX_SAFE_INTEGER,
71
- enabled: true,
72
- weight: 1,
73
- createdAt,
74
- updatedAt: createdAt,
75
- meta: {},
76
- };
77
- }
78
-
79
- if (!upstream.oauthProviderId) {
80
- throw new Error(`Upstream ${upstream.id} is missing oauthProviderId`);
81
- }
82
-
83
- const provider = getOAuthProvider(upstream.oauthProviderId);
84
- if (!provider) {
85
- throw new Error(`OAuth provider not available: ${upstream.oauthProviderId}`);
86
- }
87
-
88
- const credentials = await provider.login({
89
- onAuth(info) {
90
- openUrlInBrowser(info.url);
91
- ctx.ui.notify(`${provider.name}: ${info.instructions ?? "Finish login in your browser."}`, "info");
92
- ctx.ui.notify(info.url, "info");
93
- },
94
- onPrompt(prompt) {
95
- return promptRequired(ctx, prompt.message, prompt.placeholder);
96
- },
97
- onProgress(message) {
98
- ctx.ui.notify(message, "info");
99
- },
100
- });
101
-
102
- const normalized = normalizeCredentials(credentials);
103
-
104
- return {
105
- id: `acct_${randomUUID().slice(0, 8)}`,
106
- label,
107
- provider: upstream.oauthProviderId,
108
- upstreamId: upstream.id,
109
- access: normalized.access,
110
- refresh: normalized.refresh,
111
- expires: normalized.expires,
112
- enabled: true,
113
- weight: 1,
114
- createdAt,
115
- updatedAt: createdAt,
116
- meta: normalized.meta,
117
- };
118
- }
119
-
120
- function toCredentials(account: StoredRouterAccount): OAuthCredentials {
121
- return {
122
- access: account.access,
123
- refresh: account.refresh,
124
- expires: account.expires,
125
- ...(account.meta ?? {}),
126
- };
127
- }
128
-
129
- export async function refreshAccountCredentials(account: StoredRouterAccount): Promise<StoredRouterAccount> {
130
- if (account.provider === "api-key") return account;
131
-
132
- const provider = getOAuthProvider(account.provider);
133
- if (!provider) throw new Error(`OAuth provider not available: ${account.provider}`);
134
-
135
- const refreshed = await provider.refreshToken(toCredentials(account));
136
- const normalized = normalizeCredentials(refreshed);
137
-
138
- return {
139
- ...account,
140
- access: normalized.access,
141
- refresh: normalized.refresh,
142
- expires: normalized.expires,
143
- meta: normalized.meta,
144
- updatedAt: now(),
145
- };
146
- }
147
-
148
- export async function getApiKeyForAccount(account: StoredRouterAccount): Promise<string> {
149
- if (account.provider === "api-key") return account.access;
150
-
151
- const provider = getOAuthProvider(account.provider);
152
- if (!provider) throw new Error(`OAuth provider not available: ${account.provider}`);
153
- return provider.getApiKey(toCredentials(account));
154
- }
1
+ import { spawn } from "node:child_process";
2
+ import { randomUUID } from "node:crypto";
3
+ import type { OAuthCredentials } from "@mariozechner/pi-ai";
4
+ import { getOAuthProvider } from "@mariozechner/pi-ai/oauth";
5
+ import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
6
+ import type { RouterUpstreamConfig, StoredRouterAccount } from "./types.ts";
7
+
8
+ function now() {
9
+ return Date.now();
10
+ }
11
+
12
+ function normalizeCredentials(credentials: OAuthCredentials) {
13
+ const { access, refresh, expires, ...meta } = credentials;
14
+ return {
15
+ access,
16
+ refresh,
17
+ expires,
18
+ meta,
19
+ };
20
+ }
21
+
22
+ export function openUrlInBrowser(url: string) {
23
+ const platform = process.platform;
24
+
25
+ try {
26
+ if (platform === "win32") {
27
+ const child = spawn("rundll32.exe", ["url.dll,FileProtocolHandler", url], {
28
+ detached: true,
29
+ stdio: "ignore",
30
+ });
31
+ child.unref();
32
+ return;
33
+ }
34
+
35
+ if (platform === "darwin") {
36
+ const child = spawn("open", [url], { detached: true, stdio: "ignore" });
37
+ child.unref();
38
+ return;
39
+ }
40
+
41
+ const child = spawn("xdg-open", [url], { detached: true, stdio: "ignore" });
42
+ child.unref();
43
+ } catch {
44
+ // Best effort only.
45
+ }
46
+ }
47
+
48
+ async function promptRequired(ctx: ExtensionContext, message: string, placeholder?: string): Promise<string> {
49
+ const response = await ctx.ui.input(message, placeholder);
50
+ if (response === undefined) throw new Error("Cancelled by user");
51
+ return response;
52
+ }
53
+
54
+ export async function createAccountFromUpstream(
55
+ upstream: RouterUpstreamConfig,
56
+ label: string,
57
+ ctx: ExtensionContext,
58
+ ): Promise<StoredRouterAccount> {
59
+ const createdAt = now();
60
+
61
+ if (upstream.authMode === "api-key") {
62
+ const token = await promptRequired(ctx, `Enter API key or bearer token for ${upstream.label}:`);
63
+ return {
64
+ id: `acct_${randomUUID().slice(0, 8)}`,
65
+ label,
66
+ provider: "api-key",
67
+ upstreamId: upstream.id,
68
+ access: token.trim(),
69
+ refresh: "",
70
+ expires: Number.MAX_SAFE_INTEGER,
71
+ enabled: true,
72
+ weight: 1,
73
+ createdAt,
74
+ updatedAt: createdAt,
75
+ meta: {},
76
+ };
77
+ }
78
+
79
+ if (!upstream.oauthProviderId) {
80
+ throw new Error(`Upstream ${upstream.id} is missing oauthProviderId`);
81
+ }
82
+
83
+ const provider = getOAuthProvider(upstream.oauthProviderId);
84
+ if (!provider) {
85
+ throw new Error(`OAuth provider not available: ${upstream.oauthProviderId}`);
86
+ }
87
+
88
+ const credentials = await provider.login({
89
+ onAuth(info) {
90
+ openUrlInBrowser(info.url);
91
+ ctx.ui.notify(`${provider.name}: ${info.instructions ?? "Finish login in your browser."}`, "info");
92
+ ctx.ui.notify(info.url, "info");
93
+ },
94
+ onPrompt(prompt) {
95
+ return promptRequired(ctx, prompt.message, prompt.placeholder);
96
+ },
97
+ onProgress(message) {
98
+ ctx.ui.notify(message, "info");
99
+ },
100
+ });
101
+
102
+ const normalized = normalizeCredentials(credentials);
103
+
104
+ return {
105
+ id: `acct_${randomUUID().slice(0, 8)}`,
106
+ label,
107
+ provider: upstream.oauthProviderId,
108
+ upstreamId: upstream.id,
109
+ access: normalized.access,
110
+ refresh: normalized.refresh,
111
+ expires: normalized.expires,
112
+ enabled: true,
113
+ weight: 1,
114
+ createdAt,
115
+ updatedAt: createdAt,
116
+ meta: normalized.meta,
117
+ };
118
+ }
119
+
120
+ function toCredentials(account: StoredRouterAccount): OAuthCredentials {
121
+ return {
122
+ access: account.access,
123
+ refresh: account.refresh,
124
+ expires: account.expires,
125
+ ...(account.meta ?? {}),
126
+ };
127
+ }
128
+
129
+ export async function refreshAccountCredentials(account: StoredRouterAccount): Promise<StoredRouterAccount> {
130
+ if (account.provider === "api-key") return account;
131
+
132
+ const provider = getOAuthProvider(account.provider);
133
+ if (!provider) throw new Error(`OAuth provider not available: ${account.provider}`);
134
+
135
+ const refreshed = await provider.refreshToken(toCredentials(account));
136
+ const normalized = normalizeCredentials(refreshed);
137
+
138
+ return {
139
+ ...account,
140
+ access: normalized.access,
141
+ refresh: normalized.refresh,
142
+ expires: normalized.expires,
143
+ meta: normalized.meta,
144
+ updatedAt: now(),
145
+ };
146
+ }
147
+
148
+ export async function getApiKeyForAccount(account: StoredRouterAccount): Promise<string> {
149
+ if (account.provider === "api-key") return account.access;
150
+
151
+ const provider = getOAuthProvider(account.provider);
152
+ if (!provider) throw new Error(`OAuth provider not available: ${account.provider}`);
153
+ return provider.getApiKey(toCredentials(account));
154
+ }
@@ -1,121 +1,121 @@
1
- import { existsSync, readFileSync } from "node:fs";
2
- import { CREDENTIALS_PATH, writeJsonFile } from "./config.ts";
3
- import type { RouterCredentialStore, StoredRouterAccount } from "./types.ts";
4
-
5
- const EMPTY_STORE: RouterCredentialStore = {
6
- version: 1,
7
- accounts: [],
8
- };
9
-
10
- function normalizeAccount(account: StoredRouterAccount): StoredRouterAccount {
11
- return {
12
- ...account,
13
- enabled: account.enabled !== false,
14
- weight: Number.isFinite(account.weight) && account.weight > 0 ? Math.floor(account.weight) : 1,
15
- refresh: account.refresh ?? "",
16
- access: account.access ?? "",
17
- expires: Number.isFinite(account.expires) ? account.expires : Number.MAX_SAFE_INTEGER,
18
- createdAt: Number.isFinite(account.createdAt) ? account.createdAt : Date.now(),
19
- updatedAt: Number.isFinite(account.updatedAt) ? account.updatedAt : Date.now(),
20
- meta: account.meta ?? {},
21
- };
22
- }
23
-
24
- export function redactToken(token: string): string {
25
- if (!token) return "<empty>";
26
- if (token.length <= 8) return "********";
27
- return `${token.slice(0, 4)}…${token.slice(-4)}`;
28
- }
29
-
30
- export function summarizeAccount(account: StoredRouterAccount) {
31
- return {
32
- id: account.id,
33
- label: account.label,
34
- provider: account.provider,
35
- upstreamId: account.upstreamId,
36
- enabled: account.enabled,
37
- weight: account.weight,
38
- expires: account.expires,
39
- createdAt: account.createdAt,
40
- updatedAt: account.updatedAt,
41
- access: redactToken(account.access),
42
- refresh: redactToken(account.refresh),
43
- meta: account.meta ?? {},
44
- };
45
- }
46
-
47
- export class RouterAccountStore {
48
- private data: RouterCredentialStore;
49
-
50
- constructor() {
51
- this.data = this.load();
52
- }
53
-
54
- private load(): RouterCredentialStore {
55
- if (!existsSync(CREDENTIALS_PATH)) {
56
- writeJsonFile(CREDENTIALS_PATH, EMPTY_STORE, true);
57
- return { ...EMPTY_STORE, accounts: [] };
58
- }
59
-
60
- try {
61
- const parsed = JSON.parse(readFileSync(CREDENTIALS_PATH, "utf8")) as Partial<RouterCredentialStore>;
62
- return {
63
- version: 1,
64
- accounts: Array.isArray(parsed.accounts) ? parsed.accounts.map((account) => normalizeAccount(account)) : [],
65
- };
66
- } catch {
67
- writeJsonFile(CREDENTIALS_PATH, EMPTY_STORE, true);
68
- return { ...EMPTY_STORE, accounts: [] };
69
- }
70
- }
71
-
72
- private save() {
73
- writeJsonFile(CREDENTIALS_PATH, this.data, true);
74
- }
75
-
76
- reload() {
77
- this.data = this.load();
78
- }
79
-
80
- list(): StoredRouterAccount[] {
81
- return [...this.data.accounts].sort((a, b) => a.createdAt - b.createdAt);
82
- }
83
-
84
- get(id: string): StoredRouterAccount | undefined {
85
- return this.data.accounts.find((account) => account.id === id);
86
- }
87
-
88
- add(account: StoredRouterAccount) {
89
- this.data.accounts.push(normalizeAccount(account));
90
- this.save();
91
- }
92
-
93
- update(account: StoredRouterAccount) {
94
- const index = this.data.accounts.findIndex((item) => item.id === account.id);
95
- if (index === -1) throw new Error(`Unknown account: ${account.id}`);
96
- this.data.accounts[index] = normalizeAccount(account);
97
- this.save();
98
- }
99
-
100
- remove(id: string) {
101
- const next = this.data.accounts.filter((account) => account.id !== id);
102
- this.data.accounts = next;
103
- this.save();
104
- }
105
-
106
- setEnabled(id: string, enabled: boolean) {
107
- const account = this.get(id);
108
- if (!account) throw new Error(`Unknown account: ${id}`);
109
- account.enabled = enabled;
110
- account.updatedAt = Date.now();
111
- this.update(account);
112
- }
113
-
114
- setWeight(id: string, weight: number) {
115
- const account = this.get(id);
116
- if (!account) throw new Error(`Unknown account: ${id}`);
117
- account.weight = Math.max(1, Math.floor(weight));
118
- account.updatedAt = Date.now();
119
- this.update(account);
120
- }
121
- }
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { CREDENTIALS_PATH, writeJsonFile } from "./config.ts";
3
+ import type { RouterCredentialStore, StoredRouterAccount } from "./types.ts";
4
+
5
+ const EMPTY_STORE: RouterCredentialStore = {
6
+ version: 1,
7
+ accounts: [],
8
+ };
9
+
10
+ function normalizeAccount(account: StoredRouterAccount): StoredRouterAccount {
11
+ return {
12
+ ...account,
13
+ enabled: account.enabled !== false,
14
+ weight: Number.isFinite(account.weight) && account.weight > 0 ? Math.floor(account.weight) : 1,
15
+ refresh: account.refresh ?? "",
16
+ access: account.access ?? "",
17
+ expires: Number.isFinite(account.expires) ? account.expires : Number.MAX_SAFE_INTEGER,
18
+ createdAt: Number.isFinite(account.createdAt) ? account.createdAt : Date.now(),
19
+ updatedAt: Number.isFinite(account.updatedAt) ? account.updatedAt : Date.now(),
20
+ meta: account.meta ?? {},
21
+ };
22
+ }
23
+
24
+ export function redactToken(token: string): string {
25
+ if (!token) return "<empty>";
26
+ if (token.length <= 8) return "********";
27
+ return `${token.slice(0, 4)}…${token.slice(-4)}`;
28
+ }
29
+
30
+ export function summarizeAccount(account: StoredRouterAccount) {
31
+ return {
32
+ id: account.id,
33
+ label: account.label,
34
+ provider: account.provider,
35
+ upstreamId: account.upstreamId,
36
+ enabled: account.enabled,
37
+ weight: account.weight,
38
+ expires: account.expires,
39
+ createdAt: account.createdAt,
40
+ updatedAt: account.updatedAt,
41
+ access: redactToken(account.access),
42
+ refresh: redactToken(account.refresh),
43
+ meta: account.meta ?? {},
44
+ };
45
+ }
46
+
47
+ export class RouterAccountStore {
48
+ private data: RouterCredentialStore;
49
+
50
+ constructor() {
51
+ this.data = this.load();
52
+ }
53
+
54
+ private load(): RouterCredentialStore {
55
+ if (!existsSync(CREDENTIALS_PATH)) {
56
+ writeJsonFile(CREDENTIALS_PATH, EMPTY_STORE, true);
57
+ return { ...EMPTY_STORE, accounts: [] };
58
+ }
59
+
60
+ try {
61
+ const parsed = JSON.parse(readFileSync(CREDENTIALS_PATH, "utf8")) as Partial<RouterCredentialStore>;
62
+ return {
63
+ version: 1,
64
+ accounts: Array.isArray(parsed.accounts) ? parsed.accounts.map((account) => normalizeAccount(account)) : [],
65
+ };
66
+ } catch {
67
+ writeJsonFile(CREDENTIALS_PATH, EMPTY_STORE, true);
68
+ return { ...EMPTY_STORE, accounts: [] };
69
+ }
70
+ }
71
+
72
+ private save() {
73
+ writeJsonFile(CREDENTIALS_PATH, this.data, true);
74
+ }
75
+
76
+ reload() {
77
+ this.data = this.load();
78
+ }
79
+
80
+ list(): StoredRouterAccount[] {
81
+ return [...this.data.accounts].sort((a, b) => a.createdAt - b.createdAt);
82
+ }
83
+
84
+ get(id: string): StoredRouterAccount | undefined {
85
+ return this.data.accounts.find((account) => account.id === id);
86
+ }
87
+
88
+ add(account: StoredRouterAccount) {
89
+ this.data.accounts.push(normalizeAccount(account));
90
+ this.save();
91
+ }
92
+
93
+ update(account: StoredRouterAccount) {
94
+ const index = this.data.accounts.findIndex((item) => item.id === account.id);
95
+ if (index === -1) throw new Error(`Unknown account: ${account.id}`);
96
+ this.data.accounts[index] = normalizeAccount(account);
97
+ this.save();
98
+ }
99
+
100
+ remove(id: string) {
101
+ const next = this.data.accounts.filter((account) => account.id !== id);
102
+ this.data.accounts = next;
103
+ this.save();
104
+ }
105
+
106
+ setEnabled(id: string, enabled: boolean) {
107
+ const account = this.get(id);
108
+ if (!account) throw new Error(`Unknown account: ${id}`);
109
+ account.enabled = enabled;
110
+ account.updatedAt = Date.now();
111
+ this.update(account);
112
+ }
113
+
114
+ setWeight(id: string, weight: number) {
115
+ const account = this.get(id);
116
+ if (!account) throw new Error(`Unknown account: ${id}`);
117
+ account.weight = Math.max(1, Math.floor(weight));
118
+ account.updatedAt = Date.now();
119
+ this.update(account);
120
+ }
121
+ }
@@ -1,14 +1,14 @@
1
- {
2
- "name": "oauth-router",
3
- "private": true,
4
- "type": "module",
5
- "description": "Pi extension for multi-account OAuth-aware routing across OpenAI-compatible upstreams",
6
- "pi": {
7
- "extensions": [
8
- "./index.ts"
9
- ]
10
- },
11
- "scripts": {
12
- "verify": "python scripts/vibe-verify.py"
13
- }
14
- }
1
+ {
2
+ "name": "oauth-router",
3
+ "private": true,
4
+ "type": "module",
5
+ "description": "Pi extension for multi-account OAuth-aware routing across OpenAI-compatible upstreams",
6
+ "pi": {
7
+ "extensions": [
8
+ "./index.ts"
9
+ ]
10
+ },
11
+ "scripts": {
12
+ "verify": "python scripts/vibe-verify.py"
13
+ }
14
+ }
@@ -1,27 +1,27 @@
1
- import type { EligibleAccount, RoutingPolicyName } from "./types.ts";
2
-
3
- function sortStable(accounts: EligibleAccount[]): EligibleAccount[] {
4
- return [...accounts].sort((a, b) => {
5
- if (a.account.createdAt !== b.account.createdAt) return a.account.createdAt - b.account.createdAt;
6
- return a.account.id.localeCompare(b.account.id);
7
- });
8
- }
9
-
10
- function expandByWeight(accounts: EligibleAccount[]): EligibleAccount[] {
11
- return sortStable(accounts).flatMap((entry) => Array.from({ length: Math.max(1, entry.account.weight || 1) }, () => entry));
12
- }
13
-
14
- export function chooseEligibleAccount(
15
- policy: RoutingPolicyName,
16
- eligible: EligibleAccount[],
17
- cursor: number,
18
- ): { selected?: EligibleAccount; nextCursor: number } {
19
- if (eligible.length === 0) return { selected: undefined, nextCursor: 0 };
20
-
21
- const ordered = policy === "weighted-round-robin" ? expandByWeight(eligible) : sortStable(eligible);
22
- const index = ((cursor % ordered.length) + ordered.length) % ordered.length;
23
- return {
24
- selected: ordered[index],
25
- nextCursor: (index + 1) % ordered.length,
26
- };
27
- }
1
+ import type { EligibleAccount, RoutingPolicyName } from "./types.ts";
2
+
3
+ function sortStable(accounts: EligibleAccount[]): EligibleAccount[] {
4
+ return [...accounts].sort((a, b) => {
5
+ if (a.account.createdAt !== b.account.createdAt) return a.account.createdAt - b.account.createdAt;
6
+ return a.account.id.localeCompare(b.account.id);
7
+ });
8
+ }
9
+
10
+ function expandByWeight(accounts: EligibleAccount[]): EligibleAccount[] {
11
+ return sortStable(accounts).flatMap((entry) => Array.from({ length: Math.max(1, entry.account.weight || 1) }, () => entry));
12
+ }
13
+
14
+ export function chooseEligibleAccount(
15
+ policy: RoutingPolicyName,
16
+ eligible: EligibleAccount[],
17
+ cursor: number,
18
+ ): { selected?: EligibleAccount; nextCursor: number } {
19
+ if (eligible.length === 0) return { selected: undefined, nextCursor: 0 };
20
+
21
+ const ordered = policy === "weighted-round-robin" ? expandByWeight(eligible) : sortStable(eligible);
22
+ const index = ((cursor % ordered.length) + ordered.length) % ordered.length;
23
+ return {
24
+ selected: ordered[index],
25
+ nextCursor: (index + 1) % ordered.length,
26
+ };
27
+ }