@undefineds.co/linx 0.2.17 → 0.2.19

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 (117) hide show
  1. package/README.md +3 -6
  2. package/dist/generated/version.js +1 -1
  3. package/dist/generated/version.js.map +1 -1
  4. package/dist/index.js +256 -143
  5. package/dist/index.js.map +1 -1
  6. package/dist/lib/account-api.js +1 -1
  7. package/dist/lib/account-api.js.map +1 -1
  8. package/dist/lib/account-session.js +1 -1
  9. package/dist/lib/account-session.js.map +1 -1
  10. package/dist/lib/ai-command.js +186 -367
  11. package/dist/lib/ai-command.js.map +1 -1
  12. package/dist/lib/chat-api.js +177 -19
  13. package/dist/lib/chat-api.js.map +1 -1
  14. package/dist/lib/codex-plugin/bridge.js.map +1 -1
  15. package/dist/lib/codex-plugin/codex-native-proxy.js.map +1 -1
  16. package/dist/lib/codex-plugin/index.js.map +1 -1
  17. package/dist/lib/codex-plugin/runner.js.map +1 -1
  18. package/dist/lib/credentials-store.js +7 -1
  19. package/dist/lib/credentials-store.js.map +1 -1
  20. package/dist/lib/default-model.js +1 -0
  21. package/dist/lib/default-model.js.map +1 -1
  22. package/dist/lib/login-command.js +33 -10
  23. package/dist/lib/login-command.js.map +1 -1
  24. package/dist/lib/models.js +2 -27
  25. package/dist/lib/models.js.map +1 -1
  26. package/dist/lib/node-warning-filter.js +34 -0
  27. package/dist/lib/node-warning-filter.js.map +1 -0
  28. package/dist/lib/oidc-auth.js +130 -18
  29. package/dist/lib/oidc-auth.js.map +1 -1
  30. package/dist/lib/oidc-session-storage.js.map +1 -1
  31. package/dist/lib/pi-adapter/auth.js +47 -11
  32. package/dist/lib/pi-adapter/auth.js.map +1 -1
  33. package/dist/lib/pi-adapter/branding.js +802 -78
  34. package/dist/lib/pi-adapter/branding.js.map +1 -1
  35. package/dist/lib/pi-adapter/index.js.map +1 -1
  36. package/dist/lib/pi-adapter/interactive.js +179 -5
  37. package/dist/lib/pi-adapter/interactive.js.map +1 -1
  38. package/dist/lib/pi-adapter/pod-approval.js +8 -0
  39. package/dist/lib/pi-adapter/pod-approval.js.map +1 -0
  40. package/dist/lib/pi-adapter/pod-mirror-mapping.js +189 -0
  41. package/dist/lib/pi-adapter/pod-mirror-mapping.js.map +1 -0
  42. package/dist/lib/pi-adapter/pod-mirror.js +416 -0
  43. package/dist/lib/pi-adapter/pod-mirror.js.map +1 -0
  44. package/dist/lib/pi-adapter/pod-tools.js +104 -0
  45. package/dist/lib/pi-adapter/pod-tools.js.map +1 -0
  46. package/dist/lib/pi-adapter/runtime.js +327 -28
  47. package/dist/lib/pi-adapter/runtime.js.map +1 -1
  48. package/dist/lib/pi-adapter/session.js +608 -0
  49. package/dist/lib/pi-adapter/session.js.map +1 -0
  50. package/dist/lib/pi-adapter/stream.js +154 -4
  51. package/dist/lib/pi-adapter/stream.js.map +1 -1
  52. package/dist/lib/pi-adapter/theme.js.map +1 -1
  53. package/dist/lib/pi-adapter/web-fetch.js +154 -0
  54. package/dist/lib/pi-adapter/web-fetch.js.map +1 -0
  55. package/dist/lib/pod-chat-store.js +40 -30
  56. package/dist/lib/pod-chat-store.js.map +1 -1
  57. package/dist/lib/pod-data-session.js +110 -0
  58. package/dist/lib/pod-data-session.js.map +1 -0
  59. package/dist/lib/profile-identity.js +16 -60
  60. package/dist/lib/profile-identity.js.map +1 -1
  61. package/dist/lib/prompt.js.map +1 -1
  62. package/dist/lib/runtime-target.js +1 -1
  63. package/dist/lib/runtime-target.js.map +1 -1
  64. package/dist/lib/solid-auth.js.map +1 -1
  65. package/dist/lib/thread-utils.js.map +1 -1
  66. package/dist/lib/watch/archive.js +1 -1
  67. package/dist/lib/watch/archive.js.map +1 -1
  68. package/dist/lib/watch/auth.js +1 -1
  69. package/dist/lib/watch/auth.js.map +1 -1
  70. package/dist/lib/watch/codex-composer.js.map +1 -1
  71. package/dist/lib/watch/codex-footer.js.map +1 -1
  72. package/dist/lib/watch/codex-overlay.js.map +1 -1
  73. package/dist/lib/watch/codex-request-form.js +1 -1
  74. package/dist/lib/watch/codex-request-form.js.map +1 -1
  75. package/dist/lib/watch/codex-request-input.js.map +1 -1
  76. package/dist/lib/watch/display.js.map +1 -1
  77. package/dist/lib/watch/format.js.map +1 -1
  78. package/dist/lib/watch/hooks/claude.js +4 -0
  79. package/dist/lib/watch/hooks/claude.js.map +1 -1
  80. package/dist/lib/watch/hooks/codebuddy.js +4 -0
  81. package/dist/lib/watch/hooks/codebuddy.js.map +1 -1
  82. package/dist/lib/watch/hooks/codex.js +4 -0
  83. package/dist/lib/watch/hooks/codex.js.map +1 -1
  84. package/dist/lib/watch/hooks/index.js.map +1 -1
  85. package/dist/lib/watch/hooks/shared.js +0 -1
  86. package/dist/lib/watch/hooks/shared.js.map +1 -1
  87. package/dist/lib/watch/index.js.map +1 -1
  88. package/dist/lib/watch/pod-ai.js +29 -37
  89. package/dist/lib/watch/pod-ai.js.map +1 -1
  90. package/dist/lib/watch/pod-approval.js +822 -220
  91. package/dist/lib/watch/pod-approval.js.map +1 -1
  92. package/dist/lib/watch/pod-persistence.js +214 -106
  93. package/dist/lib/watch/pod-persistence.js.map +1 -1
  94. package/dist/lib/watch/runner.js +243 -38
  95. package/dist/lib/watch/runner.js.map +1 -1
  96. package/dist/lib/watch/secretary.js +238 -0
  97. package/dist/lib/watch/secretary.js.map +1 -0
  98. package/dist/lib/watch/types.js.map +1 -1
  99. package/dist/watch-cli.js +8 -34
  100. package/dist/watch-cli.js.map +1 -1
  101. package/package.json +3 -9
  102. package/vendor/agent-runtime/dist/acp.d.ts +27 -0
  103. package/vendor/agent-runtime/dist/acp.js +86 -0
  104. package/vendor/agent-runtime/dist/companion-model.d.ts +7 -0
  105. package/vendor/agent-runtime/dist/companion-model.js +12 -0
  106. package/vendor/agent-runtime/dist/index.d.ts +3 -0
  107. package/vendor/agent-runtime/dist/index.js +3 -0
  108. package/vendor/agent-runtime/dist/turn-controller.d.ts +69 -0
  109. package/vendor/agent-runtime/dist/turn-controller.js +129 -0
  110. package/vendor/agent-runtime/package.json +11 -0
  111. package/vendor/client/dist/client/index.d.ts +0 -118
  112. package/vendor/client/dist/client/index.js +0 -260
  113. package/vendor/client/dist/index.d.ts +0 -1
  114. package/vendor/client/dist/index.js +0 -1
  115. package/vendor/client/dist/watch/index.d.ts +0 -226
  116. package/vendor/client/dist/watch/index.js +0 -1114
  117. package/vendor/client/package.json +0 -9
