Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ See [STATUS.md](server/STATUS.md) to learn more about which features will remain

## UNRELEASED

- `atomic-cli connect`: connect the CLI with a key made on this machine,
approved in the app (`/app/connect-agent`), instead of pasting your agent
secret. Without a config file, the CLI now points you to it rather than
asking for the secret.

- New default ontology `notifications` (`lib/defaults/notifications.json`):
the `Inbox` and `Notification` classes, and an `inbox` property the private
drive points to its Inbox with.
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

31 changes: 25 additions & 6 deletions browser/CHANGELOG.md

Large diffs are not rendered by default.

85 changes: 85 additions & 0 deletions browser/cli/src/commands/connect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/* eslint-disable no-console */
import chalk from 'chalk';
import { spawn } from 'node:child_process';
import { hostname } from 'node:os';
import {
connectAgentUrl,
enableLoro,
publishAgentName,
waitForGrant,
} from '@tomic/lib';
import { loadOrCreateLocalAgent } from '@tomic/lib/node';
import { store, serverUrl, KEY_TOOL } from '../store.js';

const flag = (args: string[], name: string): string | undefined => {
const i = args.indexOf(name);

return i === -1 ? undefined : args[i + 1];
};

function openInBrowser(url: string) {
const opener =
process.platform === 'darwin'
? 'open'
: process.platform === 'win32'
? 'explorer'
: 'xdg-open';

try {
spawn(opener, [url], { detached: true, stdio: 'ignore' })
.on('error', () => undefined)
.unref();
} catch {
// No browser to open: the printed link is enough.
}
}

/**
* `ad-generate connect`: give this machine its own key for the server in
* atomic.config.json, approved in the app, so private ontologies can be read
* without putting an agent secret in the config file (which often ends up in
* a repository). The key lives in ~/.config/atomic-cli/.
*/
export const connectCommand = async (args: string[]) => {
if (!serverUrl) {
console.error(chalk.red('Set "serverUrl" in atomic.config.json first.'));
process.exitCode = 1;

return;
}

const local = await loadOrCreateLocalAgent(KEY_TOOL, serverUrl);
const name = flag(args, '--name') ?? `ad-generate on ${hostname()}`;
store.setAgent(local!.agent);

// Resources arrive as Loro snapshots; the name lives on one.
await enableLoro();
await publishAgentName(store, name).catch(() => undefined);

const link = connectAgentUrl(flag(args, '--app') ?? serverUrl, {
publicKey: local!.publicKey,
name,
});

console.log(
chalk.cyan(`\nOpen this link to let "${name}" read your ontologies:\n`),
);
console.log(` ${link}\n`);
console.log('Waiting for you to click Allow...');
openInBrowser(link);

try {
await waitForGrant(store, local!.agent.subject!);
} catch (e) {
console.error(chalk.red(e instanceof Error ? e.message : String(e)));
process.exitCode = 1;

return;
}

console.log(
chalk.green(
`\nConnected. The key is stored in ${local!.path}. Revoke it any time under Connected apps in your account settings.`,
),
);
};
3 changes: 2 additions & 1 deletion browser/cli/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ export interface AtomicConfig {
useNamespaces?: boolean;
/**
* [OPTIONAL] The secret of the agent that is used to access your atomic data server. This can also be provided as a command line argument if you don't want to store it in the config file.
* If left empty the public agent is used.
* If left empty, a key from `ad-generate connect` is used, or else the public agent.
* Prefer `ad-generate connect`: this file often ends up in a repository.
*/
agentSecret?: string;
/** HTTP(S) origin used to fetch `atomic:` / `did:ad:` ontology subjects via `/resource?subject=`. */
Expand Down
6 changes: 6 additions & 0 deletions browser/cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ commands.set('ontologies', () =>
),
);

commands.set('connect', () =>
import('./commands/connect.js').then(m =>
m.connectCommand(process.argv.slice(3)),
),
);

commands.set('init', () =>
import('./commands/init.js').then(m => m.initCommand(process.argv.slice(3))),
);
Expand Down
19 changes: 16 additions & 3 deletions browser/cli/src/store.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Agent, Store, type OptionalClass, type Resource } from '@tomic/lib';
import { loadOrCreateLocalAgent } from '@tomic/lib/node';
import { atomicConfig } from './config.js';

const stripTrailingSlash = (url: string): string => url.replace(/\/$/, '');
Expand Down Expand Up @@ -54,12 +55,24 @@ const getAgent = async (): Promise<Agent | undefined> => {
secret = atomicConfig.agentSecret;
}

if (!secret) return undefined;
if (secret) {
return Agent.fromSecret(secret, 'js');
}

// A key from `ad-generate connect`, if this machine has one for the server.
if (serverUrl) {
return (
await loadOrCreateLocalAgent(KEY_TOOL, serverUrl, { create: false })
)?.agent;
}

return Agent.fromSecret(secret, 'js');
return undefined;
};

const serverUrl = resolveServerUrl();
/** Where `ad-generate connect` keeps this machine's key. */
export const KEY_TOOL = 'atomic-cli';

export const serverUrl = resolveServerUrl();