@@ -1,27 +1,20 @@
1
- import { resolveLinxPodUrl } from '../../vendor/client/dist/index.js';
2
- import { aiConfigProviderUri, buildAIConfigMutationPlan, buildAIConfigProviderStateMap, getAIConfigProviderMetadata, normalizeAIConfigProviderId as extractAIConfigProviderId, normalizeAIConfigResourceId as extractAIConfigResourceId, sameAIConfigProviderFamily as sameAIConfigProviderId, } from '@undefineds.co/models/ai-config';
3
- import { DCTerms, RDFS, UDFS, XPOD_AI, XPOD_CREDENTIAL } from '@undefineds.co/models';
4
- import { getClientCredentials, loadCredentials } from './credentials-store.js';
1
+ import { resolveLinxPodUrl } from '@undefineds.co/models/client';
2
+ import { aiModelTable, aiProviderTable, buildAIConfigMutationPlan, buildAIConfigProviderStateMap, credentialTable, drizzle, getAIConfigProviderMetadata, initSolidTables, normalizeAIConfigProviderId, normalizeAIConfigResourceId, sameAIConfigProviderFamily, solidSchema, } from './models.js';
3
+ import { getClientCredentialId, getClientCredentialKey, getClientCredentials, loadCredentials } from './credentials-store.js';
5
4
  import { loadAccountSession } from './account-session.js';
6
5
  import { authenticatedFetch, getAccessToken } from './solid-auth.js';
6
+ import { getOidcAccessToken } from './oidc-auth.js';
7
7
  import { promptPassword } from './prompt.js';
8
- const XSD_DATE_TIME = 'http://www.w3.org/2001/XMLSchema#dateTime';
9
8
  let aiRuntimePromise = null;
10
9
  async function loadAiRuntime() {
11
10
  if (!aiRuntimePromise) {
12
11
  aiRuntimePromise = Promise.resolve({
13
- DCTerms,
14
- RDFS,
15
- UDFS,
16
- XPOD_AI,
17
- XPOD_CREDENTIAL,
18
- aiConfigProviderUri,
19
12
  buildAIConfigMutationPlan,
20
13
  buildAIConfigProviderStateMap,
21
- extractAIConfigProviderId,
22
- extractAIConfigResourceId,
23
14
  getAIConfigProviderMetadata,
24
- sameAIConfigProviderId,
15
+ normalizeAIConfigProviderId,
16
+ normalizeAIConfigResourceId,
17
+ sameAIConfigProviderFamily,
25
18
  });
26
19
  }
27
20
  return aiRuntimePromise;
@@ -31,104 +24,6 @@ function maskSecret(value) {
31
24
  return '****';
32
25
  return `${value.slice(0, 4)}****${value.slice(-4)}`;
33
26
  }
34
- function absolutePodUri(podUrl, relativePath) {
35
- return `${podUrl.replace(/\/?$/, '/')}${relativePath.replace(/^\/+/, '')}`;
36
- }
37
- function escapeRegex(value) {
38
- return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
39
- }
40
- function quoteLiteral(value) {
41
- return `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n')}"`;
42
- }
43
- function dateTimeLiteral(value) {
44
- return `"${value.toISOString()}"^^<${XSD_DATE_TIME}>`;
45
- }
46
- function splitResourceBlocks(turtle, resourceUrl) {
47
- if (!turtle.trim())
48
- return [];
49
- const pattern = new RegExp(`(?=<${escapeRegex(resourceUrl)}#)`);
50
- return turtle
51
- .split(pattern)
52
- .map((block) => block.trim())
53
- .filter(Boolean);
54
- }
55
- function matchLiteral(block, predicateUri) {
56
- const match = block.match(new RegExp(`<${escapeRegex(predicateUri)}>\\s+"([^"]*)"(?:\\^\\^<[^>]+>)?`));
57
- return match?.[1];
58
- }
59
- function matchIri(block, predicateUri) {
60
- const match = block.match(new RegExp(`<${escapeRegex(predicateUri)}>\\s+<([^>]+)>`));
61
- return match?.[1];
62
- }
63
- function matchFirstLiteral(block, predicateUris) {
64
- for (const predicateUri of predicateUris) {
65
- if (!predicateUri)
66
- continue;
67
- const value = matchLiteral(block, predicateUri);
68
- if (value !== undefined)
69
- return value;
70
- }
71
- return undefined;
72
- }
73
- function matchFirstIri(block, predicateUris) {
74
- for (const predicateUri of predicateUris) {
75
- if (!predicateUri)
76
- continue;
77
- const value = matchIri(block, predicateUri);
78
- if (value !== undefined)
79
- return value;
80
- }
81
- return undefined;
82
- }
83
- function parseCredentialRows(turtle, resourceUrl, runtime) {
84
- return splitResourceBlocks(turtle, resourceUrl)
85
- .map((block) => {
86
- const subject = block.match(new RegExp(`^<${escapeRegex(resourceUrl)}#([^>]+)>`));
87
- if (!subject?.[1])
88
- return null;
89
- return {
90
- id: subject[1],
91
- provider: matchFirstIri(block, [runtime.XPOD_CREDENTIAL.provider, runtime.UDFS.provider]),
92
- service: matchFirstLiteral(block, [runtime.XPOD_CREDENTIAL.service, runtime.UDFS.service]),
93
- status: matchFirstLiteral(block, [runtime.XPOD_CREDENTIAL.status, runtime.UDFS.status]),
94
- apiKey: matchFirstLiteral(block, [runtime.XPOD_CREDENTIAL.apiKey, runtime.UDFS.apiKey]),
95
- baseUrl: matchFirstLiteral(block, [runtime.XPOD_CREDENTIAL.baseUrl, runtime.UDFS.baseUrl]),
96
- label: matchFirstLiteral(block, [runtime.XPOD_CREDENTIAL.label, runtime.RDFS.label]),
97
- };
98
- })
99
- .filter(Boolean);
100
- }
101
- function parseProviderRows(turtle, resourceUrl, runtime) {
102
- return splitResourceBlocks(turtle, resourceUrl)
103
- .map((block) => {
104
- const subject = block.match(new RegExp(`^<${escapeRegex(resourceUrl)}#([^>]+)>`));
105
- if (!subject?.[1])
106
- return null;
107
- return {
108
- id: subject[1],
109
- baseUrl: matchFirstLiteral(block, [runtime.XPOD_AI.baseUrl, runtime.UDFS.baseUrl]),
110
- proxyUrl: matchFirstLiteral(block, [runtime.XPOD_AI.proxyUrl, runtime.UDFS.proxyUrl]),
111
- hasModel: matchFirstIri(block, [runtime.XPOD_AI.hasModel, runtime.UDFS.hasModel]),
112
- };
113
- })
114
- .filter(Boolean);
115
- }
116
- function parseModelRows(turtle, resourceUrl, runtime) {
117
- return splitResourceBlocks(turtle, resourceUrl)
118
- .map((block) => {
119
- const subject = block.match(new RegExp(`^<${escapeRegex(resourceUrl)}#([^>]+)>`));
120
- if (!subject?.[1])
121
- return null;
122
- return {
123
- id: subject[1],
124
- displayName: matchFirstLiteral(block, [runtime.XPOD_AI.displayName, runtime.RDFS.label]),
125
- modelType: matchFirstLiteral(block, [runtime.XPOD_AI.modelType, runtime.UDFS.modelType]),
126
- isProvidedBy: matchFirstIri(block, [runtime.XPOD_AI.isProvidedBy, runtime.UDFS.isProvidedBy]),
127
- status: matchFirstLiteral(block, [runtime.XPOD_AI.status, runtime.UDFS.status]),
128
- };
129
- })
130
- .filter(Boolean);
131
- }
132
27
  async function requireApiKey(argv) {
133
28
  if (argv['api-key']?.trim()) {
134
29
  return argv['api-key'].trim();
@@ -144,160 +39,202 @@ async function resolvePodWriteContext(urlOverride) {
144
39
  if (!creds) {
145
40
  throw new Error('No local client credentials found. Run `linx login` first.');
146
41
  }
42
+ if (!creds.webId) {
43
+ throw new Error('No WebID found in local credentials. Run `linx login` again.');
44
+ }
147
45
  const clientCreds = getClientCredentials(creds);
148
- if (!clientCreds) {
149
- throw new Error('Only client credentials auth is supported for `linx ai connect`.');
46
+ let accessToken = null;
47
+ if (clientCreds) {
48
+ const baseUrl = (urlOverride ?? creds.url).replace(/\/?$/, '/');
49
+ const tokenResult = await getAccessToken(getClientCredentialId(clientCreds), getClientCredentialKey(clientCreds), baseUrl);
50
+ accessToken = tokenResult?.accessToken ?? null;
51
+ }
52
+ else {
53
+ accessToken = await getOidcAccessToken(creds);
150
54
  }
151
- const baseUrl = (urlOverride ?? creds.url).replace(/\/?$/, '/');
152
- const tokenResult = await getAccessToken(clientCreds.clientId, clientCreds.clientSecret, baseUrl);
153
- if (!tokenResult) {
55
+ if (!accessToken) {
154
56
  throw new Error('Failed to obtain Pod access token. Run `linx login` again.');
155
57
  }
156
58
  return {
157
- accessToken: tokenResult.accessToken,
59
+ accessToken,
60
+ webId: creds.webId,
158
61
  podUrl: loadAccountSession()?.podUrl || resolveLinxPodUrl(creds.webId),
159
62
  };
160
63
  }
161
- async function patchResource(resourceUrl, accessToken, body) {
162
- const res = await authenticatedFetch(resourceUrl, accessToken, {
163
- method: 'PATCH',
164
- headers: { 'Content-Type': 'application/sparql-update' },
165
- body,
64
+ function createAiConfigDb(context) {
65
+ const solidSession = {
66
+ info: {
67
+ isLoggedIn: true,
68
+ webId: context.webId,
69
+ podUrl: context.podUrl,
70
+ },
71
+ fetch: (input, init) => (authenticatedFetch(requestInputToUrl(input), context.accessToken, init)),
72
+ logout: async () => { },
73
+ };
74
+ return drizzle(solidSession, {
75
+ logger: false,
76
+ disableInteropDiscovery: true,
77
+ schema: solidSchema,
166
78
  });
167
- if (!res.ok) {
168
- throw new Error(`Failed to update ${resourceUrl}.`);
169
- }
170
79
  }
171
- async function fetchResourceText(resourceUrl, accessToken) {
172
- const res = await authenticatedFetch(resourceUrl, accessToken, {
173
- headers: { Accept: 'text/turtle' },
174
- });
175
- if (res.status === 404) {
176
- return '';
80
+ async function loadAiConfigRows(db) {
81
+ await initSolidTables(db, [credentialTable, aiProviderTable, aiModelTable]);
82
+ const [credentialRows, providerRows, modelRows] = await Promise.all([
83
+ db.select().from(credentialTable).execute(),
84
+ db.select().from(aiProviderTable).execute(),
85
+ db.select().from(aiModelTable).execute(),
86
+ ]);
87
+ return { credentialRows, providerRows, modelRows };
88
+ }
89
+ async function upsertByLocator(db, table, locator, insert, update) {
90
+ const existing = await db.findByLocator(table, locator);
91
+ if (!existing) {
92
+ await db.insert(table).values(insert).execute();
93
+ return;
177
94
  }
178
- if (!res.ok) {
179
- throw new Error(`Failed to read ${resourceUrl}.`);
95
+ await db.updateByLocator(table, locator, update);
96
+ }
97
+ async function deleteByLocatorIfExists(db, table, locator) {
98
+ const existing = await db.findByLocator(table, locator);
99
+ if (existing) {
100
+ await db.deleteByLocator(table, locator);
180
101
  }
181
- return res.text();
182
102
  }
183
- function buildProviderSparql(runtime, resourceUrl, podUrl, payload) {
184
- const subject = `<${resourceUrl}#${payload.id}>`;
185
- const deletes = [
186
- `OPTIONAL { ${subject} <${runtime.XPOD_AI.baseUrl}> ?oldBaseUrl }`,
187
- `OPTIONAL { ${subject} <${runtime.XPOD_AI.proxyUrl}> ?oldProxyUrl }`,
188
- `OPTIONAL { ${subject} <${runtime.XPOD_AI.hasModel}> ?oldHasModel }`,
189
- ];
190
- const inserts = [`${subject} a <${runtime.XPOD_AI.Provider}> .`];
191
- if (payload.baseUrl) {
192
- inserts.push(`${subject} <${runtime.XPOD_AI.baseUrl}> ${quoteLiteral(payload.baseUrl)} .`);
103
+ function requestInputToUrl(input) {
104
+ if (typeof input === 'string')
105
+ return input;
106
+ if (input instanceof URL)
107
+ return input.toString();
108
+ if (typeof Request !== 'undefined' && input instanceof Request)
109
+ return input.url;
110
+ return String(input);
111
+ }
112
+ export async function runAiCommand(argv, dependencies = {}) {
113
+ const action = argv.action;
114
+ const aiRuntime = dependencies.aiRuntime ?? await loadAiRuntime();
115
+ const podContext = await (dependencies.resolvePodWriteContext ?? resolvePodWriteContext)(argv.url);
116
+ const db = (dependencies.createDb ?? createAiConfigDb)(podContext);
117
+ const { credentialRows, providerRows, modelRows } = await (dependencies.loadAiConfigRows ?? loadAiConfigRows)(db);
118
+ const write = dependencies.write ?? ((chunk) => process.stdout.write(chunk));
119
+ const upsert = dependencies.upsertByLocator ?? upsertByLocator;
120
+ const deleteIfExists = dependencies.deleteByLocatorIfExists ?? deleteByLocatorIfExists;
121
+ const getApiKey = dependencies.requireApiKey ?? requireApiKey;
122
+ if (action === 'status') {
123
+ const states = aiRuntime.buildAIConfigProviderStateMap({
124
+ fallbackToCatalogModels: false,
125
+ credentialRows,
126
+ providerRows,
127
+ modelRows,
128
+ });
129
+ const filtered = Object.values(states).filter((state) => {
130
+ if (!state.apiKey)
131
+ return false;
132
+ if (!argv.provider)
133
+ return true;
134
+ return aiRuntime.sameAIConfigProviderFamily(argv.provider, state.id);
135
+ });
136
+ if (filtered.length === 0) {
137
+ write('No LinX cloud AI credentials found.\n');
138
+ return;
139
+ }
140
+ const lines = [];
141
+ for (const state of filtered) {
142
+ lines.push(`provider: ${state.id}`);
143
+ if (state.selectedModelId) {
144
+ lines.push(`model: ${state.selectedModelId}`);
145
+ }
146
+ lines.push(`api-key: ${maskSecret(state.apiKey)}`);
147
+ lines.push('');
148
+ }
149
+ write(`${lines.join('\n').trimEnd()}\n`);
150
+ return;
193
151
  }
194
- if (payload.proxyUrl) {
195
- inserts.push(`${subject} <${runtime.XPOD_AI.proxyUrl}> ${quoteLiteral(payload.proxyUrl)} .`);
152
+ const providerArg = argv.provider?.trim();
153
+ if (!providerArg) {
154
+ throw new Error(action === 'disconnect'
155
+ ? 'Usage: linx ai disconnect <provider>'
156
+ : 'Usage: linx ai connect <provider> --api-key <key>');
196
157
  }
197
- if (payload.hasModel) {
198
- inserts.push(`${subject} <${runtime.XPOD_AI.hasModel}> <${absolutePodUri(podUrl, payload.hasModel)}> .`);
158
+ const provider = aiRuntime.normalizeAIConfigProviderId(providerArg);
159
+ if (action === 'disconnect') {
160
+ for (const row of credentialRows) {
161
+ const rowProvider = typeof row.provider === 'string' ? row.provider : row.id;
162
+ if (!aiRuntime.sameAIConfigProviderFamily(rowProvider, provider)) {
163
+ continue;
164
+ }
165
+ await deleteIfExists(db, credentialTable, { id: row.id });
166
+ }
167
+ write(`Disconnected AI provider: ${provider}\n`);
168
+ return;
199
169
  }
200
- return `DELETE {
201
- ${subject} <${runtime.XPOD_AI.baseUrl}> ?oldBaseUrl .
202
- ${subject} <${runtime.XPOD_AI.proxyUrl}> ?oldProxyUrl .
203
- ${subject} <${runtime.XPOD_AI.hasModel}> ?oldHasModel .
204
- }
205
- INSERT {
206
- ${inserts.join('\n ')}
207
- }
208
- WHERE {
209
- ${deletes.join('\n ')}
210
- }`;
211
- }
212
- function buildCredentialSparql(runtime, resourceUrl, podUrl, payload) {
213
- const subject = `<${resourceUrl}#${payload.id}>`;
214
- const deletes = [
215
- `OPTIONAL { ${subject} <${runtime.XPOD_CREDENTIAL.provider}> ?oldProvider }`,
216
- `OPTIONAL { ${subject} <${runtime.XPOD_CREDENTIAL.service}> ?oldService }`,
217
- `OPTIONAL { ${subject} <${runtime.XPOD_CREDENTIAL.status}> ?oldStatus }`,
218
- `OPTIONAL { ${subject} <${runtime.XPOD_CREDENTIAL.apiKey}> ?oldApiKey }`,
219
- `OPTIONAL { ${subject} <${runtime.XPOD_CREDENTIAL.baseUrl}> ?oldBaseUrl }`,
220
- `OPTIONAL { ${subject} <${runtime.XPOD_CREDENTIAL.label}> ?oldLabel }`,
221
- ];
222
- const inserts = [
223
- `${subject} a <${runtime.XPOD_CREDENTIAL.Credential}> .`,
224
- `${subject} <${runtime.XPOD_CREDENTIAL.provider}> <${absolutePodUri(podUrl, payload.provider)}> .`,
225
- `${subject} <${runtime.XPOD_CREDENTIAL.service}> ${quoteLiteral(payload.service)} .`,
226
- `${subject} <${runtime.XPOD_CREDENTIAL.status}> ${quoteLiteral(payload.status)} .`,
227
- ];
228
- if (payload.apiKey) {
229
- inserts.push(`${subject} <${runtime.XPOD_CREDENTIAL.apiKey}> ${quoteLiteral(payload.apiKey)} .`);
170
+ const apiKey = await getApiKey(argv);
171
+ const modelId = argv.model?.trim();
172
+ const plan = aiRuntime.buildAIConfigMutationPlan({
173
+ providerId: provider,
174
+ currentProviderRows: providerRows,
175
+ currentCredentialRows: credentialRows,
176
+ currentModelRows: modelRows,
177
+ updates: {
178
+ enabled: true,
179
+ apiKey,
180
+ baseUrl: argv['base-url']?.trim() || undefined,
181
+ models: modelId
182
+ ? [{
183
+ id: modelId,
184
+ name: modelId,
185
+ enabled: true,
186
+ capabilities: [],
187
+ }]
188
+ : undefined,
189
+ },
190
+ });
191
+ if (plan.providerPayload?.id) {
192
+ await upsert(db, aiProviderTable, { id: plan.providerPayload.id }, plan.providerPayload, {
193
+ baseUrl: plan.providerPayload.baseUrl,
194
+ proxyUrl: plan.providerPayload.proxyUrl,
195
+ hasModel: plan.providerPayload.hasModel,
196
+ });
230
197
  }
231
- if (payload.baseUrl) {
232
- inserts.push(`${subject} <${runtime.XPOD_CREDENTIAL.baseUrl}> ${quoteLiteral(payload.baseUrl)} .`);
198
+ if (plan.credentialPayload?.id
199
+ && plan.credentialPayload.provider
200
+ && plan.credentialPayload.service
201
+ && plan.credentialPayload.status) {
202
+ await upsert(db, credentialTable, { id: plan.credentialPayload.id }, plan.credentialPayload, {
203
+ provider: plan.credentialPayload.provider,
204
+ service: plan.credentialPayload.service,
205
+ status: plan.credentialPayload.status,
206
+ apiKey: plan.credentialPayload.apiKey,
207
+ baseUrl: plan.credentialPayload.baseUrl,
208
+ label: plan.credentialPayload.label,
209
+ });
233
210
  }
234
- if (payload.label) {
235
- inserts.push(`${subject} <${runtime.XPOD_CREDENTIAL.label}> ${quoteLiteral(payload.label)} .`);
211
+ for (const modelPayload of plan.modelUpserts) {
212
+ if (!modelPayload.id || !modelPayload.isProvidedBy || !modelPayload.createdAt || !modelPayload.updatedAt) {
213
+ continue;
214
+ }
215
+ await upsert(db, aiModelTable, {
216
+ id: modelPayload.id,
217
+ isProvidedBy: modelPayload.isProvidedBy,
218
+ }, modelPayload, {
219
+ displayName: modelPayload.displayName,
220
+ modelType: modelPayload.modelType,
221
+ isProvidedBy: modelPayload.isProvidedBy,
222
+ status: modelPayload.status,
223
+ updatedAt: modelPayload.updatedAt,
224
+ });
236
225
  }
237
- return `DELETE {
238
- ${subject} <${runtime.XPOD_CREDENTIAL.provider}> ?oldProvider .
239
- ${subject} <${runtime.XPOD_CREDENTIAL.service}> ?oldService .
240
- ${subject} <${runtime.XPOD_CREDENTIAL.status}> ?oldStatus .
241
- ${subject} <${runtime.XPOD_CREDENTIAL.apiKey}> ?oldApiKey .
242
- ${subject} <${runtime.XPOD_CREDENTIAL.baseUrl}> ?oldBaseUrl .
243
- ${subject} <${runtime.XPOD_CREDENTIAL.label}> ?oldLabel .
244
- }
245
- INSERT {
246
- ${inserts.join('\n ')}
247
- }
248
- WHERE {
249
- ${deletes.join('\n ')}
250
- }`;
251
- }
252
- function buildModelSparql(runtime, resourceUrl, podUrl, payload) {
253
- const subject = `<${resourceUrl}#${payload.id}>`;
254
- const deletes = [
255
- `OPTIONAL { ${subject} <${runtime.XPOD_AI.displayName}> ?oldDisplayName }`,
256
- `OPTIONAL { ${subject} <${runtime.XPOD_AI.modelType}> ?oldModelType }`,
257
- `OPTIONAL { ${subject} <${runtime.XPOD_AI.isProvidedBy}> ?oldIsProvidedBy }`,
258
- `OPTIONAL { ${subject} <${runtime.XPOD_AI.status}> ?oldStatus }`,
259
- `OPTIONAL { ${subject} <${runtime.XPOD_AI.createdAt}> ?oldCreatedAt }`,
260
- `OPTIONAL { ${subject} <${runtime.XPOD_AI.updatedAt}> ?oldUpdatedAt }`,
261
- ];
262
- const inserts = [
263
- `${subject} a <${runtime.XPOD_AI.Model}> .`,
264
- `${subject} <${runtime.XPOD_AI.displayName}> ${quoteLiteral(payload.displayName || payload.id)} .`,
265
- `${subject} <${runtime.XPOD_AI.modelType}> ${quoteLiteral(payload.modelType || 'chat')} .`,
266
- `${subject} <${runtime.XPOD_AI.isProvidedBy}> <${absolutePodUri(podUrl, payload.isProvidedBy)}> .`,
267
- `${subject} <${runtime.XPOD_AI.status}> ${quoteLiteral(payload.status || 'active')} .`,
268
- `${subject} <${runtime.XPOD_AI.createdAt}> ${dateTimeLiteral(payload.createdAt)} .`,
269
- `${subject} <${runtime.XPOD_AI.updatedAt}> ${dateTimeLiteral(payload.updatedAt)} .`,
270
- ];
271
- return `DELETE {
272
- ${subject} <${runtime.XPOD_AI.displayName}> ?oldDisplayName .
273
- ${subject} <${runtime.XPOD_AI.modelType}> ?oldModelType .
274
- ${subject} <${runtime.XPOD_AI.isProvidedBy}> ?oldIsProvidedBy .
275
- ${subject} <${runtime.XPOD_AI.status}> ?oldStatus .
276
- ${subject} <${runtime.XPOD_AI.createdAt}> ?oldCreatedAt .
277
- ${subject} <${runtime.XPOD_AI.updatedAt}> ?oldUpdatedAt .
278
- }
279
- INSERT {
280
- ${inserts.join('\n ')}
281
- }
282
- WHERE {
283
- ${deletes.join('\n ')}
284
- }`;
285
- }
286
- function buildCredentialDeleteSparql(runtime, providerRef) {
287
- return `DELETE {
288
- ?credential ?predicate ?object .
289
- }
290
- WHERE {
291
- {
292
- ?credential <${runtime.XPOD_CREDENTIAL.provider}> <${providerRef}> ;
293
- ?predicate ?object .
294
- }
295
- UNION
296
- {
297
- ?credential <${runtime.UDFS.provider}> <${providerRef}> ;
298
- ?predicate ?object .
299
- }
300
- }`;
226
+ for (const modelIdToDelete of plan.modelDeleteIds) {
227
+ await deleteIfExists(db, aiModelTable, {
228
+ id: modelIdToDelete,
229
+ isProvidedBy: plan.providerId,
230
+ });
231
+ }
232
+ const metadata = aiRuntime.getAIConfigProviderMetadata(provider);
233
+ write(`Connected AI provider: ${metadata.id}\n`);
234
+ if (modelId) {
235
+ write(`model: ${aiRuntime.normalizeAIConfigResourceId(modelId)}\n`);
236
+ }
237
+ write(`api-key: ${maskSecret(apiKey)}\n`);
301
238
  }
302
239
  export const aiCommand = {
303
240
  command: 'ai <action> [provider]',
@@ -309,130 +246,12 @@ export const aiCommand = {
309
246
  })
310
247
  .positional('provider', {
311
248
  type: 'string',
312
- description: 'Provider/backend id, for example anthropic, openai, codebuddy',
249
+ description: 'Provider/backend id, for example claude, anthropic, codebuddy, codex',
313
250
  })
314
251
  .option('url', { type: 'string', description: 'Server base URL override' })
315
252
  .option('api-key', { type: 'string', description: 'Provider API key' })
316
253
  .option('model', { type: 'string', description: 'Default model id' })
317
254
  .option('base-url', { type: 'string', description: 'Provider API base URL override' }),
318
- handler: async (argv) => {
319
- const action = argv.action;
320
- const aiRuntime = await loadAiRuntime();
321
- const { accessToken, podUrl } = await resolvePodWriteContext(argv.url);
322
- const providerResource = absolutePodUri(podUrl, '/settings/ai/providers.ttl');
323
- const credentialResource = absolutePodUri(podUrl, '/settings/credentials.ttl');
324
- const modelResource = absolutePodUri(podUrl, '/settings/ai/models.ttl');
325
- if (action === 'status') {
326
- const [credentialTurtle, providerTurtle, modelTurtle] = await Promise.all([
327
- fetchResourceText(credentialResource, accessToken),
328
- fetchResourceText(providerResource, accessToken),
329
- fetchResourceText(modelResource, accessToken),
330
- ]);
331
- const states = aiRuntime.buildAIConfigProviderStateMap({
332
- fallbackToCatalogModels: false,
333
- credentialRows: parseCredentialRows(credentialTurtle, credentialResource, aiRuntime),
334
- providerRows: parseProviderRows(providerTurtle, providerResource, aiRuntime),
335
- modelRows: parseModelRows(modelTurtle, modelResource, aiRuntime),
336
- });
337
- const filtered = Object.values(states).filter((state) => {
338
- if (!state.apiKey)
339
- return false;
340
- if (!argv.provider)
341
- return true;
342
- return aiRuntime.sameAIConfigProviderId(argv.provider, state.id);
343
- });
344
- if (filtered.length === 0) {
345
- process.stdout.write('No LinX cloud AI credentials found.\n');
346
- return;
347
- }
348
- const lines = [];
349
- for (const state of filtered) {
350
- lines.push(`provider: ${state.id}`);
351
- if (state.selectedModelId) {
352
- lines.push(`model: ${state.selectedModelId}`);
353
- }
354
- lines.push(`api-key: ${maskSecret(state.apiKey)}`);
355
- lines.push('');
356
- }
357
- process.stdout.write(`${lines.join('\n').trimEnd()}\n`);
358
- return;
359
- }
360
- const providerArg = argv.provider?.trim();
361
- if (!providerArg) {
362
- throw new Error(action === 'disconnect'
363
- ? 'Usage: linx ai disconnect <provider>'
364
- : 'Usage: linx ai connect <provider> --api-key <key>');
365
- }
366
- const provider = aiRuntime.extractAIConfigProviderId(providerArg);
367
- if (action === 'disconnect') {
368
- const providerRef = absolutePodUri(podUrl, `/settings/ai/providers.ttl#${provider}`);
369
- await patchResource(credentialResource, accessToken, buildCredentialDeleteSparql(aiRuntime, providerRef));
370
- process.stdout.write(`Disconnected AI provider: ${provider}\n`);
371
- return;
372
- }
373
- const apiKey = await requireApiKey(argv);
374
- const modelId = argv.model?.trim();
375
- const plan = aiRuntime.buildAIConfigMutationPlan({
376
- providerId: provider,
377
- currentProviderRows: [],
378
- currentCredentialRows: [],
379
- currentModelRows: [],
380
- updates: {
381
- enabled: true,
382
- apiKey,
383
- baseUrl: argv['base-url']?.trim() || undefined,
384
- models: modelId
385
- ? [{
386
- id: modelId,
387
- name: modelId,
388
- enabled: true,
389
- capabilities: [],
390
- }]
391
- : undefined,
392
- },
393
- });
394
- if (plan.providerPayload?.id) {
395
- await patchResource(providerResource, accessToken, buildProviderSparql(aiRuntime, providerResource, podUrl, {
396
- id: plan.providerPayload.id,
397
- baseUrl: plan.providerPayload.baseUrl,
398
- proxyUrl: plan.providerPayload.proxyUrl,
399
- hasModel: plan.providerPayload.hasModel,
400
- }));
401
- }
402
- if (plan.credentialPayload?.id
403
- && plan.credentialPayload.provider
404
- && plan.credentialPayload.service
405
- && plan.credentialPayload.status) {
406
- await patchResource(credentialResource, accessToken, buildCredentialSparql(aiRuntime, credentialResource, podUrl, {
407
- id: plan.credentialPayload.id,
408
- provider: plan.credentialPayload.provider,
409
- service: plan.credentialPayload.service,
410
- status: plan.credentialPayload.status,
411
- apiKey: plan.credentialPayload.apiKey,
412
- baseUrl: plan.credentialPayload.baseUrl,
413
- label: plan.credentialPayload.label,
414
- }));
415
- }
416
- for (const modelPayload of plan.modelUpserts) {
417
- if (!modelPayload.id || !modelPayload.isProvidedBy || !modelPayload.createdAt || !modelPayload.updatedAt) {
418
- continue;
419
- }
420
- await patchResource(modelResource, accessToken, buildModelSparql(aiRuntime, modelResource, podUrl, {
421
- id: modelPayload.id,
422
- displayName: modelPayload.displayName,
423
- modelType: modelPayload.modelType,
424
- isProvidedBy: modelPayload.isProvidedBy,
425
- status: modelPayload.status,
426
- createdAt: modelPayload.createdAt,
427
- updatedAt: modelPayload.updatedAt,
428
- }));
429
- }
430
- const metadata = aiRuntime.getAIConfigProviderMetadata(provider);
431
- process.stdout.write(`Connected AI provider: ${metadata.id}\n`);
432
- if (modelId) {
433
- process.stdout.write(`model: ${aiRuntime.extractAIConfigResourceId(modelId)}\n`);
434
- }
435
- process.stdout.write(`api-key: ${maskSecret(apiKey)}\n`);
436
- },
255
+ handler: async (argv) => runAiCommand(argv),
437
256
  };
438
257
  //# sourceMappingURL=ai-command.js.map