export const store = new Store(serverUrl ? { serverUrl } : {});

Expand Down
2 changes: 2 additions & 0 deletions browser/cli/src/usage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,6 @@ ad-generate <command>
Commands:
ontologies Generates typescript files for ontologies specified in the config file.
init Creates a template config file.
connect Gives this machine its own key for the server, approved in the app,
so private ontologies can be read without an agent secret.
`;
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { InlineFormattedResourceList } from '@components/InlineFormattedResource
import { useResource } from '@tomic/react';
import { MCP_TOOL_NAMES } from '../defaultMCPServers';
import { core, Client } from '@tomic/lib';
import { tryExpandRef } from '@helpers/subjectRefs';
import { tryExpandRef } from '@tomic/react';

interface ToolMessageProps {
part: ToolUIPart | DynamicToolUIPart;
Expand Down
4 changes: 2 additions & 2 deletions browser/data-browser/src/chunks/AI/ClientOnlyTransport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ import { createOllama } from 'ollama-ai-provider-v2';
import { addFieldsIf } from '@helpers/addIf';
import { stringifyTree, useGetDriveStructure } from './useGetDriveStructure';
import { useSettings } from '@helpers/AppSettings';
import { shortenSubject } from '@helpers/subjectRefs';
import { getClassesOnDrive } from './atomicSchemaHelpers';
import { shortenSubject } from '@tomic/react';
import { getClassesOnDrive } from '@tomic/react';
import { createHostedModel } from '@helpers/managed/ai';
import { hostedVoiceModel } from './hostedVoiceModel';

Expand Down
91 changes: 0 additions & 91 deletions browser/data-browser/src/chunks/AI/atomicSchemaHelpers.ts

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import { describe, expect, it, vi } from 'vitest';
import type { Store } from '@tomic/react';
import { processAtomicResources } from './processAtomicResources';

vi.mock('./jsonAdCompact', () => ({
vi.mock('@tomic/react', async importOriginal => ({
...(await importOriginal<typeof import('@tomic/react')>()),
buildClassContext: vi.fn().mockResolvedValue({}),
describeClassCompact: vi.fn(),
toCompact: vi.fn().mockResolvedValue({ name: 'Bread', price: 3 }),
Expand Down
4 changes: 2 additions & 2 deletions browser/data-browser/src/chunks/AI/processAtomicResources.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
// @wc-ignore-file
import type { Store } from '@tomic/react';
import type { AIMessageContext } from './types';
import { shortenRefsDeep } from '@helpers/subjectRefs';
import { shortenRefsDeep } from '@tomic/react';
import {
buildClassContext,
describeClassCompact,
toCompact,
} from './jsonAdCompact';
} from '@tomic/react';
import { getClassContextForAgent } from './resourceContextProviders';

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ import {
type Resource,
type Store,
} from '@tomic/react';
import { shortenSubject } from '@helpers/subjectRefs';
import { shortenSubject } from '@tomic/react';
import { getDocumentContentForAgent } from './getDocumentContentForAgent';
import { hasDocumentContent } from '@chunks/RTE/readDocumentV2TiptapJson';
import { buildClassContext, describeClassCompact } from './jsonAdCompact';
import { buildClassContext, describeClassCompact } from '@tomic/react';
import { getTableContextForAgent } from './tableContextProvider';

const MESSAGE_SAMPLE_LIMIT = 10;
Expand Down
4 changes: 2 additions & 2 deletions browser/data-browser/src/chunks/AI/tableContextProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@ import {
type Resource,
type Store,
} from '@tomic/react';
import { shortenRefsDeep, shortenSubject } from '@helpers/subjectRefs';
import { shortenRefsDeep, shortenSubject } from '@tomic/react';
import {
buildClassContext,
describeClassCompact,
toCompact,
} from './jsonAdCompact';
} from '@tomic/react';

const ROW_SAMPLE_LIMIT = 20;

Expand Down
3 changes: 2 additions & 1 deletion browser/data-browser/src/chunks/AI/updateTableRows.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { expect, it, vi } from 'vitest';
import { core, type Store } from '@tomic/lib';
import { updateTableRows } from './updateTableRows';
vi.mock('./jsonAdCompact', () => ({
vi.mock('@tomic/react', async importOriginal => ({
...(await importOriginal<typeof import('@tomic/react')>()),
buildClassContext: async () => ({}),
resolveKey: (_: unknown, key: string) => ({ subject: key }),
coerceValueIn: (_: unknown, value: unknown) => value,
Expand Down
4 changes: 2 additions & 2 deletions browser/data-browser/src/chunks/AI/updateTableRows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import {
type Resource,
type JSONValue,
} from '@tomic/lib';
import { expandSubject, shortenRefsDeep } from '@helpers/subjectRefs';
import { buildClassContext, resolveKey, coerceValueIn } from './jsonAdCompact';
import { expandSubject, shortenRefsDeep } from '@tomic/react';
import { buildClassContext, resolveKey, coerceValueIn } from '@tomic/react';

export async function updateTableRows(
store: Store,
Expand Down
Loading
Loading